Repository navigation
Decode \uXXXX and \UXXXXXXXX in string literals; add <<'…' and <<"…" - #62
Merged
Merged
Conversation
…nd <<"…" (BIG-8659) Tests for the U2.1 language decisions (2026-10-16): a lone surrogate keeps its escape text, \UXXXXXXXX is the only long form, and <<'…' and <<"…" are both marked strings. Streaming tests feed every cut of a program and check that the sink only ever receives a prefix of the finished string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… <<'…' and <<"…" (BIG-8659) One read_escape() serves both string loops. It returns the decoded text, null (the backslash is content), or PENDING when the input ends before the escape is decided: a bare backslash, a partial \u or \U, or a high surrogate that a low surrogate escape can still follow. On PENDING the loop stops, so an open string never holds text that more input can change, and a redirect sink only ever receives a prefix of the finished string. A lone surrogate, a \U value outside Unicode, and an incomplete escape keep their text, as every unknown escape does. A marked string now opens with << glued to any quote; the single-width form gathers with the same escapes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ckslash holdback (BIG-8659) Both gather loops had the same escape block, so it moves into gather_escape(). get_string_value() no longer holds back a trailing backslash: the gather never appends an undecided one now, because read_escape() reports it pending. A trailing quote is still held back. No test changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CHANGELOG: two breaking entries under [Unreleased]. The prompt doc gets a short Unicode escapes section with two claims that the smoke test runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or (BIG-8659)
Local review F2: the program in "each level decodes its own escapes" had
lost its backslash when the file was written, so it passed on 0.21.0. It
now fails on main ("\\u0027") and passes on the branch. Review F6: the
outer-quote test asserts the error type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nes (BIG-8659) Local review F4: get_token_location() sets end_pos to start_pos plus the decoded length, so it falls short after every escape (\n by one, a surrogate pair by ten). The error underline in errors.ts uses it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rce (BIG-8659) Both string loops record the source position of the closing delimiter in token_end_pos (a field that was declared but unused), and get_token_location() uses it. Other tokens keep start_pos plus their length, which equals their source span. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gex and JSON (BIG-8659) Local review F1: docs/forthic-prompt.md is generated by scripts/generate-docs.ts, and npm run docs:build removed the new section. The section now lives in the template, and the escape list names the two new escapes. Running docs:build again changes nothing. Local review F3: CHANGELOG and prompt say that a decoded character is plain content (a decoded . in a regex, a decoded quote in JSON text), and that regex and JSON want a raw literal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
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.
Problem
Models write Unicode escapes in Forthic strings:
\u2014for a dash,\u0027for an apostrophe,\uD83D\uDE00for an emoji, and the Python form\U0001F600. Forthic kept each one as text, so a user saw\u2014in a reply. The bigb host repairs some of them before it parses the program (BIG-8657, bigb#7596), but that repair cannot decode',"or\: it cannot know if a literal will be parsed again as code, where a decoded quote can close the inner literal. It also covers only one host path.Also, a marked string had to be triple-quoted.
<<"streamed reply"tokenized as unknown words, so a reply in a regular string could not stream (BIG-8660 needs it).Options and choices
\UXXXXXXXX(Python; about 5 prod programs each week),\u{…}(JavaScript), both, or neither.\Uonly (Rino).<<'…': supported, next to<<"…"(Rino). Same escapes and holdback as the triple forms.get_string_value()for a partial escape, or stop gathering at an escape that the input ends inside. Stop gathering. The open string then never holds undecided text, one rule covers both widths, and the old backslash holdback goes away. Only the triple-quote delimiter holdback stays. One visible effect: a settled\\at the end of a chunk now reaches the sink one chunk earlier than in 0.21.0, which held every trailing backslash.end_pos: it wasstart_posplus the decoded length, so it fell short after every escape (\nby one, a surrogate pair by ten). A closed string now records where its content ends in the source.Key changes
src/forthic/tokenizer.tsread_escape(index)returns the escape's text and length, null (the backslash is content), or PENDING (the input ends first:\, a partial\u/\U, or a high surrogate that a low surrogate escape can still follow).read_hex()reads the digits.gather_escape()serves both gather loops. On PENDING the loop stops, so a streaming chunk returns the open string without the undecided escape, andrun()throwsUnterminatedStringErroras before.is_string_redirect_start()accepts<<glued to any quote.transition_from_GATHER_STRINGtakesis_string_redirect, setsstring_redirect_open, and marks its token. It no longer advances before the escape check, like the triple-quote loop.get_string_value()holds back only a trailing run of the triple-quote delimiter.token_end_posat the closing delimiter, andget_token_location()uses it forend_pos. The error underline inerrors.tsusesend_pos.Breaking changes
\uXXXXand\UXXXXXXXX. A program that needs the text\u2014writes\\u2014orr'\u2014'.<<glued to a quote is now a marked string, not a word.Tests
src/forthic/tests/unit/core/tokenizer.test.ts: "Unicode escapes (\uXXXX, \UXXXXXXXX)": every non-raw width and marked strings, hex case, surrogate pairs, lone surrogates, incomplete escapes,\Ulimits,\u{…}and raw literals unchanged,\\u, decoded quotes are content. "Marked single-width strings": both quotes, escapes,<< "x"with a space, unterminated.src/forthic/tests/unit/core/tokenizer.test.ts: "String token positions after escapes":end_posafter escapes and without them.src/forthic/tests/integration/string_escapes.test.ts: "Unicode escapes in code strings":RUNandMAPcode strings; a decoded quote in the outer literal closes the inner one (UnknownWordError).src/forthic/tests/integration/string_redirect.test.ts: "escapes stream as their decoded text": four programs, every cut; the sink text is always a prefix of the final string and equals it at the end.Why other files changed
CHANGELOG.md: two breaking entries under [Unreleased], with a note that regex and JSON want a raw literal.scripts/generate-docs.ts: the prompt template. It names the new escapes and has a short "Unicode escapes" section. Its twocode→expectedclaims run innpm run smoke.docs/forthic-prompt.md: regenerated from the template.Limits
RUNorMAPruns reports a position in the decoded code text. After an escape in the outer literal, that position does not match the source.\nhas the same limit in 0.21.0.Closes BIG-8659.
🤖 Generated with Claude Code