https://github.com/simonsolnes/sarpe
Schwifty Parser Combinators
https://github.com/simonsolnes/sarpe
Last synced: about 1 year ago
JSON representation
Schwifty Parser Combinators
- Host: GitHub
- URL: https://github.com/simonsolnes/sarpe
- Owner: simonsolnes
- Created: 2023-05-09T19:00:35.000Z (about 3 years ago)
- Default Branch: trunk
- Last Pushed: 2024-02-02T20:00:04.000Z (over 2 years ago)
- Last Synced: 2025-06-10T22:04:08.665Z (about 1 year ago)
- Language: Swift
- Size: 20.5 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Sarpe
/sɑːrp/
Haskell-like parser combinators in a Schwifty manner
**Toy project**
If you are looking for something more production-ready, I would reccomend: [davedufresne/SwiftParsec](https://github.com/davedufresne/SwiftParsec)
Most the theory is from Scott Wlaschin's work on parser combinators with F#. I really reccomend his talk ["Understanding parser combinators: a deep dive"](https://www.youtube.com/watch?v=RDalzi7mhdY). It was my first intro to combinators and what peaked my interest.
## What are parser combinators
Parsers combinators are parsers that be semantically combined with other parsers. Such as:
- Presedence: `parser1.preceded(by: parser2)`
- Repitition: `parser1.repeat(4...)`
- Branching: `either(parser1, parser2)`
Parser combinators are also monads, which mean we can bind them to a function that returns a parser, and apply it to the result.
- Map: `parseDigits.map { Int($0) }`
- Optional: `parser.optional()`
- Application: `parser1.apply(parser2)`
The most important parser is `satisfy`, which takes one element and returns `.success` if it matches the predicate function.
*Example*: `satsify {"a"..."z" ~= $0}` (takes a character if it's a lowercase letter)
The whole parser library can be built from `bind` and `satisfy`, but with supporting backtrack prevention, limits and for optimalization reasons, there are a few custom functions.
Combinators are very modular, so one can implement the parts and combine it to a bigger parser.
## Features
- **Swifty**: Focus on creating parsers with Swift's expressiveness rather having a big API surface
- **No operator overloading**: a lot of monadic parser libraries use `<&>`, '>>=`, `<|>` etc. to combine parsers. I prefer words.
- **Few primitives**: Makes it easy to optimize
- **Generic**: Not limited to strings
- **Value oriented**: Parsers are values, and they are immutable.
- **Backtrack prevention**:
- **Buffer limit aware**:
## Examples
### Cat or dog
```swift
enum Animal {
case cat
case dog
}
let parser = either(
literal("cat").to(Animal.cat),
literal("dog").to(Animal.dog)
)
assert(parser.parse("cat") == .success(.cat, ""))
```
### `Bind` example
```swift
enum Number: Equatable {
case signed(Int)
case unsigned(UInt)
}
let number = char("-").optional().bind { minus in
let unsignedNumber = satisfy { "0" ... "9" ~= $0 }
.repeat(0...)
.map { String($0) }
if let minus {
return unsignedNumber
.map { -Int($0)! }
.map { Number.signed($0) }
} else {
return unsignedNumber
.map { UInt($0)! }
.map { Number.unsigned($0) }
}
}
assert(number.parse("3") == .limit(.unsigned(3), ""))
assert(number.parse("-0345somethingElse") == .success(.signed(-345), "somethingElse"))
```
### JSON array
Where `jsonWhitspace` and `jsonValue` are already declared.
```swift
let jsonArray = either(
jsonWhitespace
.preceded(by: char("["))
.terminated(by: char("]"))
.to(JSON.array([])),
serial(
jsonValue(),
jsonValue()
.preceded(by: char(","))
.repeat(0...)
).map { first, rest in
[first] + rest
}
.preceded(by: char("["))
.terminated(by: char("]"))
.map { JSON.array($0) }
)
```