Skip to content

Literal ${ in strings, one interpolation splitter, errors that teach - #258

Merged
b-macker merged 1 commit into
masterfrom
claude/naab-inadmissible-action-prevention-4cmn1m
Sep 27, 2026
Merged

b-macker merged 1 commit into
masterfrom
claude/naab-inadmissible-action-prevention-4cmn1m

Conversation

@b-macker

Copy link
Copy Markdown
Owner

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

  • Escape:
    • \${ is now a literal ${.
    • In f-strings, \{ and \} are literal braces.
    • A \$ not followed by { keeps its backslash, so shell text like "echo \$HOME" is unchanged.
    • \{ outside f-strings also keeps its backslash, so regex text like a\{2\} is unchanged.
    • Both compatibility claims are measured on the old build as well as the new one.
  • One splitter: the new header include/naab/string_interpolation.h is 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.
  • VM/tree-walker divergence fixed (existed before this PR):
    • "n=${n}" + "!" printed n=${n}! on the VM but n=5! on the tree-walker.
    • Cause: the VM combined the two literals into one constant from their raw source text, before interpolation.
    • It now only combines literals that contain no ${.
  • Errors that teach:
    • A parse error inside ${...} 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.
    • String literals now carry their source location, and tokens inside ${...} are shifted to that line. Runtime errors inside ${...} therefore report the real line instead of line 1.
    • An undefined ALL_CAPS name (as in "echo ${HOME}") now explains that the string was interpolated and offers \${HOME} or env.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.
  • Formatter: it now writes the escape back out as \${. Writing the lexer's internal marker instead would have turned a literal ${ back into live interpolation the next time the file ran.
  • Docs:
    • Book chapter 2 now documents string interpolation and its escapes; the book didn't cover interpolation before.
    • CLAUDE.md gets a new gotcha entry.
    • Corrections to the regex suite's header comment and to docs/unit-test-findings.md: both still described my Windows-stall theory, which the next Windows run disproved.
    • The findings doc also records a use-after-free race in 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.sh passes 14/14, including a new PARITY-interp_escaped check: an escaped \${t} is not treated as a use of t, while the existing PARITY-interp check still requires an unescaped ${t} to be flagged.
  • Differential VM vs tree-walker 56/0, error-message leak check 874/0, formatter idempotence passes.
  • Full suite: running locally, result to follow.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ELUfjXZvx8kzXo1UJjrAhC


Generated by Claude Code

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
@github-actions

Copy link
Copy Markdown

NAAb Governance Report

Metric Count
Files checked 16
Passed 16
Failed 0

All governance checks passed!

Generated by NAAb Governance Engine v4.0

@b-macker
b-macker marked this pull request as ready for review September 27, 2026 12:55
@b-macker
b-macker merged commit 03718f2 into master Sep 27, 2026
23 checks passed
@b-macker
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>
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.

2 participants