Skip to content

2.0.0

Latest

Choose a tag to compare

@github-actions github-actions released this 01 Oct 17:45
Immutable release. Only release title and notes can be modified.
0c67c54

roc-parser 2.0.0

2.0.0 is a redesign. Every module has a new API that follows the Roc
language reference: type modules, methods called with a dot, Try with an
open error tag named after the format, and Try(_, [Missing]) for optional
values. CSV rows and YAML documents can now be decoded straight into your own
record types with parser_for. Every format parser was also rewritten to
follow its specification and checked against a mature reference
implementation, so many inputs now parse differently.

Code written for 1.x does not compile against 2.0.0 without changes. Start
with the upgrade guide below, then read the behaviour changes, which can
affect your data even where your code compiles unchanged.

Upgrade guide

Each table maps a 1.x name to its 2.0.0 replacement. A dash means the item
was removed; the note says what to use instead.

Parser

1.x 2.0.0
ParseResult(input, a) : Try({ val, input }, [ParsingFailure(Str)]) ParseResult(input, a) : Try({ value, rest }, [ParseError({ message, offset })])
build_primitive_parser(fun) Parser.custom(fun), where fun returns Ok({ value, rest }) or Err(ParseError({ message, offset }))
parse_partial(p, input) p.run(input)
parse(p, input, is_done) p.parse(input): the completion predicate is gone; leftover input is a ParseError
[ParsingFailure(Str), ParsingIncomplete(input)] [ParseError({ message : Str, offset : U64 })]
apply(fun_parser, arg_parser) fun_parser.keep(arg_parser)
sep_by1 sep_by_one_or_more
maybe(p) : Parser(input, Try(a, [Nothing])) p.maybe() : Parser(input, Try(a, [Missing]))
Internal and_then p.and_then(fun), now public and documented

fail, custom, run, parse, one_of, flatten, many, one_or_more,
sep_by and sep_by_one_or_more carry where [input.len : input -> U64].
List(U8) and every input type in this package satisfy it; a custom input
type must provide len.

String is now Utf8

1.x 2.0.0
import parser.String import parser.Utf8
String.Utf8 (the input type) Utf8.Bytes, still List(U8)
String.parse_str Utf8.parse_str
String.parse_str_partial, returning { val, input } Utf8.parse_str_partial, returning { value, rest }
String.parse_utf8 Utf8.parse_bytes
String.parse_utf8_partial Utf8.parse_bytes_partial, returning { value, rest }
[ParsingFailure(Str), ParsingIncomplete(...)] [ParseError({ message : Str, offset : U64 })]
String.any_thing Utf8.rest
String.any_string Utf8.rest_str
String.one_of - (use Parser.one_of)
String.str_from_utf8 - (use Str.from_utf8 or Str.from_utf8_lossy)
String.str_from_ascii - (it crashed on non-ASCII bytes; use Str.from_utf8_lossy)

codeunit, codeunit_satisfies, utf8, string, any_codeunit, digit
and digits keep their names.

CSV

1.x 2.0.0
CSV (opaque), CSVRecord, CSVField CSV.Record : List(List(U8))
parse_str(p, text) with [ParsingFailure(Str), SyntaxError(Str), ParsingIncomplete(CSVRecord)] CSV.parse_with(p, text) with [InvalidCsv(CSV.Error)]
parse_str_to_csv(text) CSV.parse_records(text) : Try(List(Record), [InvalidCsv(Error)])
parse_csv(p, csv) CSV.decode(p, records)
parse_str_to_csv_record, parse_csv_record - (use CSV.parse_records, then CSV.decode or p.parse(record))
file CSV.parser : Parser(Utf8.Bytes, List(Record))
field : Parser(String.Utf8, a) -> Parser(CSVRecord, a) CSV.field : Parser(Utf8.Bytes, a) -> Parser(Record, a)

CSV.Error is { record, field, line, column, message }. record, string,
u64 and f64 keep their names.

HTTP

1.x 2.0.0
Header : [Header(Str, Str)] Header : { name : Str, value : Str }, with HTTP.header(headers, name) for a case-insensitive lookup
HttpVersion Version : { major : U8, minor : U8 }
Request.uri Request.target
Request.http_version, Response.http_version .version
Response.status Response.reason
Method Method, plus Extension(Str) for any other method token
String.parse_str(HTTP.request, text) HTTP.parse_request(bytes) : Try({ request, rest }, [InvalidHttp(Error)])
String.parse_str(HTTP.response, text) HTTP.parse_response(bytes) : Try({ response, rest }, [InvalidHttp(Error)])

HTTP.Error is { offset, message }. HTTP.request and HTTP.response
remain as parsers for embedding; their failures now carry the byte offset of
the problem.

Markdown

1.x 2.0.0
String.parse_str(Markdown.all, text) Markdown.parse_str(text) : List(Markdown); parsing cannot fail
String.parse_str(Markdown.inlines, text) Markdown.parse_inlines(text) : List(Inline)
Markdown.all, Markdown.inlines (parsers) Markdown.parser, Markdown.inline_parser
Frontmatter({ raw : Str }) Frontmatter(Str), and Markdown.frontmatter(blocks) : Try(Str, [Missing])
LinkTarget.title : [Some(Str), None] LinkTarget.title : Try(Str, [Missing])
TODO(Str) variant - (never produced by the CommonMark parser)
Markdown.heading, link, image, code - (parse the document or the inlines and match on the tree)
to_debug_str, inline_to_debug_str - (use Str.inspect)

Xml

1.x 2.0.0
xml_parser Xml.parse_str(text) : Try(Xml, [InvalidXml(Xml.Error)]), or Xml.parser to embed
xml_declaration : [Given(Declaration), Missing] declaration : Try(Declaration, [Missing])
Declaration.encoding : [Given(TextEncoding), Missing] encoding : Try(TextEncoding, [Missing])
Version (opaque, Version.new(after_dot)) Version : { major : U64, minor : U64 }
Element(name, attributes, children) Element({ name, attributes, children })

Xml.Error is { line, column, message }.

Yaml

1.x 2.0.0
String(Str) variant Text(Str)
YamlError(Yaml.Error) InvalidYaml(Yaml.Error)

Breaking changes

One error convention

Every format reports invalid input with an open tag named after the format,
holding a record with a message and the most precise location the format
has:

  • InvalidCsv({ record, field, line, column, message })
  • InvalidYaml({ line, column, message })
  • InvalidXml({ line, column, message })
  • InvalidHttp({ offset, message })

Combinator runners (Utf8.parse_str, parse_bytes and the partial runners,
Parser.parse and Parser.run) return ParseError({ message, offset }).
The offset is the byte offset of the furthest failure: the failure that
read furthest into the input, even when a repetition stopped at it or an
alternative recovered from it. A bad element in the middle of many or
sep_by is reported as its own failure rather than as leftover input, and
leftover input after a successful parse is unexpected input at the
leftover. Because every error is an open tag union, ? composes them in one
function without map_err.

Messages quote at most 32 bytes of the input, decoded lossily, so they stay
short and cannot crash on bytes that are not UTF-8. Utf8.codeunit,
Utf8.codeunit_satisfies, Utf8.utf8 and Utf8.string no longer quote the
input at all ("expected char ," rather than "expected char , but found
x. While reading: ..."): their messages are built once, when the parser is
made, because building one on every failure made alternatives and
repetitions several times slower. The offset says where the failure is.
Utf8.digits followed by leftover input reports unexpected input at the
leftover. alt keeps one failure instead of joining messages with "or".
Code that matches on message text may need updating.

Optional values are Try(_, [Missing])

Parser.maybe, the XML declaration and its encoding, and Markdown link
titles all use Try(a, [Missing]), as the language reference recommends.
The same type marks optional fields for type-directed decoding.

Removed

  • Parser.apply, build_primitive_parser, parse_partial, sep_by1.
  • String (now Utf8), String.one_of, str_from_utf8, str_from_ascii.
  • The opaque CSV type, CSVRecord, CSVField, parse_csv,
    parse_csv_record, parse_str_to_csv, parse_str_to_csv_record, file,
    and the SyntaxError, ParsingFailure and ParsingIncomplete errors.
  • HTTP.HttpVersion and the Header(name, value) tag.
  • Markdown.all, inlines, heading, link, image, code,
    to_debug_str, inline_to_debug_str and the TODO variant.
  • Xml.xml_parser, Version.new and the Given tag.
  • Yaml's String variant and YamlError.

Utf8, CSV and HTTP are void modules: opaque types backed by [], as the
language reference recommends for modules that hold no values of their own.

New dependency

roc-parser now depends on
roc-lang/unicode 4.2.0, for the
Unicode data CommonMark needs (punctuation and whitespace categories, case
folding). Roc fetches it with the package; no code change is needed.

New features

Type-directed decoding with parser_for

  • CSV.parse(text) decodes a file with a header row into a list of records.
    The row type comes from your annotation: each field reads the column with
    the same name, a Try(a, [Missing]) field is Err(Missing) when its column
    is absent, and a Try(a, [Null]) field is Err(Null) when its cell is
    empty. Cells decode as Str, Bool, any integer type, Dec, F32, F64
    and tags without payloads.

  • CSV.parse_normalized matches headers such as First Name or first-name
    to a field first_name, and CSV.parse_headerless decodes rows without a
    header into tuples.

  • Yaml.decode(text) decodes a document into a record, list, dict or tuple.
    Plain scalars are resolved for the target type, so version: 1.10 decodes
    into a Str field as written. Yaml.decoder takes options for kebab-case
    or camelCase keys and for rejecting unknown keys (by default they are
    skipped).

  • A required field that is absent fails with MissingRequiredField(name),
    which the compiler adds to the error type, so annotations name it:

    people : Try(List({ name : Str, age : U64 }), [InvalidCsv(CSV.Error), MissingRequiredField(Str)])
    people = CSV.parse("name,age\nAda,36\n")
  • CSV.Parseable and Yaml.Parseable are where-clause aliases for writing
    your own generic functions over decodable types.

Other additions

  • CSV.split_header separates a header row from raw records.
  • HTTP.header(headers, name) looks a header up case-insensitively.
  • HTTP.parse_request and HTTP.parse_response take List(U8) and return
    the message with the rest of the bytes, which is the next pipelined
    message. HTTP.parse_requests reads a whole pipeline.
  • HTTP.Method gains Extension(Str) for any RFC 9110 method token.
  • Markdown.parse_str, parse_inlines, parser, inline_parser and
    Markdown.frontmatter.
  • Xml.parse_str, with a one-based line and byte column on every error, and
    the node methods name, attribute, children_named and text.
  • Yaml.parser and the tree methods get, at, get_path, as_str,
    as_i64, as_bool and as_list.
  • to_hash on the Markdown, Xml and Yaml tree types, so trees can be
    Dict keys or Set members.
  • Yaml.to_inspect escapes control characters.
  • Parser.span runs a parser and returns the input it consumed, as a slice.
  • Utf8.ByteClass (from_bytes, from_predicate, complement,
    contains) with Utf8.find_any, Utf8.skip_class, Utf8.find_line_end
    and the Utf8.span_class parser find or skip runs of bytes 16 at a time
    with SIMD instructions.

Behaviour changes

CSV

  • The dialect is RFC 4180 with the relaxations of Python's csv module
    (strict mode). A final line break no longer starts an extra record, and
    empty input has no records instead of [[""]]. A lone CR separates records,
    alongside LF and CRLF.
  • Fields may hold any UTF-8, tabs, NUL and control characters, and a " that
    is not at the start of a field is an ordinary character. These were syntax
    errors. A blank line is a record with one empty field.
  • A quoted field followed by anything other than ,, a line break or the
    end of input is an error.
  • CSV.u64 and CSV.f64 use a data grammar instead of Roc literal syntax:
    1_000, 0x10 and -0 are no longer U64 values, 1_0.5 and 0x1p3 are
    no longer F64 values, and F64 accepts inf, infinity and nan in any
    case, with 1e400 saturating to infinity instead of failing.
  • With CSV.parse_with and CSV.decode, a record with fields the parser did
    not read is an InvalidCsv error naming the record. With CSV.parse, every
    record must have exactly as many fields as the header; blank lines are
    skipped.

HTTP

  • Requests and responses are parsed per RFC 9112 and RFC 9110. The body is
    delimited by Content-Length or decoded from chunked coding, and the
    bytes after the message are left for the next pipelined message. Before,
    every remaining byte became the body.
  • To rule out request smuggling, these are rejected: both Content-Length
    and Transfer-Encoding; conflicting, malformed or overflowing
    Content-Length; any transfer coding other than a single chunked;
    Transfer-Encoding in HTTP/1.0; a missing or duplicate Host in HTTP/1.1;
    whitespace before a field's colon; obs-fold; bare CR or LF line endings; and
    control characters in field values.
  • Status codes are exactly three digits, 100 to 999 (before, 65736 was read
    as 200), and an empty reason phrase is allowed. The version is
    DIGIT "." DIGIT, field names must be tokens, field values are trimmed of
    surrounding whitespace, and request targets must be visible ASCII.
  • Any method token is accepted, as Extension(name) when it is not one of
    the standard methods. Method names are case-sensitive, so get is
    Extension("get").

YAML

  • Plain scalars resolve with the YAML 1.2 core schema. Only null, Null,
    NULL and ~ are null and only true/True/TRUE and
    false/False/FALSE are booleans, so nUlL and tRUE are now text.
    0o octal, 0x hexadecimal, .inf and .nan are numbers, and values such
    as 1.2.3 or 1e are text instead of errors.
  • Literal and folded block scalars (|, >, with chomping and indentation
    indicators), all double-quoted escapes, nested compact sequences (- - x)
    and sequences at their key's indentation (steps:\n- a) are supported.
  • Invalid YAML that was accepted, sometimes as a wrong value, is rejected:
    characters outside YAML's printable set, block collections on the ---
    line, plain scalars starting with an indicator or containing : , text
    after a closing quote, and keys starting with anchors, tags or other
    indicators. A leading byte order mark is dropped.
  • Quotes and brackets inside plain scalars are text (k: it's # comment is
    it's), a lone CR is a line break, and flow collections count toward the
    100-level nesting limit.
  • Error columns count bytes and point at the value itself rather than the
    space before it: an unterminated flow collection at port: [8080 is now
    reported at column 7, not 6.

XML

  • Documents are checked for XML 1.0 well-formedness: end tags must match
    their start tags, and < or a bare & in attribute values, ]]> and
    control characters in text, duplicate attributes, and attributes without
    whitespace between them are rejected.
  • Names may contain digits and non-ASCII letters (<h1> parses). Entity and
    character references are decoded, and line endings and attribute values are
    normalised as the specification requires.
  • Comments and processing instructions are accepted wherever the
    specification allows and dropped from the tree. A DOCTYPE is rejected as
    unsupported.
  • Encoding names are compared case-insensitively, so encoding="utf-8" is
    Utf8Encoding, and any 1.x version is accepted.

Markdown

  • The parser was rebuilt on the CommonMark 0.31.2 parsing strategy, with the
    GitHub Flavored Markdown tables, task lists, strikethrough and extended
    autolinks. Against the CommonMark spec examples it now agrees with
    cmark-gfm on block structure apart from documented oracle quirks; many
    documents give a different, specification-conforming tree.
  • An unclosed code fence runs to the end of the document, changing the bullet
    character starts a new list, and a pipe inside a table cell's code span must
    be escaped as \|.
  • A soft line break inside a paragraph is kept as "\n" inside Text.
  • Block quotes and lists nest at most 1,000 levels deep, and so do emphasis,
    strikethrough, links and images; deeper markers, delimiters and brackets
    are read as text, so very deep input no longer overflows the stack.
  • Emphasis follows the delimiter-run rules, links and images support inline,
    full, collapsed and shortcut references, HTML5 named and numeric character
    references are decoded, whitespace inside <...> destinations is trimmed,
    and GFM www. autolinks get an http:// prefix.
  • Raw HTML and link destinations pass through unfiltered. Sanitize them
    before rendering untrusted Markdown to HTML.

Fixes

  • Crashes: a non-UTF-8 byte in an HTTP header value or request target, a
    non-UTF-8 CSV field in an error message, and an unsupported YAML escape
    before a multi-byte character no longer abort the process.
  • Hangs: many, one_or_more and sep_by stop when an element succeeds
    without consuming input, instead of looping forever.
  • Wrong results: YAML quoted scalars end at the real closing quote, and
    - key: value takes its indentation from the content; XML <a></b> is
    rejected; Markdown keeps \v and \f at line ends.
  • Quadratic cases, now linear: failure messages that quoted the whole
    remaining input; XML duplicate-attribute checks and sibling lists; YAML
    documents with many block scalars, keys or compact sequence entries, and
    deeply nested flow collections; Markdown pathological inline patterns, long
    blocks and lists, and deep container nesting.

Performance

The quadratic cases listed under Fixes are gone. The CSV, HTTP and XML
parsers scan with SIMD (Utf8.ByteClass), and XML cuts names, text and
attribute values from the validated input string instead of validating each
one. The
Performance chapter
compares the parsers with Go, Rust and Python libraries, states the
complexity guarantees and the known super-linear cases, and gives tips; the
benchmark harness and comparators are described in the Benchmarks chapter.

Documentation and tooling

  • A new manual covers each format, error reporting, choosing an approach,
    the design's background, compatibility, conformance, performance,
    contributing, property testing and maintenance. Every Roc example in it is
    compiled, tested and run in CI.
  • The documentation site moved. GitHub Pages now serves only the current
    release's manual and API reference, rebuilt from each release, so links to
    the old /<version>/ pages no longer work. Each release carries its own
    manual (HTML and PDF) and API reference as release assets.
  • Property-test targets built on
    roc-fuzz 0.4.2 cover every
    module: raw-input targets with metamorphic relations, structured generators
    that build the expected result from the specification, and targets that
    inject known violations (malformed XML, HTTP request smuggling). CI runs a
    smoke test of each on every pull request and longer scheduled campaigns.
  • Conformance reviews compare each format with a pinned reference
    implementation and its test suite: Python's csv module, expat and the W3C
    XML Conformance Test Suite, ruamel.yaml and the yaml-test-suite, h11, and
    cmark-gfm with markdown-it-py on the CommonMark spec examples. Every allowed
    disagreement is recorded in a baseline with a reason.
  • The examples in examples/ now use the package source. Each release
    attaches them as roc-parser-examples-<version>.zip, pinned to its bundle,
    instead of a follow-up pull request rewriting their URLs.
  • A benchmark harness compares the parsers with Go, Rust and Python
    libraries, weekly and on demand.

Thanks

Thanks to @albx79 for the block scalar parsing
in #80 that
started the YAML work in this release.

Using this release

Built and tested with Roc nightly-2026-09-29-7f11a82 (https://github.com/roc-lang/nightlies/releases/tag/nightly-2026-09-29-7f11a82); install that nightly to use it.

app [main!] {
    # your platform here
    parser: "https://github.com/lukewilliamboswell/roc-parser/releases/download/2.0.0/7CLzCK6qUz7zmj6nvBxMEFu11HPwQTnCovKiyWzDSLTW.tar.zst",
}

Roc API changes

Comparing https://github.com/lukewilliamboswell/roc-parser/releases/download/1.2.0/GzeZxk7V7GHFa42qhgzd8gUgX6cEyY3NmrwmDfsuskNd.tar.zst (old, 1.2.0) with package/main.roc (new)...

---- CSV - MAJOR ----

    - CSV.CSVField : (alias for String.Utf8)
    - CSV.CSVRecord : (alias for List(CSV.CSVField))
    + CSV.Error : (alias for { column : Num.U64, field : Num.U64, line : Num.U64, message : Str, record : Num.U64 })
    + CSV.Format : (opaque type)
    + CSV.Format.invalid_value : CSV.Format, CSV.State -> [InvalidCsv({ column : Num.U64, field : Num.U64, line : Num.U64, message : Str, record : Num.U64 })]a
    + CSV.Format.parse_bool : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Bool }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_dec : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.Dec }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_f32 : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.F32 }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_f64 : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.F64 }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_i128 : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.I128 }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_i16 : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.I16 }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_i32 : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.I32 }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_i64 : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.I64 }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_i8 : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.I8 }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_null : CSV.Format, CSV.State -> Try(CSV.State, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_record_after_field : CSV.Format, CSV.State -> Try([Continue(CSV.State), Done(CSV.State)]a, [InvalidCsv(CSV.Error)]b)
    + CSV.Format.parse_record_field : CSV.Format, Encoding.FieldName.FieldNames(_shape), CSV.State -> Try([Continue(CSV.State), Done(CSV.State), Field({ field : Encoding.FieldName(_shape), rest : CSV.State }), TryField({ name : Str, rest : CSV.State }), TryFieldCaseless({ name : Str, rest : CSV.State })]b, [InvalidCsv(CSV.Error)]c)
    + CSV.Format.parse_record_start : CSV.Format, CSV.State -> Try([Counted({ len : Num.U64, rest : CSV.State }), Uncounted(CSV.State)]a, [InvalidCsv(CSV.Error)]b)
    + CSV.Format.parse_str : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Str }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_tag_union : CSV.Format, Encoding.ParseTagUnionSpec(a), CSV.State -> Try({ rest : CSV.State, value : a }, [InvalidCsv({ column : Num.U64, field : Num.U64, line : Num.U64, message : Str, record : Num.U64 })]b)
    + CSV.Format.parse_tuple_end : CSV.Format, CSV.State, Num.U64 -> Try(CSV.State, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_tuple_next : CSV.Format, CSV.State, Num.U64, Num.U64 -> Try(CSV.State, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_tuple_start : CSV.Format, CSV.State, Num.U64 -> Try(CSV.State, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_u128 : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.U128 }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_u16 : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.U16 }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_u32 : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.U32 }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_u64 : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.U64 }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.parse_u8 : CSV.Format, CSV.State -> Try({ rest : CSV.State, value : Num.U8 }, [InvalidCsv(CSV.Error)]a)
    + CSV.Format.rename_field : CSV.Format, Str -> Str
    + CSV.Format.skip_record_field : CSV.Format, CSV.State -> Try(CSV.State, [InvalidCsv(CSV.Error)]a)
    + CSV.Record : (alias for List(List(Num.U8)))
    + CSV.State : (opaque type)
    + CSV.decode : Parser(CSV.Record, a), List(CSV.Record) -> Try(List(a), [InvalidCsv(CSV.Error)]b)
    - CSV.field : Parser(String.Utf8, a) -> Parser(List(CSV.CSVField), a)
    + CSV.field : Parser(Utf8.Bytes, a) -> Parser(List(List(Num.U8)), a)
    - CSV.file : Parser(String.Utf8, CSV)
    - CSV.is_eq : a
    + CSV.parse : Str -> Try(List(row), [InvalidCsv(CSV.Error)]errs) where row.parser_for : CSV.Format -> (CSV.State -> Try({ rest : CSV.State, value : row }, [InvalidCsv(CSV.Error)]errs))
    - CSV.parse_csv : Parser(CSV.CSVRecord, a), CSV -> Try(List(a), [ParsingFailure(Str), ParsingIncomplete(List(CSV.CSVField))]b)
    - CSV.parse_csv_record : Parser(CSV.CSVRecord, a), CSV.CSVRecord -> Try(a, [ParsingFailure(Str), ParsingIncomplete(List(CSV.CSVField))]b)
    + CSV.parse_headerless : Str -> Try(List(row), [InvalidCsv(CSV.Error)]errs) where row.parser_for : CSV.Format -> (CSV.State -> Try({ rest : CSV.State, value : row }, [InvalidCsv(CSV.Error)]errs))
    + CSV.parse_normalized : Str -> Try(List(row), [InvalidCsv(CSV.Error)]errs) where row.parser_for : CSV.Format -> (CSV.State -> Try({ rest : CSV.State, value : row }, [InvalidCsv(CSV.Error)]errs))
    + CSV.parse_records : Str -> Try(List(CSV.Record), [InvalidCsv(CSV.Error)]a)
    - CSV.parse_str : Parser(CSV.CSVRecord, a), Str -> Try(List(a), [ParsingFailure(Str), ParsingIncomplete(CSV.CSVRecord), SyntaxError(Str)]b)
    - CSV.parse_str_to_csv : Str -> Try(CSV, [ParsingFailure(Str), ParsingIncomplete(List(Num.U8))]a)
    - CSV.parse_str_to_csv_record : Str -> Try(List(CSV.CSVField), [ParsingFailure(Str), ParsingIncomplete(List(Num.U8))]a)
    + CSV.parse_with : Parser(CSV.Record, a), Str -> Try(List(a), [InvalidCsv(CSV.Error)]b)
    + CSV.parser : Parser(List(Num.U8), List(CSV.Record))
    - CSV.record : a -> Parser(CSV.CSVRecord, a)
    + CSV.record : a -> Parser(CSV.Record, a)
    + CSV.split_header : List(CSV.Record) -> { header : List(Str), rows : List(List(List(Num.U8))) }

---- HTTP - MAJOR ----

    + HTTP.Error : (alias for { message : Str, offset : Num.U64 })
    - HTTP.Header : (alias for [Header(Str, Str)]#polarity)
    + HTTP.Header : (alias for { name : Str, value : Str })
    - HTTP.HttpVersion : (alias for { major : Num.U8, minor : Num.U8 })
    - HTTP.Method : (alias for [Connect, Delete, Get, Head, Options, Patch, Post, Put, Trace]#polarity)
    + HTTP.Method : (alias for [Connect, Delete, Extension(Str), Get, Head, Options, Patch, Post, Put, Trace]#polarity)
    - HTTP.Request : (alias for { body : List(Num.U8), headers : List(HTTP.Header), http_version : HTTP.HttpVersion, method : HTTP.Method, uri : Str })
    + HTTP.Request : (alias for { body : List(Num.U8), headers : List(HTTP.Header), method : HTTP.Method, target : Str, version : HTTP.Version })
    - HTTP.Response : (alias for { body : List(Num.U8), headers : List(HTTP.Header), http_version : HTTP.HttpVersion, status : Str, status_code : Num.U16 })
    + HTTP.Response : (alias for { body : List(Num.U8), headers : List(HTTP.Header), reason : Str, status_code : Num.U16, version : HTTP.Version })
    + HTTP.Version : (alias for { major : Num.U8, minor : Num.U8 })
    + HTTP.header : List({ name : Str, value : Str }), Str -> Try(Str, [Missing]a)
    + HTTP.parse_request : List(Num.U8) -> Try({ request : HTTP.Request, rest : List(Num.U8) }, [InvalidHttp(HTTP.Error)]a)
    + HTTP.parse_requests : List(Num.U8) -> Try(List(HTTP.Request), [InvalidHttp(HTTP.Error)]a)
    + HTTP.parse_response : List(Num.U8) -> Try({ response : HTTP.Response, rest : List(Num.U8) }, [InvalidHttp(HTTP.Error)]a)
    - HTTP.request : Parser(String.Utf8, { body : List(Num.U8), headers : List(HTTP.Header), http_version : HTTP.HttpVersion, method : HTTP.Method, uri : Str })
    + HTTP.request : Parser(List(Num.U8), HTTP.Request)
    - HTTP.response : Parser(List(Num.U8), { body : List(Num.U8), headers : List(HTTP.Header), http_version : HTTP.HttpVersion, status : Str, status_code : Num.U16 })
    + HTTP.response : Parser(List(Num.U8), HTTP.Response)

---- Markdown - MAJOR ----

    - Markdown : := [Blockquote(List(Markdown)), Code({ info : Str, pre : Str }), Frontmatter({ raw : Str }), Heading({ content : List(Markdown.Inline), level : Markdown.Level }), HtmlBlock(Str), ListBlock({ items : List({ blocks : List(Markdown), task : Markdown.TaskState }), kind : Markdown.ListKind, loose : Bool }), Paragraph(List(Markdown.Inline)), TODO(Str), Table({ align : List(Markdown.Alignment), header : List(List(Markdown.Inline)), rows : List(List(List(Markdown.Inline))) }), ThematicBreak]
    + Markdown : := [Blockquote(List(Markdown)), Code({ info : Str, pre : Str }), Frontmatter(Str), Heading({ content : List(Markdown.Inline), level : Markdown.Level }), HtmlBlock(Str), ListBlock({ items : List({ blocks : List(Markdown), task : Markdown.TaskState }), kind : Markdown.ListKind, loose : Bool }), Paragraph(List(Markdown.Inline)), Table({ align : List(Markdown.Alignment), header : List(List(Markdown.Inline)), rows : List(List(List(Markdown.Inline))) }), ThematicBreak]
    + Markdown.Alignment.to_hash : a
    + Markdown.Inline.to_hash : a
    + Markdown.Level.to_hash : a
    - Markdown.LinkTarget : (alias for { href : Str, title : [None, Some(Str)]#polarity })
    + Markdown.LinkTarget : (alias for { href : Str, title : Try(Str, [Missing]#polarity) })
    + Markdown.ListKind.to_hash : a
    + Markdown.TaskState.to_hash : a
    - Markdown.all : Parser(String.Utf8, List(Markdown))
    - Markdown.code : Parser(List(Num.U8), Markdown)
    + Markdown.frontmatter : List(Markdown) -> Try(Str, [Missing]a)
    - Markdown.heading : Parser(List(Num.U8), Markdown)
    - Markdown.image : Parser(List(Num.U8), Markdown.Inline)
    + Markdown.inline_parser : Parser(List(Num.U8), List(Markdown.Inline))
    - Markdown.inline_to_debug_str : Markdown.Inline -> Str
    - Markdown.inlines : Parser(String.Utf8, List(Markdown.Inline))
    - Markdown.link : Parser(List(Num.U8), Markdown.Inline)
    + Markdown.parse_inlines : Str -> List(Markdown.Inline)
    + Markdown.parse_str : Str -> List(Markdown)
    + Markdown.parser : Parser(List(Num.U8), List(Markdown))
    - Markdown.to_debug_str : Markdown -> Str
    + Markdown.to_hash : a

---- Parser - MAJOR ----

    - Parser.ParseResult : (alias for Try({ input : input, val : a }, [ParsingFailure(Str)]#polarity))
    + Parser.ParseResult : (alias for Try({ rest : input, value : a }, [ParseError({ message : Str, offset : Num.U64 })]#polarity))
    + Parser.and_then : Parser(input, a), (a -> Parser(input, b)) -> Parser(input, b)
    - Parser.apply : Parser(input, a -> b), Parser(input, a) -> Parser(input, b)
    - Parser.build_primitive_parser : (input -> Parser.ParseResult(input, a)) -> Parser(input, a)
    + Parser.custom : (input -> Parser.ParseResult(input, a)) -> Parser(input, a) where input.len : input -> Num.U64
    - Parser.fail : Str -> Parser(input, a)
    + Parser.fail : Str -> Parser(input, a) where input.len : input -> Num.U64
    - Parser.flatten : Parser(input, Try(a, Str)) -> Parser(input, a)
    + Parser.flatten : Parser(input, Try(a, Str)) -> Parser(input, a) where input.len : input -> Num.U64
    - Parser.many : Parser(input, a) -> Parser(input, List(a))
    + Parser.many : Parser(input, a) -> Parser(input, List(a)) where input.len : input -> Num.U64
    - Parser.maybe : Parser(input, a) -> Parser(input, Try(a, [Nothing]c))
    + Parser.maybe : Parser(input, a) -> Parser(input, Try(a, [Missing]c))
    - Parser.one_of : List(Parser(input, a)) -> Parser(input, a)
    + Parser.one_of : List(Parser(input, a)) -> Parser(input, a) where input.len : input -> Num.U64
    - Parser.one_or_more : Parser(input, a) -> Parser(input, List(a))
    + Parser.one_or_more : Parser(input, a) -> Parser(input, List(a)) where input.len : input -> Num.U64
    - Parser.parse : Parser(input, a), input, (input -> Bool) -> Try(a, [ParsingFailure(Str), ParsingIncomplete(input)]c)
    + Parser.parse : Parser(input, a), input -> Try(a, [ParseError({ message : Str, offset : Num.U64 })]c) where input.len : input -> Num.U64
    - Parser.parse_partial : Parser(input, a), input -> Parser.ParseResult(input, a)
    + Parser.run : Parser(input, a), input -> Parser.ParseResult(input, a) where input.len : input -> Num.U64
    - Parser.sep_by : Parser(input, a), Parser(input, sep) -> Parser(input, List(a))
    + Parser.sep_by : Parser(input, a), Parser(input, sep) -> Parser(input, List(a)) where input.len : input -> Num.U64
    - Parser.sep_by1 : Parser(input, a), Parser(input, sep) -> Parser(input, List(a))
    + Parser.sep_by_one_or_more : Parser(input, a), Parser(input, sep) -> Parser(input, List(a)) where input.len : input -> Num.U64
    + Parser.span : Parser(List(item), a) -> Parser(List(item), List(item))

---- String - MAJOR ----

    Removed module

---- Utf8 - MINOR ----

    Added module

---- Xml - MAJOR ----

    - Xml : := { root : Xml.Node, xml_declaration : [Given(Xml.Declaration), Missing] }
    + Xml : := { declaration : Try(Xml.Declaration, [Missing]), root : Xml.Node }
    - Xml.Declaration : (alias for { encoding : [Given(Xml.TextEncoding), Missing]#polarity, version : Xml.Version })
    + Xml.Declaration : (alias for { encoding : Try(Xml.TextEncoding, [Missing]#polarity), version : Xml.Version })
    + Xml.Error : (alias for { column : Num.U64, line : Num.U64, message : Str })
    - Xml.Node : := [Element(Str, List({ name : Str, value : Str }), List(Xml.Node)), Text(Str)]
    + Xml.Node : := [Element({ attributes : List(Xml.Attribute), children : List(Xml.Node), name : Str }), Text(Str)]
    + Xml.Node.attribute : Xml.Node, Str -> Try(Str, [Missing]a)
    + Xml.Node.children_named : Xml.Node, Str -> List(Xml.Node)
    + Xml.Node.name : Xml.Node -> Try(Str, [NotAnElement]a)
    + Xml.Node.text : Xml.Node -> Str
    + Xml.Node.to_hash : a
    - Xml.Version : (opaque type)
    + Xml.Version : (alias for { major : Num.U64, minor : Num.U64 })
    - Xml.Version.is_eq : a
    - Xml.Version.new : Num.U8 -> Xml.Version
    + Xml.parse_str : Str -> Try(Xml, [InvalidXml(Xml.Error)]a)
    + Xml.parser : Parser(List(Num.U8), Xml)
    + Xml.to_hash : a
    - Xml.xml_parser : Parser(List(Num.U8), Xml)

---- Yaml - MAJOR ----

    - Yaml : := [Bool(Bool), Float(Num.F64), Int(Num.I64), Mapping(List({ key : Str, value : Yaml })), Null, Sequence(List(Yaml)), String(Str)]
    + Yaml : := [Bool(Bool), Float(Num.F64), Int(Num.I64), Mapping(List({ key : Str, value : Yaml })), Null, Sequence(List(Yaml)), Text(Str)]
    + Yaml.Cursor : (opaque type)
    + Yaml.DecodeOptions : (alias for { keys : [CamelCase, KebabCase, SnakeCase]#polarity, unknown_keys : [Reject, Skip]#polarity })
    - Yaml.Error : (alias for { column : Num.U64, ..{ line : Num.U64, message : Str } })
    + Yaml.Error : (alias for { column : Num.U64, line : Num.U64, message : Str })
    + Yaml.Format : (opaque type)
    + Yaml.Format.invalid_value : Yaml.Format, Yaml.Cursor -> [InvalidYaml({ column : Num.U64, line : Num.U64, message : Str })]a
    + Yaml.Format.parse_bool : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Bool }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_dec : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.Dec }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_dict_after_entry : Yaml.Format, Yaml.Cursor -> Try([Done(Yaml.Cursor)][Continue(Yaml.Cursor)]a, [InvalidYaml(Yaml.Error)]b)
    + Yaml.Format.parse_dict_after_key : Yaml.Format, Yaml.Cursor -> Try(Yaml.Cursor, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_dict_next : Yaml.Format, Yaml.Cursor -> Try([Done(Yaml.Cursor)][Entry(Yaml.Cursor)]a, [InvalidYaml(Yaml.Error)]b)
    + Yaml.Format.parse_dict_start : Yaml.Format, Yaml.Cursor -> Try([Counted({ len : Num.U64, rest : Yaml.Cursor }), Uncounted(Yaml.Cursor)]a, [InvalidYaml(Yaml.Error)]b)
    + Yaml.Format.parse_f32 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.F32 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_f64 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.F64 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_i128 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.I128 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_i16 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.I16 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_i32 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.I32 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_i64 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.I64 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_i8 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.I8 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_key_i16 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.I16 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_key_i32 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.I32 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_key_i64 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.I64 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_key_i8 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.I8 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_key_str : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Str }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_key_u16 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.U16 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_key_u32 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.U32 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_key_u64 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.U64 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_key_u8 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.U8 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_list_after_item : Yaml.Format, Yaml.Cursor -> Try([Done(Yaml.Cursor)][Continue(Yaml.Cursor)]a, [InvalidYaml(Yaml.Error)]b)
    + Yaml.Format.parse_list_next : Yaml.Format, Yaml.Cursor -> Try([Done(Yaml.Cursor)][Item(Yaml.Cursor)]a, [InvalidYaml(Yaml.Error)]b)
    + Yaml.Format.parse_list_start : Yaml.Format, Yaml.Cursor -> Try([Counted({ len : Num.U64, rest : Yaml.Cursor }), Uncounted(Yaml.Cursor)]a, [InvalidYaml(Yaml.Error)]b)
    + Yaml.Format.parse_null : Yaml.Format, Yaml.Cursor -> Try(Yaml.Cursor, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_record_after_field : Yaml.Format, Yaml.Cursor -> Try([Done(Yaml.Cursor)][Continue(Yaml.Cursor)]a, [InvalidYaml(Yaml.Error)]b)
    + Yaml.Format.parse_record_field : Yaml.Format, Encoding.FieldName.FieldNames(_shape), Yaml.Cursor -> Try([Continue(Yaml.Cursor), Done(Yaml.Cursor), Field({ field : Encoding.FieldName(_shape), rest : Yaml.Cursor }), TryField({ name : Str, rest : Yaml.Cursor }), TryFieldCaseless({ name : Str, rest : Yaml.Cursor })]b, [InvalidYaml(Yaml.Error)]c)
    + Yaml.Format.parse_record_start : Yaml.Format, Yaml.Cursor -> Try([Counted({ len : Num.U64, rest : Yaml.Cursor }), Uncounted(Yaml.Cursor)]a, [InvalidYaml(Yaml.Error)]b)
    + Yaml.Format.parse_str : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Str }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_tag_union : Yaml.Format, Encoding.ParseTagUnionSpec(a), Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : a }, [InvalidYaml({ column : Num.U64, line : Num.U64, message : Str })]b)
    + Yaml.Format.parse_tuple_end : Yaml.Format, Yaml.Cursor, Num.U64 -> Try(Yaml.Cursor, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_tuple_next : Yaml.Format, Yaml.Cursor, Num.U64, Num.U64 -> Try(Yaml.Cursor, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_tuple_start : Yaml.Format, Yaml.Cursor, Num.U64 -> Try(Yaml.Cursor, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_u128 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.U128 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_u16 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.U16 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_u32 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.U32 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_u64 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.U64 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.parse_u8 : Yaml.Format, Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : Num.U8 }, [InvalidYaml(Yaml.Error)]a)
    + Yaml.Format.rename_field : Yaml.Format, Str -> Str
    + Yaml.Format.skip_record_field : Yaml.Format, Yaml.Cursor -> Try(Yaml.Cursor, [InvalidYaml(Yaml.Error)]a)
    + Yaml.as_bool : Yaml -> Try(Bool, [WrongType]a)
    + Yaml.as_i64 : Yaml -> Try(Num.I64, [WrongType]a)
    + Yaml.as_list : Yaml -> Try(List(Yaml), [WrongType]a)
    + Yaml.as_str : Yaml -> Try(Str, [WrongType]a)
    + Yaml.at : Yaml, Num.U64 -> Try(Yaml, [Missing]a)
    + Yaml.decode : Str -> Try(a, [InvalidYaml(Yaml.Error)]errs) where a.parser_for : Yaml.Format -> (Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : a }, [InvalidYaml(Yaml.Error)]errs))
    + Yaml.decoder : Yaml.DecodeOptions -> (Str -> Try(a, [InvalidYaml(Yaml.Error)]errs)) where a.parser_for : Yaml.Format -> (Yaml.Cursor -> Try({ rest : Yaml.Cursor, value : a }, [InvalidYaml(Yaml.Error)]errs))
    + Yaml.get : Yaml, Str -> Try(Yaml, [Missing]a)
    + Yaml.get_path : Yaml, List(Str) -> Try(Yaml, [Missing]a)
    - Yaml.parse_str : Str -> Try(Yaml, [YamlError(Yaml.Error)]a)
    + Yaml.parse_str : Str -> Try(Yaml, [InvalidYaml(Yaml.Error)]a)
    + Yaml.parser : Parser(List(Num.U8), Yaml)
    + Yaml.to_hash : a

This is a MAJOR change.

1.2.0 -> 2.0.0

2.0.0 satisfies the required MAJOR bump.

Docs

Examples

The examples, with their parser dependency pinned to this release;
unzip, then follow the README.md inside.