Skip to content

Commit 075d59b

Browse files
feat: add array and dictionary literals
1 parent 73c5598 commit 075d59b

4 files changed

Lines changed: 153 additions & 1 deletion

File tree

Sources/LeafKit/LeafParser.swift

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,74 @@ public class LeafParser {
375375
let expr = try parseExpression(minimumPrecedence: 1)
376376
try expect(token: .expression(.rightParen), while: "parsing parenthesized expression")
377377
return expr
378+
case .leftBracket: // array or dictionary
379+
try consume()
380+
// empty array
381+
if let (endSpan, tok) = try peek(), tok == .expression(.rightBracket) {
382+
try consume()
383+
return .init(.arrayLiteral([]), span: combine(span, endSpan))
384+
}
385+
// empty dictionary
386+
if let (_, tok) = try peek(), tok == .expression(.colon) {
387+
try consume()
388+
let (endSpan, tok) = try expectExpression(while: "parsing end bracket of dictionary literal")
389+
guard tok == .rightBracket else {
390+
throw error(.expectedGot(expected: .expression(.rightBracket), got: .expression(tok), while: "parsing end bracket of dictionary literal"), endSpan)
391+
}
392+
return .init(.dictionaryLiteral([]), span: combine(span, endSpan))
393+
}
394+
// parse the first element
395+
let firstElement = try parseExpression(minimumPrecedence: 0)
396+
// now, whether the next token is a comma or a colon determines if we're parsing an array or dictionary
397+
let (signifierSpan, signifier) = try expectPeekExpression(while: "parsing array or dictionary literal")
398+
if signifier == .comma { // parse an n-item array where n >= 2
399+
400+
var items: [Expression] = [firstElement]
401+
repeat {
402+
try expect(token: .expression(.comma), while: "in the middle of parsing parameters")
403+
items.append(try parseExpression(minimumPrecedence: 0))
404+
} while try peek()?.1 == .expression(.comma)
405+
406+
guard let (endSpan, token) = try read() else {
407+
throw error(.earlyEOF(wasExpecting: "closing bracket for array"), .eof)
408+
}
409+
guard case .expression(.rightBracket) = token else {
410+
throw error(.expectedGot(expected: .expression(.rightBracket), got: token, while: "looking for closing bracket of array"), endSpan)
411+
}
412+
413+
return .init(.arrayLiteral(items), span: combine(span, endSpan))
414+
415+
} else if signifier == .rightBracket { // parse a single-item array
416+
try consume()
417+
return .init(.arrayLiteral([firstElement]), span: combine(span, signifierSpan))
418+
} else if signifier == .colon { // parse an n-item dictionary where n >= 1
419+
try consume()
420+
421+
// parse the first element manually before hitting the loop
422+
let firstValue = try parseExpression(minimumPrecedence: 0)
423+
424+
var pairs: [(Expression, Expression)] = [(firstElement, firstValue)]
425+
426+
while try peek()?.1 == .expression(.comma) {
427+
try consume() // eat comma
428+
let key = try parseExpression(minimumPrecedence: 0)
429+
_ = try expect(token: .expression(.colon), while: "parsing dictionary item")
430+
let value = try parseExpression(minimumPrecedence: 0)
431+
pairs.append((key, value))
432+
}
433+
434+
guard let (endSpan, token) = try read() else {
435+
throw error(.earlyEOF(wasExpecting: "closing bracket for dictionary"), .eof)
436+
}
437+
guard case .expression(.rightBracket) = token else {
438+
throw error(.expectedGot(expected: .expression(.rightBracket), got: token, while: "looking for closing bracket of dictionary"), endSpan)
439+
}
440+
441+
return .init(.dictionaryLiteral(pairs), span: combine(span, endSpan))
442+
} else {
443+
let expected: [LeafScanner.Token] = [.expression(.comma), .expression(.rightBracket), .expression(.colon)]
444+
throw error(.expectedOneOfGot(expected: expected, got: .expression(signifier), while: "parsing array or dictionary literal"), combine(span, signifierSpan))
445+
}
378446
case .operator(let op) where op.data.kind.prefix:
379447
try consume()
380448
let expr = try parseAtom()
@@ -404,7 +472,7 @@ public class LeafParser {
404472
case .boolean(let val):
405473
try consume()
406474
return .init(.boolean(val), span: span)
407-
case .comma, .rightParen:
475+
case .comma, .rightParen, .rightBracket, .colon:
408476
try consume()
409477
throw error(.unexpected(token: .expression(expr), while: "parsing expression atom"), span)
410478
}
@@ -784,6 +852,11 @@ public struct Expression: SExprRepresentable {
784852
return #"(\#(op.rawValue) \#(rhs.sexpr()))"#
785853
case .binary(let lhs, let op, let rhs):
786854
return #"(\#(op.rawValue) \#(lhs.sexpr()) \#(rhs.sexpr()))"#
855+
case .arrayLiteral(let items):
856+
return #"(array_literal \#(items.sexpr()))"#
857+
case .dictionaryLiteral(let pairs):
858+
let inner = pairs.map { "(\($0.0.sexpr()) \($0.1.sexpr()))" }.joined(separator: " ")
859+
return #"(dictionary_literal \#(inner))"#
787860
}
788861
}
789862

@@ -797,6 +870,8 @@ public struct Expression: SExprRepresentable {
797870
case tagApplication(name: Substring, params: [Expression])
798871
case unary(LeafScanner.Operator, Expression)
799872
case binary(Expression, LeafScanner.Operator, Expression)
873+
case arrayLiteral([Expression])
874+
case dictionaryLiteral([(Expression, Expression)])
800875
}
801876
}
802877

Sources/LeafKit/LeafScanner.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ public class LeafScanner {
131131
case decimal(base: Int, digits: Substring)
132132
case leftParen
133133
case rightParen
134+
case leftBracket
135+
case rightBracket
136+
case colon
134137
case comma
135138
case `operator`(Operator)
136139
case identifier(Substring)
@@ -153,6 +156,12 @@ public class LeafScanner {
153156
return ".rightParen"
154157
case .comma:
155158
return ".comma"
159+
case .leftBracket:
160+
return ".leftBracket"
161+
case .rightBracket:
162+
return ".rightBracket"
163+
case .colon:
164+
return ".colon"
156165
case .stringLiteral(let substr):
157166
return ".stringLiteral(\(substr.debugDescription))"
158167
case .boolean(let val):
@@ -429,6 +438,12 @@ public class LeafScanner {
429438
return map(((.init(from: pos, to: self.pos)), .boolean(false)))
430439
}
431440
return map((.init(from: pos, to: self.pos), .identifier(ident)))
441+
case "[":
442+
return map((nextAndSpan(1), .leftBracket))
443+
case "]":
444+
return map((nextAndSpan(1), .rightBracket))
445+
case ":":
446+
return map((nextAndSpan(1), .colon))
432447
case "!" where peekCharacter == "=":
433448
return map((nextAndSpan(2), .operator(.unequal)))
434449
case "!":

Sources/LeafKit/LeafSerialize/ExpressionEvaluation.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,5 +154,17 @@ func evaluateExpression(
154154
throw LeafError(.typeError(shouldHaveBeen: .dictionary, got: val.concreteType ?? .void))
155155
}
156156
return dict[String(field)] ?? .trueNil
157+
case .arrayLiteral(let items):
158+
return .array(try items.map { try eval($0) })
159+
case .dictionaryLiteral(let pairs):
160+
return .dictionary(Dictionary(try pairs.map { data -> (String, LeafData) in
161+
let (key, val) = data
162+
let keyData = try eval(key)
163+
let valData = try eval(val)
164+
guard let str = keyData.coerce(to: .string).string else {
165+
throw LeafError(.typeError(shouldHaveBeen: .string, got: keyData.concreteType ?? .void))
166+
}
167+
return (str, valData)
168+
}, uniquingKeysWith: { $1 }))
157169
}
158170
}

Tests/LeafKitTests/LeafTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,56 @@ final class LeafTests: XCTestCase {
377377
try XCTAssertEqual(render(input), expectation)
378378
}
379379

380+
// Validate parsing and evaluation of array literals
381+
func testArrayLiterals() throws {
382+
let input = """
383+
#for(item in []):#(item)#endfor
384+
#for(item in [1]):#(item)#endfor
385+
#for(item in ["hi"]):#(item)#endfor
386+
#for(item in [1, "hi"]):#(item)#endfor
387+
"""
388+
389+
let syntax = """
390+
(for (array_literal) (substitution(variable))) (raw)
391+
(for (array_literal (integer)) (substitution(variable))) (raw)
392+
(for (array_literal(string)) (substitution(variable))) (raw)
393+
(for (array_literal(integer) (string)) (substitution(variable)))
394+
"""
395+
396+
let expectation = """
397+
398+
1
399+
hi
400+
1hi
401+
"""
402+
403+
let parsed = try parse(input)
404+
assertSExprEqual(parsed.sexpr(), syntax)
405+
406+
try XCTAssertEqual(render(input), expectation)
407+
}
408+
409+
// Validate parsing and evaluation of dictionary literals
410+
func testDictionaryLiterals() throws {
411+
let input = """
412+
#with(["hi": "world"]):#(hi)#endwith
413+
"""
414+
415+
let syntax = """
416+
(with (dictionary_literal ((string)(string)))
417+
(substitution(variable)))
418+
"""
419+
420+
let expectation = """
421+
world
422+
"""
423+
424+
let parsed = try parse(input)
425+
assertSExprEqual(parsed.sexpr(), syntax)
426+
427+
try XCTAssertEqual(render(input), expectation)
428+
}
429+
380430
// Validate parse resolution of evaluable expressions
381431
func testComplexParameters() throws {
382432
let input = """

0 commit comments

Comments
 (0)