Skip to content

Add Lexer.string-literal and Lexer.char-literal - #22

Merged
hellerve merged 1 commit into
mainfrom
claude/lexer-string-literal
Jul 13, 2026
Merged

hellerve merged 1 commit into
mainfrom
claude/lexer-string-literal

Conversation

@carpentry-agent

Copy link
Copy Markdown
Contributor

What

Adds two escape-aware quoted-token parsers to Parser.Lexer, completing the
standard Parsec token surface (stringLiteral / charLiteral):

  • Parser.Lexer.string-literal — parses a double-quoted string literal,
    decoding backslash escapes, and returns the decoded text.
  • Parser.Lexer.char-literal — parses a single-quoted character literal
    with the same escape set, and returns the decoded Char.

Both recognize the C/JSON escape set: \n, \t, \r, \\, \", \',
\0, \a, \b, \f, \v, and \xHH (two hex digits). A shared private
decode-escape helper backs both.

Why

The Lexer submodule shipped identifier, integer, float, comments, and
hex/octal/binary-int, but had no quoted-string or character parser — the
one standard Parsec token combinator that was missing, and the most-reached-for
token in real grammars. Without it, callers had to hand-roll escape decoding
(the examples/lisp.carp lexer can't parse string atoms, and examples/kv.carp
only accepts integer values).

Behavior notes

  • string-literal fails empty when there's no opening quote, so it composes
    in alt without needing try. It fails consumed (matching block-comment)
    on an unterminated string or an unrecognized escape.
  • char-literal fails consumed on an empty literal (''), a multi-character
    literal ('ab'), a missing closing quote, or an unrecognized escape.
  • The result of string-literal is a byte-level Carp string, so a \0 / \x00
    escape embeds a NUL and truncates it like any Carp string (documented on the
    parser).

Tests

19 new assertions covering the escape set, \xHH hex escapes, the empty string,
unterminated / unknown-escape / missing-quote / empty / multi-character failure
paths, parse-partial leaving trailing input, and composition with lexeme.
Full suite: 294 passed / 0 failed locally. carp-fmt --check and angler
clean on parsec.carp; CHANGELOG updated.


Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

Add two escape-aware quoted-token parsers to the Lexer submodule,
completing the standard Parsec token surface (stringLiteral/charLiteral):

- string-literal: parses a double-quoted string, decoding backslash
  escapes (\n \t \r \\ \" \' \0 \a \b \f \v and \xHH) and
  returning the decoded text. Fails empty when no opening quote is
  present so it composes in alt without try; fails consumed on an
  unterminated string or unrecognized escape.
- char-literal: parses a single-quoted character with the same escape
  set, returning the decoded Char.

A shared decode-escape helper backs both. Adds 19 tests covering the
escape set, hex escapes, empty/unterminated/multi-char/unknown-escape
failures, parse-partial trailing input, and lexeme composition.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

Checked out claude/lexer-string-literal, built and ran the full suite with carp -x test/parsec.carp: 294 passed / 0 failed locally, matching the PR. CI green on ubuntu + macos.

Findings

I went past the test suite to probe the byte-oriented design and crash edges. No blocking issues found.

Verified behaviors (via a scratch harness):

  • Raw multi-byte UTF-8 in a string body round-trips byte-for-byte ("café"café, len 5). This works because String.char-at returns a raw byte and StringBuf.append-char writes exactly one byte (sb->data[sb->len++] = c) — no accidental UTF-8 re-encoding, which was the main correctness risk in a per-byte parser.
  • High \xHH escapes assemble raw bytes: "\xc3\xa9" produces the same 2 bytes as a literal é; "\xff" yields length 1. Consistent with the byte-level contract in the docstring.
  • Malformed input fails cleanly, never crashes: backslash-at-EOF, \x / \x4 truncated at EOF, \x4" (non-hex second digit), lone opening quote, and empty input all return errors. The pos < len guards before every char-at hold, so there's no out-of-bounds read (which segfaults silently in Carp).
  • char-literal on a raw multi-byte 'é' fails cleanly rather than mis-decoding — correct for a single-byte Char contract.

The \0/\x00 NUL-truncation is a genuine limitation but is explicitly documented on string-literal, and it's inherent to Carp's C-string representation, so it's the right call to document rather than work around.

The empty-vs-consumed failure distinction (ErrEmpty on missing opening quote so it composes under alt without try, ErrConsumed on a partial match) is implemented correctly and matches block-comment's convention.

Verdict: merge

Complete, well-tested, and the byte-oriented edges I stress-tested all behave correctly. This fills the one missing standard Parsec token combinator cleanly.

@hellerve
hellerve marked this pull request as ready for review July 13, 2026 21:15
@hellerve
hellerve merged commit 9ded062 into main Jul 13, 2026
2 checks passed
@hellerve
hellerve deleted the claude/lexer-string-literal branch July 13, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant