7.4 KiB
Canonical writer correction: unary Negate
2026-09-13. Applies to the staged S2 canonical writer. Source contract inspected at 472fe7a4339a23a3339b2b91e8d53d76442f60c4; the subsequent test-only AsKind correction a2a28f8d23a46ba1696b6a8410d4d3b5c1808f49 does not change that parser contract. This note records an accepted design/source-review correction, not writer implementation or executable roundtrip evidence. Public source8 admission, parser limits, model types and Exact semantics are unchanged.
Superseded recommendation
The preserved r2 design, expression grammar section, recommends: “Canonical unary Negate always prints -(...)”. The initial S2 writer plan repeated that rule. Mandatory grouping of every Negate operand is superseded by the parent-aware prefix rule below. The r2 document remains byte-identical historical evidence, including its recorded copy SHA256 3a5b96fd130e3859932ee25544b3ff2f6a21f0774f3b5849e546f9574b9dbf7d.
The correction preserves the reason for the original recommendation: negative literal nodes and unary operation nodes must remain distinct. It removes only redundant syntax frames that can make an otherwise admitted typed tree unreadable after canonical writing.
Counterexample and unchanged limits
Use a Number-typed property and a case override whose binding contains 127 Unary Negates around Number(-1). The Expr tree has logical depth 128. An authored binding consisting of - repeated 127 times followed by ratio(-1, 1) fits the existing frame profile by source inspection.
The staged declaration parser counts a frame for each Negate, each explicit group and each ratio constructor, alongside declaration/override braces. See prefix and group parsing, shared frame cursor and the unchanged limit256.
| Binding location and spelling | Active-frame calculation | Peak |
|---|---|---|
| Case override, prefix minus without redundant groups | declaration1 + override1 + Negates127 + ratio1 | 130 |
| Case override, superseded mandatory groups | declaration1 + override1 + Negates127 + groups127 + ratio1 | 257: exceeds256 |
| Calculation body, prefix minus without redundant groups | declaration1 + Negates127 + ratio1 | 129 |
| Calculation body, superseded mandatory groups | declaration1 + Negates127 + groups127 + ratio1 | 256 |
A calculation-only writer test therefore misses the case-override failure. These counts describe the current staged declaration parser. Future Project/include caller frames remain inherited context under S4; they must still be counted. This counterexample is not a general resource proof for every depth128 tree, and the correction does not raise the parser ceiling.
Accepted writer policy
Emit Unary Negate as prefix - and write the operand with unary-precedence parent context:
- Parenthesize a lower-precedence operand, such as a binary expression:
- (1 + 2). - Do not add a redundant group around another unary or an atom/constructor:
- - 1,- abs(1),- ratio(-1, 1). - Keep the separating space in canonical prefix output. Existing source with other accepted spacing remains valid.
- Number and Quantity magnitude continue to use Exact::canonical_literal: negative literals remain
ratio(-n, d), including denominator1. No leading minus is substituted for a negative literal node.
The required topology discriminators are:
| Canonical binding | Model topology |
|---|---|
ratio(-1, 1) |
Number(-1) |
- 1 |
Negate(Number(1)) |
- ratio(-1, 1) |
Negate(Number(-1)) |
- - 1 |
Negate(Negate(Number(1))) |
- (1 + 2) |
Negate(Add(Number(1), Number(2))) |
No arithmetic evaluation, folding or reassociation is permitted. All other accepted precedence rules remain: group lower-precedence binary children; group a right binary child of equal precedence; group any comparison child of any comparison parent on either side; preserve ordered list and constructor children and callee-qualified call arguments.
S3 subtree boundary
A changed subtree must be emitted with parent context derived from its owning binding field and typed child path. It cannot always use top-level expression context.
For example, S2a's operand span in -(1) includes (1). Replacing that span with a binary subtree must emit (1 + 2), retaining the necessary group. Replacing it with a negative Exact atom or another unary needs no redundant group; the retained prefix still constructs a Unary node. Adjacent retained/new minus prefixes remain separate tokens in the existing raw scanner. Constructor delimiters outside the selected value span remain owned by the parent.
This is a writer helper contract. S3 still owns original-span selection, comment handling, simultaneous edit composition and complete-model recompile validation. Alias-only updates continue to preserve original spelling and trivia; they do not invoke canonical subtree normalization.
Required executable acceptance, not yet run
The writer milestone must author and execute these controls before claiming the correction implemented:
negate_override_roundtrip_preserves_depth128_within_frame256: start with authored complete Number property/case source using 127 prefix Negates aroundratio(-1, 1). Parse/lower/admit it; independently inspect exactly 127 Unary nodes and the negative Exact leaf; write the case; reparse/lower and compare the complete model and canonical byte stability. Assert the staged case peak 130 and calculation-body control 129.- Source-parser controls for the superseded spelling: the case reaches257 and fails under the unchanged ceiling256, while the corresponding calculation reaches256 and passes. Preserve the distinction when later adding inherited Project/include frames to full-document tests.
all_expr_variants_and_precedence_preserve_exact_trees: include every topology discriminator above alongside all13 Expr variants and existing binary/comparison grouping controls. Compare typed trees, not evaluated values.subtree_output_respects_parent_context_and_retained_delimiters: replace the entire grouped operand span in-(1)with an additive subtree, negative Exact atom and another Negate; reparse and compare the intended parent/child topology. Required grouping survives; unnecessary grouping is not forced.
Retain the separate width1024, output-byte/structural-work and cleanup controls. Existing S2b execution proves its own lowerer fixtures, not these future writer cases. No JSON depth-switch or native/performance acceptance follows from this source analysis.
Decision provenance
The independent critic /root/canvas_followup_acceptance_review identified the257-frame counterexample and rechecked the corrected prefix/parent-context policy against the frozen scanner/parser. Parent accepted the correction. The reconciled local writer plan has SHA256 e6ef6108792896cfb65e825cd19e984bf1c1729cff242653ed7947b4c6e9a803; its exact correction diff has SHA256 2e7d9f8627293301c450412466b7d2f27fac574cd4a0d1f09bccb2f36c1904be; the source-review report including the bounded recheck has SHA256 8ba73ffa442eeaa03f22f0137112a95aace199dbd7267f62de1737ba3e892ffa. These identify design/source-review evidence only. The complete rationale and acceptance contract are recorded here so implementation does not depend on local artifacts.