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:
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:
- 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).
- 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.
Summary
mergeStatements()silently corrupts programs thatparse()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):
Patch touching only the unrelated
bstatement:mergeStatements(existing, patch)returns:ais preserved intact —parse()sees it as a full ternary, so merge should too.acollapses 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()instatements.ts, viaternaryDepth+ newline look-ahead) and the streaming scanner (scanNewCompleted()inparser.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()inmerge.ts) that tracks only bracket depth and string state and breaks on every depth-0 newline:The chopped
? Title("Yes")/: Title("No")fragments then fail theIdent = expressionshape check inparseStatements()→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: anythingparse()accepts and renders must survivemergeStatements()unchanged when the patch doesn't touch it.Suggested fix
Either of:
splitStatementSource()the same ternary-continuation handling assplit()/scanNewCompleted()(trackternaryDepthat 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).mergeStatements()reuse the token-levelsplit()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.