Add Lexer.string-literal and Lexer.char-literal - #22
Conversation
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.
There was a problem hiding this comment.
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 becauseString.char-atreturns a raw byte andStringBuf.append-charwrites 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
\xHHescapes 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/\x4truncated at EOF,\x4"(non-hex second digit), lone opening quote, and empty input all return errors. Thepos < lenguards before everychar-athold, so there's no out-of-bounds read (which segfaults silently in Carp). char-literalon a raw multi-byte'é'fails cleanly rather than mis-decoding — correct for a single-byteCharcontract.
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.
What
Adds two escape-aware quoted-token parsers to
Parser.Lexer, completing thestandard 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 literalwith 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 privatedecode-escapehelper backs both.Why
The
Lexersubmodule shippedidentifier,integer,float, comments, andhex/octal/binary-int, but had no quoted-string or character parser — theone 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.carplexer can't parse string atoms, andexamples/kv.carponly accepts integer values).
Behavior notes
string-literalfails empty when there's no opening quote, so it composesin
altwithout needingtry. It fails consumed (matchingblock-comment)on an unterminated string or an unrecognized escape.
char-literalfails consumed on an empty literal (''), a multi-characterliteral (
'ab'), a missing closing quote, or an unrecognized escape.string-literalis a byte-level Carp string, so a\0/\x00escape embeds a NUL and truncates it like any Carp string (documented on the
parser).
Tests
19 new assertions covering the escape set,
\xHHhex escapes, the empty string,unterminated / unknown-escape / missing-quote / empty / multi-character failure
paths,
parse-partialleaving trailing input, and composition withlexeme.Full suite: 294 passed / 0 failed locally.
carp-fmt --checkandanglerclean on
parsec.carp; CHANGELOG updated.Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.