Skip to content

Commit

Permalink
make a bunch of changes to get party to 1.0; see CHANGELOG.md
Browse files Browse the repository at this point in the history
  • Loading branch information
RyanBrewer317 committed Mar 21, 2024
1 parent d7e2b25 commit 1be755a
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 32 deletions.
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2024-03-21

### Added

- Parse errors return positions finally!
- `digits` convenience function
- `many_concat` convenience function
- `many1_concat` convenience function
- A little demo in the README

### Changed

- Update dependencies for Gleam 1.0!
- `alt` renamed to `either`.

### Fixed

- Make pattern matching "exhaustive" with explicit panics for illegal inputs. This is required to make `party` compile with Gleam 1.0.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@
[![Package Version](https://img.shields.io/hexpm/v/party)](https://hex.pm/packages/party)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/party/)

A Gleam project
A little parser combinator library for Gleam!

A little demo:
```gleam
fn identstring() -> Parser(String, e) {
use first <- do(lowercase_letter())
use rest <- do(many_concat(either(alphanum(), char("_"))))
return(first <> rest)
}
```

## Installation

Expand Down
5 changes: 3 additions & 2 deletions gleam.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
name = "party"
version = "0.1.3"
version = "1.0.0"
description = "A little parser combinator library"

licences = ["GPL-3.0-or-later"]
repository = { type = "github", user = "RyanBrewer317", repo = "party" }
gleam = ">= 0.32.0"
# links = [{ title = "Website", href = "https://gleam.run" }]

[dependencies]
gleam_stdlib = "~> 0.30"
gleam_stdlib = "~> 0.36"

[dev-dependencies]
gleeunit = "~> 0.10"
4 changes: 2 additions & 2 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# You typically do not need to edit this file

packages = [
{ name = "gleam_stdlib", version = "0.30.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "704258528887F95075FFED7AAE1CCF836A9B88E3AADA2F69F9DA15815F94A4F9" },
{ name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
{ name = "gleeunit", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "1397E5C4AC4108769EE979939AC39BF7870659C5AFB714630DEEEE16B8272AD5" },
]

[requirements]
gleam_stdlib = { version = "~> 0.30" }
gleam_stdlib = { version = "~> 0.36" }
gleeunit = { version = "~> 0.10" }
104 changes: 78 additions & 26 deletions src/party.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import gleam/result
/// adding a `int.parse` call into your parser pipeline.
/// See `try` for using this feature.
pub type ParseError(e) {
Unexpected(error: String)
UserError(error: e)
Unexpected(pos: Position, error: String)
UserError(pos: Position, error: e)
}

/// The type for positions within a string.
Expand Down Expand Up @@ -64,9 +64,9 @@ pub fn satisfy(when pred: fn(String) -> Bool) -> Parser(String, e) {
"\n" -> Ok(#(h, t, Position(row + 1, 0)))
_ -> Ok(#(h, t, Position(row, col + 1)))
}
False -> Error(Unexpected(h))
False -> Error(Unexpected(pos, h))
}
[] -> Error(Unexpected("EOF"))
[] -> Error(Unexpected(pos, "EOF"))
}
})
}
Expand All @@ -83,7 +83,7 @@ pub fn uppercase_letter() -> Parser(String, e) {

/// Parse a lowercase or uppercase letter.
pub fn letter() -> Parser(String, e) {
alt(lowercase_letter(), uppercase_letter())
either(lowercase_letter(), uppercase_letter())
}

/// Parse a specific character.
Expand All @@ -96,16 +96,21 @@ pub fn digit() -> Parser(String, e) {
satisfy(fn(c) { string.contains("0123456789", c) })
}

/// Parse a sequence of digits.
pub fn digits() -> Parser(String, e) {
many1_concat(digit())
}

/// Parse the first parser, or the second if the first fails.
pub fn alt(p: Parser(a, e), q: Parser(a, e)) -> Parser(a, e) {
pub fn either(p: Parser(a, e), q: Parser(a, e)) -> Parser(a, e) {
Parser(fn(source, pos) { result.or(run(p, source, pos), run(q, source, pos)) })
}

/// Parse with the first parser in the list that doesn't fail.
pub fn choice(ps: List(Parser(a, e))) -> Parser(a, e) {
Parser(fn(source, pos) {
case ps {
[] -> Error(Unexpected("choiceless choice"))
[] -> panic as "choice doesn't accept an empty list of parsers" // TODO: should this be an Unexpected instead?
[p] -> run(p, source, pos)
[p, ..t] ->
case run(p, source, pos) {
Expand All @@ -118,19 +123,17 @@ pub fn choice(ps: List(Parser(a, e))) -> Parser(a, e) {

/// Parse an alphanumeric character.
pub fn alphanum() -> Parser(String, e) {
alt(digit(), letter())
either(digit(), letter())
}

/// Parse zero or more whitespace characters.
pub fn whitespace() -> Parser(String, e) {
many(choice([char(" "), char("\t"), char("\n")]))
|> map(string.concat)
many_concat(choice([char(" "), char("\t"), char("\n")]))
}

/// Parse one or more whitespace characters.
pub fn whitespace1() -> Parser(String, e) {
many1(choice([char(" "), char("\t"), char("\n")]))
|> map(string.concat)
many1_concat(choice([char(" "), char("\t"), char("\n")]))
}

/// Keep trying the parser until it fails, and return the array of parsed results.
Expand All @@ -140,35 +143,73 @@ pub fn many(p: Parser(a, e)) -> Parser(List(a), e) {
case run(p, source, pos) {
Error(_) -> Ok(#([], source, pos))
Ok(#(x, r, pos2)) ->
result.map(
run(many(p), r, pos2),
fn(res) { #([x, ..res.0], res.1, res.2) },
)
result.map(run(many(p), r, pos2), fn(res) {
#([x, ..res.0], res.1, res.2)
})
}
})
}

/// Parse a certain string as many times as possible, returning everything that was parsed.
/// This cannot fail because it parses zero or more times!
pub fn many_concat(p: Parser(String, e)) -> Parser(String, e) {
many(p) |> map(string.concat)
}

/// Keep trying the parser until it fails, and return the array of parsed results.
/// This can fail, because it must parse successfully at least once!
pub fn many1(p: Parser(a, e)) -> Parser(List(a), e) {
Parser(fn(source, pos) {
case run(p, source, pos) {
Error(e) -> Error(e)
Ok(#(x, r, pos2)) ->
result.map(
run(many(p), r, pos2),
fn(res) { #([x, ..res.0], res.1, res.2) },
)
result.map(run(many(p), r, pos2), fn(res) {
#([x, ..res.0], res.1, res.2)
})
}
})
}

/// Parse a certain string as many times as possible, returning everything that was parsed.
/// This can fail, because it must parse successfully at least once!
pub fn many1_concat(p: Parser(String, e)) -> Parser(String, e) {
many1(p) |> map(string.concat)
}

/// Do the first parser, ignore its result, then do the second parser.
pub fn seq(p: Parser(a, e), q: Parser(b, e)) -> Parser(b, e) {
use _ <- do(p)
q
}

/// Parse a sequence separated by the given separator parser.
pub fn sep(parser: Parser(a, e), by s: Parser(b, e)) -> Parser(List(a), e) {
use mb_a <- do(perhaps(parser))
case mb_a {
Ok(a) -> {
use res <- do(perhaps(s))
case res {
Ok(_) -> {
use rest <- do(sep(parser, by: s))
return([a, ..rest])
}
Error(Nil) -> return([a])
}
}
Error(Nil) -> return([])
}
}

/// Parse a sequence separated by the given separator parser.
/// This only succeeds if at least one element of the sequence was parsed.
pub fn sep1(parser: Parser(a, e), by s: Parser(b, e)) -> Parser(List(a), e) {
use sequence <- do(sep(parser, by: s))
case sequence {
[] -> fail()
_ -> return(sequence)
}
}

/// Do `p`, then apply `f` to the result if it succeeded.
pub fn map(p: Parser(a, e), f: fn(a) -> b) -> Parser(b, e) {
Parser(fn(source, pos) {
Expand All @@ -188,7 +229,7 @@ pub fn try(p: Parser(a, e), f: fn(a) -> Result(b, e)) -> Parser(b, e) {
Ok(#(x, r, pos2)) ->
case f(x) {
Ok(a) -> Ok(#(a, r, pos2))
Error(e) -> Error(UserError(e))
Error(e) -> Error(UserError(pos2, e))
}
Error(e) -> Error(e)
}
Expand All @@ -203,8 +244,8 @@ pub fn error_map(p: Parser(a, e), f: fn(e) -> f) -> Parser(a, f) {
Ok(res) -> Ok(res)
Error(e) ->
case e {
UserError(e) -> Error(UserError(f(e)))
Unexpected(s) -> Error(Unexpected(s))
UserError(pos, e) -> Error(UserError(pos, f(e)))
Unexpected(pos, s) -> Error(Unexpected(pos, s))
}
}
})
Expand All @@ -228,6 +269,7 @@ pub fn all(ps: List(Parser(a, e))) -> Parser(a, e) {
use _ <- do(h)
all(t)
}
_ -> panic as "all(parsers) doesn't accept an empty list of parsers" // TODO: should this be an Unexpected instead?
}
}

Expand All @@ -244,11 +286,11 @@ pub fn string(s: String) -> Parser(String, e) {
}

/// Negate a parser: if it succeeds, this fails, and vice versa.
/// Example: `seq(string("if"), not(alt(alphanum(), char("_"))))`
/// Example: `seq(string("if"), not(either(alphanum(), char("_"))))`
pub fn not(p: Parser(a, e)) -> Parser(Nil, e) {
Parser(fn(source, pos) {
case run(p, source, pos) {
Ok(_) -> Error(Unexpected(""))
Ok(_) -> Error(Unexpected(pos, ""))
// todo: better error message here (add a label system)
Error(_) -> Ok(#(Nil, source, pos))
}
Expand All @@ -260,7 +302,7 @@ pub fn end() -> Parser(Nil, e) {
Parser(fn(source, pos) {
case source {
[] -> Ok(#(Nil, source, pos))
[h, ..] -> Error(Unexpected(h))
[h, ..] -> Error(Unexpected(pos, h))
}
})
}
Expand Down Expand Up @@ -298,3 +340,13 @@ pub fn do(p: Parser(a, e), f: fn(a) -> Parser(b, e)) -> Parser(b, e) {
pub fn return(x) {
Parser(fn(source, pos) { Ok(#(x, source, pos)) })
}

/// Immediately fail regardless of the next input
pub fn fail() -> Parser(a, e) {
Parser(fn(source, pos) {
case source {
[] -> Error(Unexpected(pos, "EOF"))
[h, .._t] -> Error(Unexpected(pos, h))
}
})
}
3 changes: 2 additions & 1 deletion test/party_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ pub fn pos_test() {

pub fn demorgan_test() {
// not a perfect test because it's just on the input "a" but it's something.
// TODO: fuzz test this with a bunch of generated strings. It should work on any, I think.
party.go(party.seq(party.not(party.digit()), party.not(party.letter())), "a")
|> should.equal(party.go(
party.not(party.alt(party.digit(), party.letter())),
party.not(party.either(party.digit(), party.letter())),
"a",
))
}

0 comments on commit 1be755a

Please sign in to comment.