Skip to content

fix(repl): suppress spurious ';' on trailing newline / trailing ';' (#620 burndown)#640

Merged
jsavin merged 2 commits into
developfrom
fix/normalize-trailing-newline-spurious-semicolon
May 25, 2026
Merged

fix(repl): suppress spurious ';' on trailing newline / trailing ';' (#620 burndown)#640
jsavin merged 2 commits into
developfrom
fix/normalize-trailing-newline-spurious-semicolon

Conversation

@jsavin

@jsavin jsavin commented May 24, 2026

Copy link
Copy Markdown
Owner

Root cause

After #635 tightened newline normalization in script/eval, scripts ending with a trailing newline (which is every YAML | block scalar — i.e. nearly every integration test script) got a ; inserted before that final \n. The build_wrapped_script suffix \n} then made the with-wrapper end in an empty trailing statement, and evaltree returns the parser's truthy default (true) for an empty statement. The user observed true instead of the last expression's value. A symmetric issue affected user-typed trailing ; on the fallback langrunhandle_value path (no wrapper, but still 1 + 2; -> empty trailing stmt -> true).

Same bug class as #618 / #624 / #628 / #635 — wrapper / parser default masks lost statement value.

Files changed

  • frontier-cli/repl_variables.c — three coordinated changes to normalize_newlines_to_semicolons:
    1. Factor whitespace+comment skip into peek_next_real_byte(). Same skip rules now used by the } guard (from fix(repl): preserve try/else and if/else across newlines in protocol mode #635) and the new EOF guard.
    2. EOF guard: when need_sep is true, peek past inter-token noise. If nothing real follows before \0, suppress the ; insertion. The } check is subordinate — it only runs when something real does follow.
    3. Trailing strip: after the scan, drop trailing ; / whitespace / \r / \n so a user-typed trailing ; doesn't manifest as an empty top-level statement.
  • tests/integration/test_cases/protocol_eval_compile_errors.yaml — five new regression tests covering: callback-style multi-line script, simple expr with trailing \n, trailing ;\n, trailing whitespace/tabs, trailing // comment.

Tests now passing

Integration before -> after:

  • Total: 2300 -> 2305 (+5 new regression tests)
  • Passed: 1936 -> 2027 (+91)
  • Failed: 157 -> 71 (-86)
  • Skipped: 207 -> 207

Zero got 'true' failures remain (was ~80 unique tests). All 49 tests in callback_tcp.yaml + callback_database.yaml pass.

Unit tests: 489 / 489 pass (unchanged).

Cluster scope

This PR fully resolves the "callback test returns 'true' instead of expected value" cluster from #620 burndown:

  • callback_tcp.yaml — 12 failures resolved
  • callback_database.yaml — 21 failures resolved
  • repl_basic.yaml — multi-statement / multi-line variants resolved
  • bigstring_boundary.yaml — multi-line bigstring tests resolved
  • Various lang.evaluate, tryError, sys verbs multi-line tests resolved

Remaining out-of-scope failures (next iteration)

71 failures remain, all in different clusters (no got 'true' signature, different roots):

  • html_framework cluster (~20 failures): html.rundirective, html.glossarypatcher, html.cleanforexport, etc. — likely verb implementation gaps.
  • tcp. network cluster* (~30 failures): tcp.listenStream, tcp.readStream, tcp.writeStream, tcp.statusStream, tcp.getPeerAddress, etc. — networking integration, not callback parsing.
  • sys verbs cluster (~5 failures): sys environment verbs - isolation test, etc.
  • A few stragglers: Database - system table accessible after hydration, etc.

None share root cause with this PR — separate investigations.

Verification

  • Reproduced the bug pre-fix with minimal script 1 + 2\n returning 'true' instead of 3.
  • Traced via protocol stderr trace logging to confirm normalized + wrapped script structure.
  • Confirmed both repl_eval_with_variables_value paths (wrapped and fallback) are now correct.
  • All five new tests fail RED before the fix for the right reason (return 'true' instead of expected value), then GREEN after.
  • Full integration suite passes the same set of non-related tests as before (no regressions).

jsavin and others added 2 commits May 24, 2026 02:43
…620 burndown)

After #635 tightened newline normalization in script/eval, scripts ending
with a trailing newline (which is every YAML | block scalar) got a ';'
inserted before that final '\n'. The build_wrapped_script suffix '\n}' then
made the with-block end in an empty statement, and evaltree returned the
wrapper's truthy default ('true') instead of the user's last-expression
value. A symmetric issue affected user-typed trailing ';' on the fallback
langrunhandle_value path.

Three coordinated changes in normalize_newlines_to_semicolons:

1. Factor the whitespace+comment skip into peek_next_real_byte(). The
   existing '}'-guard lookahead and the new EOF guard share the same
   skip rules (ASCII WS, '//' line comments, UTF-8 '«...»' comments).
2. EOF guard: when need_sep is true, peek past inter-token noise. If
   nothing real follows before '\0', suppress the ';' insertion. The
   '}' check is now subordinate to this -- it only runs when something
   real does follow.
3. Trailing strip: after the scan, drop trailing ';' / whitespace /
   '\r' / '\n' so a user-typed trailing ';' doesn't manifest as an
   empty top-level statement.

Tested with five new regression cases in protocol_eval_compile_errors.yaml
covering: callback-style multi-line script, simple expr with trailing newline,
trailing semicolon-newline, trailing whitespace/tabs, trailing line comment.

Integration: 1936 passed / 157 failed -> 2027 passed / 71 failed
(net +91 passing, +5 new tests added). All 49 callback_tcp + callback_database
tests pass. Zero "got 'true'" failures remain -- the cluster is fully fixed.

Unit tests: 489 / 489 pass (unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tics

Three comment-only polish items from the local /gate round 1 review:

- P2 #1: docblock for normalize_newlines_to_semicolons now explicitly
  calls out that the trailing-';' strip changes user-visible semantics
  for explicitly-terminated scripts ("foo();" now returns foo()'s value,
  not the wrapper's truthy default 'true'). This is the REPL "always
  return the value of the last expression" convention. Previously the
  docblock framed it as symmetric closure with the EOF guard, which
  understated the behavioral change.

- P2 #3: add an inline comment at the EOF-guard / '}'-guard branch
  noting that EOF is checked first and subsumes the '}' guard's EOF
  case. Order matters and was previously implicit in the if/else-if
  ordering.

- P2 #4: add an inline comment at the trailing-strip loop explaining
  the `dst > out` guard — it prevents the strip from walking into the
  prefix on empty/fully-strippable input, since `dst == out` is its
  initial value before any byte was emitted.

No behavioral change. Integration 2028/70/207/2305; unit 489/489.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jsavin

jsavin commented May 25, 2026

Copy link
Copy Markdown
Owner Author

Local /gate round 2 (post-polish)

Verdict: PASS — zero P0/P1/P2 findings on the round-1 polish commit (6d47ccda7). Diff is comment-only.

Round 1 → Round 2

Round 1 (commit 773de2e1a) found 4 P2s, all addressed:

Tests at HEAD 6d47ccda7

  • Unit: 489/489 pass
  • Integration: 2028 passed / 70 failed / 207 skipped / 2305 total (vs 2027/71 pre-polish — one flake-recovery, no regressions, no new failures attributable to this commit since it's docs-only)
  • protocol_eval_compile_errors.yaml: 35/35 pass
  • Zero got 'true' failures remain (the cluster is drained)

Why not a full multi-reviewer re-gate?

The new commit is exclusively docblock and inline-comment edits. No control-flow, no buffer math, no new code paths, no test changes. Running bar-raiser/security/concurrency again on a comment-only diff would consume reviewer time for zero additional signal. The round-1 review of the underlying code stands.

Ready to merge.

@jsavin jsavin merged commit 05b113a into develop May 25, 2026
@jsavin jsavin deleted the fix/normalize-trailing-newline-spurious-semicolon branch May 25, 2026 02:16
@jsavin

jsavin commented May 25, 2026

Copy link
Copy Markdown
Owner Author

/auto Summary

What went well:

  • The /gate review caught the bug-in-the-bug — turned out fix(repl): normalize bare CR/LF to semicolons in protocol script/eval (#624) #628/fix(repl): preserve try/else and if/else across newlines in protocol mode #635 had a subtle trailing-\n issue that masked 91+ "got 'true'" failures across callback_tcp, callback_database, and bigstring boundary tests. Without /gate's deep trace through the wrapper semantics, this could have shipped as "the callback cluster is hopeless."
  • TDD-first: 5 new behavioral tests added before the fix. The trailing-; strip's REPL-semantics change was identified during review and docblock-clarified in round 2 rather than shipping ambiguous.
  • Local-first /gate worked exactly as designed: bar-raiser found 4 P2s, security and concurrency cleared the diff, no remote bot needed.

What could improve:

Recommendations:

  • The PR's docblock now explicitly notes that the trailing-; strip changes REPL semantics for user-typed terminated scripts ('foo();' returns foo()'s value, not 'true'). This is an intentional behavioral change, not just a bug fix. Worth a one-line callout in any user-facing REPL docs.

🤖 Generated with Claude Code /auto

jsavin added a commit that referenced this pull request May 25, 2026
…ollow-up) (#641)

* refactor(repl): table-driven normalize_newlines_to_semicolons (#640 follow-up)

After three rounds of patches (#628 -> #635 -> #640), the precedence
between the start, ';', '{', '}', and EOF/'else' guards in
normalize_newlines_to_semicolons had become inline-conditional logic
that was easy to misread and hard to extend. This refactor makes the
specification explicit: a 5x5 (prev_kind, next_kind) table maps every
transition to emit-or-suppress, and the loop becomes a single lookup.

Behavioral contract is unchanged. The cells of kEmitSep map 1:1 to the
previous code's branch outcomes:

  - START / SEMI / LBRACE rows: all suppress (matches start-of-script,
    ';' guard, '{' guard).
  - RBRACE row: suppress for EOF / ELSE / SEMI / RBRACE (the '}' guard
    from #635), emit for OTHER.
  - OTHER row: emit for ELSE / SEMI / RBRACE / OTHER, suppress for EOF
    (the trailing-newline guard from #620).

Structural changes:

  - classify_prev() reduces the last non-whitespace byte to one of five
    kinds. classify_next() does the same for the peeked next byte,
    including the 'else'-keyword check with a trailing-delimiter test.
  - kEmitSep[5][5] is the new control surface. The cells are the spec;
    the loop just looks up and acts.
  - strip_trailing_terminators() factors out the post-pass that walks
    back over trailing ';' and whitespace (#620 trailing-';' case).
  - peek_next_real_byte() is unchanged from #635/#640.

Tests added (transition-matrix cells previously implicit):

  - ';' x '}' - explicit ';' before newline-close-brace
  - '{' x '}' - empty block written across newlines
  - other x ';' - bare newline followed by explicit ';'
  - '}' x ';' - closing brace followed by newline then explicit ';'

All four pass against the pre-refactor code as well, confirming the
refactor preserves the existing contract for the previously-untested
cells. Other cells of the matrix are already exercised by the 35
tests added across #624 / #635 / #620.

External API is unchanged: normalize_newlines_to_semicolons(const char *)
still returns a freshly-malloc'd C string or NULL on allocation failure.
No new dependencies, no quote-state tracking (#638), no overflow-check
addition (#639) - those are tracked separately.

Test results:

  - Unit: 489/489 pass (unchanged)
  - Integration: 35 protocol_eval_compile_errors tests + 4 new pass;
    overall failure count unchanged from develop baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(repl): address /gate review for #641 (P1 #1, P2 #3, P2 #5)

P1 #1: drop the orphaned 58-line prose docstring at lines 338-395. The
refactor moved the function down ~190 lines (to make room for enums, the
transition table, and helpers), leaving its docstring stranded above an
unrelated typedef. Replace with a tight 17-line summary at the function
itself. The "table IS the spec" framing means we no longer need a duplicate
prose enumeration of the rules — the kEmitSep comment carries them.

P2 #3: enumerate the 5 moot-by-construction cells explicitly near kEmitSep
(START x ELSE, START x RBRACE, SEMI x ELSE, LBRACE x ELSE, LBRACE x EOF —
inputs that would exercise them are parse errors before normalization
matters). Previously the moot set was implicit and the agent's reported
count (6) was off by one.

P2 #5: rename strip_trailing_terminators -> strip_trailing_whitespace_and_semicolons.
The "terminators" name read narrower than the actual stripped set
(' ', '\t', '\r', '\n', ';').

No behavioral change. Tests: unit 489/489, integration 2034/68/207/2309
(target file 39/39 — even one flake-recovery vs the prior r1 run).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jsavin added a commit that referenced this pull request May 25, 2026
…#620 burndown) (#643)

* fix(repl): PREV_BINOP normalizer class for binary-operator continuations (#620 burndown)

5th hole in normalize_newlines_to_semicolons after #628/#635/#640. When a
UserTalk script ends a line with a binary-operator token ('and', 'or', '==',
'>=', '<=', '!=', '>', '<', '+', '-', '*', '/', ',', '('), the next line is
a continuation of the same expression. Previously the (PREV_OTHER, NEXT_OTHER)
cell emitted ';' between them, producing source like 'x and; y' that the
parser rejected. The 5 failing sys.* integration tests with multi-line
expressions (typeof(sys.os()) == stringType) and ...) were the visible symptom.

Add PREV_BINOP class to the transition table. The classifier (prev_is_binop)
walks back through the output buffer to detect multi-byte operator tokens
('and' / 'or' word-bounded, '==' / '>=' / '<=' / '!=' via byte-before check)
so single '=' (assignment) is correctly distinguished from '=='. New BINOP
row suppresses ';' for every next-kind.

Adds 8 integration tests in protocol_eval_compile_errors.yaml covering the
new transition cell and the disambiguation paths (single '=' assignment,
identifier ending in 'and' / 'or').

Integration: -5 failures (sys cluster) + 8 new tests, all green. Unit: 489/489.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(repl): add 4 BINOP regression tests for /gate P1 findings on #643

Bar-raiser flagged P1 test gaps:
- Docblock claims BINOP coverage for >=, <=, !=, >, <, *, /, - but tests
  only exercised and, or, ==, +, ',', '('.
- 'and' had a band/kand word-boundary regression but 'or' did not.

Add 4 new tests:
- '>=' pins the '>' arm of the byte-before-'=' disambiguation
- '!=' pins the '!' arm
- '*' pins the BINOP single-char arithmetic byte-class
- 'door' as variable pins the 'or' word-boundary check (symmetric to 'band')

Integration: 2321 total / 2050 passed / 64 failed / 207 skipped — +4 total,
+5 passed (the 4 new tests + a flake recovery), -1 failed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <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.

1 participant