|

swift-parsing: One Grammar for Parsing and Printing

A grammar connecting structured values with their textual representation

Reading a version string such as 2.7.1 is straightforward. Split it at the dots and convert the three pieces to integers. But what if the application also needs to write version strings? Now two implementations must agree on separators, component order, and valid values. When the format changes, they can easily drift apart.

swift-parsing addresses this with ParserPrinter: a parser that can both read text and generate it from structured values. The interesting part is hidden in the implementation. Printing proceeds in reverse order, with each component prepended to the text already produced.

One Description, Two Directions

import Parsing

let version = ParsePrint {
  Int.parser()
  ".".utf8
  Int.parser()
  ".".utf8
  Int.parser()
}

let (major, minor, patch) = try version.parse("2.7.1")
let next = try version.print((major, minor, patch + 1))
print(String(decoding: next, as: UTF8.self)) // 2.7.2

Int.parser() reads UTF-8 bytes, so the separators also use ".".utf8. The builder produces a tuple of three numbers, while the string literals produce Void: they check or print a separator without adding a value to the result. Parse can combine parsers that only read input. ParsePrint, however, requires every component to be able to print as well.

Why Printing Happens in Reverse Order

The ParserPrinter protocol adds print(_:into:) to Parser. In ParserBuilder.swift, SkipFirst, SkipSecond, and Take2 parse the first component before the second. Printing uses the opposite order.

Parse: integer -> dot -> integer
Print: integer <- dot <- integer

The last number is printed first. Then the separator and the preceding number are added to the front of the result.

StartsWith.print prepends a literal, and IntParser.print prepends the number’s bytes.

PrependableCollection supports this approach for Substring and its UTF-8 view. Prepending may copy memory. The advantage is that independently defined components can be composed, not a guarantee of allocation-free printing.

Repetition Must Be Reversed, Too

Many accumulates elements and reads separators. While parsing, it remembers the position after the last complete element.

When printing, Many.print writes the terminating component first. It then uses a decumulator to walk the elements in reverse order and prepend their separators.

For an array, the library provides that reverse traversal. If the result is accumulated in another way, a matching operation must be defined to break it back into elements. Tests cover count limits, extra separators, terminating-parser failures, and a parser that does not advance through its input. That last case produces an error instead of an infinite loop.

Not Every Transformation Can Be Reversed

An ordinary .map { ... } can turn a number into a Boolean, but true cannot tell us what the original number was. That is why Parsers.Map is only a Parser. By contrast, Conversion provides apply and unapply, which MapConversion uses for reading and printing.

The .memberwise(...) conversion relies on a structure’s synthesized initializer and memory layout. A custom initializer or a different field order can make its result incorrect. An explicit conversion is easier to verify.

Two-Way Operation Does Not Guarantee Exact Restoration

The documentation describes two expected properties: parsing a printed value should produce the same value, and printing parsed text should reproduce that text. Conforming to the protocol does not guarantee either property on its own. An integer parser can remove leading zeroes. This is normalization, not preservation of the original spelling.

There is another difference: parse(&input) may leave unread input behind, while the convenient parse("...") call checks completion with End. Test both directions and decide separately whether different textual representations of the same value are acceptable.

What the Repository Code Shows

Start with Parser.swift and ParserPrinter.swift for the protocols. ParserBuilder.swift explains the reverse printing order, Many.swift handles repetition, and Map.swift together with Conversion.swift shows reversible transformations. ManyTests.swift, EndTests.swift, and MapTests.swift cover important edge cases.

The project is MIT-licensed. The latest release verified for this article is 0.15.2, dated September 2, 2026. Its main package manifest uses Swift tools 6.2, and an alternative manifest supports Swift 6.1. Declared minimum platform versions are iOS 13, macOS 10.15, tvOS 13, and watchOS 6. The library product is named Parsing, and the repository includes tests and benchmarks.

The CasePaths feature is enabled by default and provides its related integration. Open issues include requests for improved error messages, which matters for tools that accept user input.

Practical Use Cases

A release tool or route generator can read and produce version numbers or paths that contain fixed separators and numeric components. One grammar reduces the risk of updating only one direction when a format changes.

For configuration data, Many can read and print a sequence of numeric identifiers. A final End() helps reject unexpected characters after the last element.

This covers a small internal field, not full CSV with quoted values and escaping. To import and export records, you can combine a parser for a single record with Many, but printing back is possible only when the transformations preserve the information needed for it. If a string is read just once, ordinary Swift tools may be simpler.

The value of swift-parsing is not that every string needs a grammar. It is that reading and writing can share one description when the format retains the information required to reverse the transformation.

Sources