Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 29 additions & 21 deletions Common/source/langscan.c
Original file line number Diff line number Diff line change
Expand Up @@ -1380,34 +1380,39 @@ boolean langstripstructuremarkers (Handle hin, Handle *hout) {

/*scan forward from the indent boundary to find the start of any
trailing comment (« or //) outside a string literal. UserTalk
strings use ASCII " (chdoublequote, 0x22) OR the Mac smart-quote
pair chopencurlyquote (0xD2) / chclosecurlyquote (0xD3); the
scanner accepts both forms (langscan.c parsepopstringconst), so
we track both here. Without that, a string like "a // b" with
"" delimiters works, but "a // b" with «...» smart quotes would
get its // misread as a comment start and the rest of the line
truncated. flqcurly distinguishes the closing delimiter to use.*/
has three literal delimiters, and all three must be tracked here
or a comment marker inside one gets misread as a real comment:
ASCII " (chdoublequote, 0x22); the Mac smart-quote pair
chopencurlyquote (0xD2) / chclosecurlyquote (0xD3); and
chsinglequote (0x27) for character and string4 constants
(parsepopstringconst / the single-quote branch below both accept
these). Without that, a string like "a // b" with "" delimiters
works, but «a // b» or 'Ç' would get its comment byte misread as
a comment start, truncating the rest of the line. Losing the
trailing structural-marker strip that way leaves a `{` in the
stored node text that the outline export re-emits from the level
transition, so each reinstall adds another brace (#866).
chclose is the delimiter that ends the literal we are inside.*/
long i = linestart + indent;
boolean flqcurly = false;
byte chclose = 0;
while (i < lineend) {
byte ch = buf [i];
if (flinstring) {
/*\ escapes the next byte (e.g. \" inside ASCII strings).
Same convention applies inside curly-quote strings; the
scanner doesn't distinguish.*/
Same convention applies inside curly-quote and
single-quote strings; the scanner doesn't distinguish.*/
if (ch == '\\' && i + 1 < lineend) {
i += 2;
continue;
}
if ((!flqcurly && ch == '"')
|| (flqcurly && ch == (byte) chclosecurlyquote))
if (ch == chclose)
flinstring = false;
++i;
continue;
}
if (ch == '"' || ch == chopencurlyquote) {
if (ch == '"' || ch == chopencurlyquote || ch == (byte) chsinglequote) {
flinstring = true;
flqcurly = (ch == chopencurlyquote);
chclose = (ch == chopencurlyquote) ? (byte) chclosecurlyquote : ch;
++i;
continue;
}
Expand Down Expand Up @@ -1453,28 +1458,31 @@ boolean langstripstructuremarkers (Handle hin, Handle *hout) {
surplus closing braces. If not, the brace we are about
to strip is needed to close an inline one-line block;
stop. Count {/} in the kept content, respecting
string-literal state for both " and « forms (escape
with backslash applies to both).*/
string-literal state for the ", « and ' forms (escape
with backslash applies to all three). Single quotes
must be tracked here for the same reason as in the
comment-start scan above: a brace inside a character
or string4 constant is not structural (#866).
k_close is the delimiter ending the current literal.*/
long try_from = stripfrom - 1;
long open_ct = 0;
long close_ct = 0;
boolean in_str = false;
boolean qcurly = false;
byte k_close = 0;
boolean esc = false;
long k;
for (k = linestart + indent; k < try_from; ++k) {
byte kc = buf [k];
if (esc) { esc = false; continue; }
if (in_str) {
if (kc == '\\') esc = true;
else if ((!qcurly && kc == '"')
|| (qcurly && kc == (byte) chclosecurlyquote))
else if (kc == k_close)
in_str = false;
continue;
}
if (kc == '"' || kc == chopencurlyquote) {
if (kc == '"' || kc == chopencurlyquote || kc == (byte) chsinglequote) {
in_str = true;
qcurly = (kc == chopencurlyquote);
k_close = (kc == chopencurlyquote) ? (byte) chclosecurlyquote : kc;
continue;
}
if (kc == '{') ++open_ct;
Expand Down
66 changes: 65 additions & 1 deletion tests/integration/test_cases/script_install_roundtrip.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,72 @@ tests:
expected_success: true
expected_result: "true"

- name: "round-trip: comment marker inside a single-quoted literal is not a comment (#866)"
description: "A single-quoted character/string4 constant holding the comment-marker byte (0xC7) must round-trip unchanged. The strip pass tracked \" and the curly-quote pair but not ', so the 0xC7 inside 'C' was mis-read as the start of a trailing comment. That set commentstart mid-literal, skipped the trailing structural-marker strip, and left a { in the stored node text that the export pass re-emitted from the level transition -- adding one extra { per install, cumulatively, until the script no longer compiled."
script: |
local (src = "on f () {\r\tlocal (s = \"x\");\r\tif s contains '\xc7' {\r\t\ts = \"y\"};\r\treturn (s)}");
new (tabletype, @system.temp.rt);
script.newScriptObject (src, @system.temp.rt.f);
local (back = string (system.temp.rt.f));
delete (@system.temp.rt);
return (back == src)
expected_success: true
expected_result: "true"

- name: "round-trip: single-quoted comment marker stays idempotent across reinstall (#866)"
description: "Reinstalling a script's own unmodified source must be idempotent. With the single-quote gap, each install appended another { to the same line, so the second read-back differed from the first and the script stopped compiling."
script: |
local (src = "on f () {\r\tlocal (s = \"x\");\r\tif s contains '\xc7' {\r\t\ts = \"y\"};\r\treturn (s)}");
new (tabletype, @system.temp.rt);
script.newScriptObject (src, @system.temp.rt.f);
local (first = string (system.temp.rt.f));
script.newScriptObject (first, @system.temp.rt.f);
local (second = string (system.temp.rt.f));
delete (@system.temp.rt);
return (first == second)
expected_success: true
expected_result: "true"

- name: "round-trip: single-quoted comment marker still compiles and runs (#866)"
description: "End-to-end behavioral check: after install the script must actually execute, not merely read back. suites.commercial.parseAete in Virgin.root is the real-world instance of this shape."
script: |
local (src = "on f (s) {\r\tif s contains '\xc7' {\r\t\treturn (1)};\r\treturn (0)}");
new (tabletype, @system.temp.rt);
script.newScriptObject (src, @system.temp.rt.f);
local (hit = system.temp.rt.f ("a\xc7b"));
local (miss = system.temp.rt.f ("ab"));
delete (@system.temp.rt);
return (hit == 1 and miss == 0)
expected_success: true
expected_result: "true"

- name: "round-trip: brace inside a single-quoted constant is not counted as structure (#866)"
description: "Exercises the brace-balance counter rather than the comment-start scan. That counter decides whether a trailing } is load-bearing (the #621 inline-block check); it tracked the same two delimiters and so counted a { or } inside a character constant as structural. Here the constant holds a brace and the line ends with an inline one-line block, so a miscount changes the strip decision and unbalances the stored node text. Both scanners now track ' as a delimiter."
script: |
local (src = "on f (c) {\r\tif c == '{' {\r\t\ttry {local (x = 1)}};\r\treturn (1)}");
new (tabletype, @system.temp.rt);
script.newScriptObject (src, @system.temp.rt.f);
local (back = string (system.temp.rt.f));
local (result = system.temp.rt.f ("{"));
delete (@system.temp.rt);
return (back == src and result == 1)
expected_success: true
expected_result: "true"

- name: "round-trip: comment marker inside a curly-quote string literal (0xD2/0xD3)"
description: "Coverage gap predating #866: the curly-quote string form was tracked by both scanners but never exercised by a test, and the delimiter refactor touches that path. A comment marker inside a Mac smart-quote string must not be read as a trailing comment. Uses the \\xd2 and \\xd3 escapes so the bytes survive the runner writing the script file as UTF-8."
script: |
local (src = "on f () {\r\tlocal (s = \xd2a \xc7 b\xd3);\r\treturn (s)}");
new (tabletype, @system.temp.rt);
script.newScriptObject (src, @system.temp.rt.f);
local (back = string (system.temp.rt.f));
delete (@system.temp.rt);
return (back == src)
expected_success: true
expected_result: "true"

- name: "round-trip: inline one-line nested block at outermost depth (#621)"
description: "Same shape as the previous test but the inline block sits at the outermost body level (no enclosing if/while). Confirms the brace-balance check generalizes across nesting depths and isn't depth-1-specific."
description: "Same shape as the depth-1 inline-block case above but the inline block sits at the outermost body level (no enclosing if/while). Confirms the brace-balance check generalizes across nesting depths and isn't depth-1-specific."
script: |
local (src = "on f () {\r\ttry {local (x = 7)};\r\treturn (1)}");
new (tabletype, @system.temp.rt);
Expand Down