Literal ${ in strings, one interpolation splitter, errors that teach - #258
Merged
b-macker merged 1 commit intoSep 27, 2026
Merged
Conversation
Found while an LLM built a real NAAb project: it embedded a JavaScript
template literal in a NAAb string and hit "Parse error at line 1, column
11: ... Got: '/'" -- no file, the position inside the extracted ${...},
and a hint about multi-line operators that had nothing to do with it. It
took a long bisection. Every NAAb string evaluates ${...}, and there was
no way to write a literal one: \${ and \x24{ both still interpolated.
- \${ is a literal ${; in f-strings \{ and \} are literal braces. A \$
not followed by { keeps its backslash and \{ outside f-strings keeps it
too, so existing shell text ("echo \$HOME") and regex text are
unchanged -- measured on the old build as well.
- include/naab/string_interpolation.h: one splitter used by the
tree-walker, the VM compiler and both taint scanners. There were four
hand-rolled copies; an evaluator and a taint scanner that disagree
about what is interpolation is a taint bypass.
- Pre-existing VM/tree-walker divergence fixed: the VM constant-folded
"n=${n}" + "!" from raw source text and printed "n=${n}!" (the
tree-walker printed "n=5!"). Found by the escape suite.
- Errors: a parse error inside ${...} names the enclosing string's
file:line:col, the expression, and recognises JavaScript/shell text,
with the escape and a <<javascript>> alternative. String literals now
carry their source location, and interpolated tokens are shifted to it,
so runtime errors inside ${...} report the real line. An undefined
ALL_CAPS name ("echo ${HOME}") explains the interpolation and offers
\${HOME} or env.get("HOME"), in both engines; === / !== point to == /
!= (verified == does not coerce: 1 == "1" is false in both engines).
- Formatter re-encodes the escape; writing the internal marker out would
have turned a literal ${ into live interpolation on the next run.
Tests: tests/parser/test_string_interp_escape.sh (25 arms, both engines;
on master only its 4 no-behaviour-change arms pass), taint parity
PARITY-interp_escaped (escaped \${t} is not a use of t; 14/14).
Differential 56/0, leak check 874/0, formatter idempotence pass.
Docs: book chapter 2 now documents interpolation and its escapes (it was
undocumented); CLAUDE.md gotcha. Also corrects the regex suite header
and findings doc, which still presented the Windows-stall hypothesis
that the next Windows run falsified, and records an AsyncCallbackPool
use-after-free race (test-only code, untouched here).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELUfjXZvx8kzXo1UJjrAhC
NAAb Governance Report
All governance checks passed! Generated by NAAb Governance Engine v4.0 |
b-macker
marked this pull request as ready for review
September 27, 2026 12:55
b-macker
deleted the
claude/naab-inadmissible-action-prevention-4cmn1m
branch
September 27, 2026 12:55
b-macker
added a commit
that referenced
this pull request
Sep 28, 2026
…n string equivalents (#261) From repo-sentinel round 2. - Interpolation parse errors (regression from #258): the evaluators shift the inner tokens to the string's file line so runtime errors point at the right line, which moved the inner parse error off "line 1" -- and the rewrite to "character N of the expression" only matched "line 1". Any string not on line 1 printed "Parser said: Parse error at line 2, column 11": a file line paired with an offset inside ${...}. The rewrite now matches any line. - string.slice suggested split() (edit distance). slice/substr, includes, padStart/padEnd/rjust/ljust, trimStart/trimEnd, replaceAll now name the NAAb equivalent with an example. Each mapping checked against behaviour: substring's end is exclusive like slice's, replace already replaces all. tests/parser/test_dogfood_hints.sh: +7 arms (I-01, S-00..S-05). Against the old binary all 6 non-control arms fail; the first draft of the S arms passed S-01 on the old build because "substring" also appeared in the old message's "Available:" list, so they now match the suggestion itself. Claude-Session: https://claude.ai/code/session_01ELUfjXZvx8kzXo1UJjrAhC Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An LLM building a real NAAb project put a JavaScript template literal inside a NAAb string and got
Parse error at line 1, column 11: ... Got: '/'. The error named no file. The "line 1" was a position inside the extracted${...}, not in the source file. The attached hint was about multi-line operators and had nothing to do with the problem, so it took a long bisection to find.The cause: every NAAb string evaluates
${...}, and there was no way to write a literal one.\${and\x24{both still interpolated.This PR adds that escape, gives the whole class of mistake errors that point at the source and suggest the fix, and fixes a VM/tree-walker divergence found along the way.
Changes
\${is now a literal${.\{and\}are literal braces.\$not followed by{keeps its backslash, so shell text like"echo \$HOME"is unchanged.\{outside f-strings also keeps its backslash, so regex text likea\{2\}is unchanged.include/naab/string_interpolation.his now used by the tree-walker, the VM compiler and both taint scanners. There were four hand-written copies, and if the evaluator and a taint scanner ever disagreed about what counts as interpolation, taint tracking could be bypassed."n=${n}" + "!"printedn=${n}!on the VM butn=5!on the tree-walker.${.${...}now gives the file, line and column of the enclosing string, shows the expression, and recognises JavaScript or shell text. It suggests the\${escape, or a<<javascript>>block instead.${...}are shifted to that line. Runtime errors inside${...}therefore report the real line instead of line 1."echo ${HOME}") now explains that the string was interpolated and offers\${HOME}orenv.get("HOME"), in both engines.===and!==now point to==and!=. I checked first that==does not coerce types:1 == "1"is false in both engines.\${. Writing the lexer's internal marker instead would have turned a literal${back into live interpolation the next time the file ran.docs/unit-test-findings.md: both still described my Windows-stall theory, which the next Windows run disproved.AsyncCallbackPool. That code is only used by tests and isn't changed here.Test Plan
tests/parser/test_string_interp_escape.sh: 25 checks across both engines. On master, only its 4 "behaviour unchanged" checks pass. Those 4 run as a separate program, so a crash elsewhere in the file can't make them fail falsely.test_taint_engine_parity.shpasses 14/14, including a newPARITY-interp_escapedcheck: an escaped\${t}is not treated as a use oft, while the existingPARITY-interpcheck still requires an unescaped${t}to be flagged.🤖 Generated with Claude Code
https://claude.ai/code/session_01ELUfjXZvx8kzXo1UJjrAhC
Generated by Claude Code