Skip to content

mergeStatements() silently corrupts multi-line ternary statements that parse() accepts (statement-splitter inconsistency) #821

Description

@Shinyaigeek

Summary

mergeStatements() silently corrupts programs that parse() accepts: a statement written as a multi-line ternary is chopped at the first newline, and the ? / : continuation lines are silently dropped. The statement collapses to just its condition — even when the patch doesn't touch that statement at all.

The root issue is a statement-splitter inconsistency: the codebase has three statement splitters, and only two of them know about ternary continuation lines.

Reproduction

Standalone Node-only repro (published @openuidev/lang-core@0.2.9, npm install && npm run repro):
https://github.com/Shinyaigeek/openui-merge-ternary-repro

Existing program (previous LLM response, as in the incremental editing flow):

root = Stack([a, b])
a = $ok
  ? Title("Yes")
  : Title("No")
b = Title("Footer")

Patch touching only the unrelated b statement:

b = Title("Updated")

mergeStatements(existing, patch) returns:

root = Stack([a, b])
a = $ok
b = Title("Updated")
  • Expected: a is preserved intact — parse() sees it as a full ternary, so merge should too.
  • Actual: both branches are silently lost; a collapses to $ok.

Root cause

The canonical style is one statement per line (and that's what the prompt instructs), but multi-line ternaries have been deliberately part of the accepted grammar since openui-lang v0.5 (#385): both the batch splitter (split() in statements.ts, via ternaryDepth + newline look-ahead) and the streaming scanner (scanNewCompleted() in parser.ts) explicitly treat a ? / : continuation line as part of the same statement — presumably because LLMs format long ternaries this way regardless of instructions.

mergeStatements() however re-splits the existing program with a third, character-level splitter (splitStatementSource() in merge.ts) that tracks only bracket depth and string state and breaks on every depth-0 newline:

else if (c === "\n" && depth <= 0) { /* statement boundary */ }

The chopped ? Title("Yes") / : Title("No") fragments then fail the Ident = expression shape check in parseStatements()split(tokenize(raw)) and are discarded without any error.

Since the incremental-editing flow feeds the previous (rendered!) LLM response back in as existing, this breaks the invariant that matters: anything parse() accepts and renders must survive mergeStatements() unchanged when the patch doesn't touch it.

Suggested fix

Either of:

  1. Align: give splitStatementSource() the same ternary-continuation handling as split() / scanNewCompleted() (track ternaryDepth at depth 0; before treating a depth-0 newline as a boundary, peek ahead to see if the next meaningful character is ?, or : while inside a ternary).
  2. Unify (structurally stronger): eliminate the third splitter entirely — have mergeStatements() reuse the token-level split() for statement boundaries, so the parser and the merger can never disagree again.

The repro script exits non-zero while the bug reproduces, so it doubles as a regression check.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions