\ before a newline inside a string literal is not a line continuation
- Type: bug (Nil-Python frontend) — Track N.
- Filed: 2026-08-18 by frank3-b, found running the corpus caller named by [[feature-b-mimic-urllib-request-over-the-rtl-http-stack]].
The bug
Python's lexer consumes \ + newline inside a string literal, emitting
nothing. NilPy emits both characters literally.
Repro
s = '''\
AB
CD'''
print(repr(s))
t = "x\
y"
print(repr(t))
pxx (pinned, HEAD df15ae3fe) prints '\\\nAB\nCD' then 'x\\\ny'.
CPython prints 'AB\nCD' then 'xy'.
Both literal forms are affected, so it is the escape handling in the string scanner, not anything specific to triple quotes.
Why it matters more than it looks
This is a silent wrong value, not an error. The string still builds, the
program still runs, and the damage is two extra characters in the middle of
text — the kind of thing that surfaces far from the cause, which
devdocs/dev/debugging-playbook.md calls the expensive case.
'''\ at the start of a triple-quoted block is a common Python idiom
precisely because it lets the first line of the content start at column 0, so
this is not an exotic corner.
How it was found — a worked example of the damage
library_candidates/webencodings/webencodings/mklabels.py is a code generator
that prints a Python module. Run under pxx and under CPython against the same
local HTTP endpoint, the two outputs differ by exactly one line: pxx's
generated file starts with a stray \ line. Everything else — all the label
mappings, the alignment padding, the JSON decoding — is identical.
So the generated module would be a syntax error in the file it generates, from a generator that ran without complaint. That is the whole failure mode in one artifact.
Reproduce it with any local server:
sed 's|http://encoding.spec.whatwg.org/encodings.json|http://127.0.0.1:PORT/enc.json|' \
library_candidates/webencodings/webencodings/mklabels.py > mklabels_local.py
pinned mklabels_local.py out && ./out > pxx.txt
python3 mklabels_local.py > cpy.txt
diff cpy.txt pxx.txt # one line: a leading '\'
with enc.json holding the WHATWG shape, e.g.
[{"encodings":[{"labels":["utf-8","utf8"],"name":"UTF-8"}],"heading":"x"}].
Not a urllib bug
Worth stating because of where it was found: urlopen is not involved. The
generator's HTTP fetch, its .read().decode('ascii') and its json.loads all
produce byte-identical results to CPython — the diff is entirely in the literal
that the generator prints.
RESOLVED 2026-08-19 — genuinely lexer-sized, one missing case arm
Reproduced at HEAD exactly as filed. Root cause is a missing arm, not a
wrong one: the escape case in compiler/pylexer.inc (the single- and
triple-quoted string scanner) handles n t r a b f v \ ' " x and octal, and has
no arm for a newline — so \ + newline fell through to the lax else that
emits a literal backslash followed by the character. That else is the
deliberate lax-dialect fallback for unknown escapes, which is why the failure
was silent and produced a plausible wrong VALUE rather than an error.
Added #10 and #13 arms that consume the newline and emit nothing.
Two details that are easy to get wrong and are covered:
SrcLinemust still advance. This arm now consumes the newline instead of the ordinary body arm below, and the scanner's own header comment records that a swallowed newline drifts every later line number. Verified: with a continuation on line 1, an error on line 4 is still reported as line 4.- CRLF is one line ending, so the
#13arm eats a following#10before the sharedInc(P). Verified against CPython on a CRLF source.
Raw literals are deliberately unchanged
Python keeps backslash-newline verbatim in an r-string, and the rawStr arm
above already did. Confirmed rather than assumed — r"a\<nl>cd" still yields
'a\\\ncd', matching CPython.
Verified — every literal form, diffed against CPython
| form | pxx | CPython |
|---|---|---|
"x\<nl>y" |
'xy' |
'xy' |
'''\<nl>AB<nl>CD''' |
'AB\nCD' |
'AB\nCD' |
r"a\<nl>cd" |
'a\\\ncd' |
'a\\\ncd' |
b'a\<nl>cd' |
b'acd' |
b'acd' |
f"a\<nl>{v}" |
'a5' |
'a5' |
"a\\nb" (ordinary \n) |
'a\\nb' |
'a\\nb' |
Regression test — WIRED, not just written
test/test_nilpy_str_line_continuation.npy + .expected, covering all six rows
above. The expectation is generated by CPython, not hand-written.
Wired into Makefile beside test_nilpy_str_isnumeric_istitle, and the wiring
was verified the way [[frank2-a-test-file-is-not-a-test]] says to: make -n compiler/pascal26 to prove the Makefile still parses without going near the
suite, then running the two wired lines verbatim. A .npy in test/ that no
build rule names is green today and uncovered forever.
Gate
make compiler/pascal26 converged after 1 round (self-host fixedpoint),
the six probes above diffed against CPython, and tools/gate.sh quick.
Log
- 2026-08-19 — resolved, commit 9ffc1637f.