Repository navigation
🐛 fix: a builder has the members a page reaches, and its capacity follows .NET's chunks - #748
Merged
edgarmesquita merged 20 commits intoOct 10, 2026
Merged
Conversation
…lows .NET's chunks AppendFormat, AppendJoin, Capacity, MaxCapacity, EnsureCapacity, the Chars indexer, Length's setter, Equals(StringBuilder) and CopyTo transpiled to members the runtime's builder did not have, a TypeError or an undefined in the browser behind a green build. AppendFormat and AppendJoin append what string.Format and string.Join write, by the same lowering, its culture policy included. Capacity is .NET's: the runtime keeps the sizes of the chunks .NET would allocate, through appends, inserts, removals, replacements and a shorter Length, which matches .NET on 41 measured sequences. The indexer is the twin's item and setItem, Equals(object) stays identity, a provider other than the current culture on Append is EQ2108, and GetChunks and the span overloads are refused at the build (#679).
…ot override it has
Main's runtime is 203,119 bytes gzipped, the exception and builder work of #667 inside the 1% its record allows. This branch adds 1,308 more: the builder's indexer, length and capacity as .NET's chunks give them, AppendFormat, AppendJoin, CopyTo, Equals and the constructors' refusals. Together they pass the record's 1%, so the record moves to 204,427.
…members The audit's verdicts move as this change meant them to: AppendFormat and AppendJoin were "native" because a strategy answered, though the browser had no such method, and are "eq" now, routed to the runtime's builder; GetChunks is fenced with EQ1004. EQ2108 gains StringBuilderStrategy as a reporting site, with the meaning it has everywhere else, a CultureInfo the browser cannot follow: a builder's Append and AppendLine with a provider and an interpolation cross only CurrentCulture or a null, which DIAGNOSTICS.md now says.
…asured it From the review of #679, each measured on .NET 10. An insert or a replace opens a chunk of at least 16, and a replace one per source chunk, so Capacity after a later cut is .NET's (16 where it was 6). The ranged constructor refuses its capacity, length and start, and the (capacity, maxCapacity) one checks in .NET's order. CopyTo checks the destination's room first. Text that fits the last chunk goes in past MaxCapacity, as on .NET; an insert past it is requiredLength's refusal, Insert with a count an OutOfMemoryException, and a repeat past an array's length .NET's OutOfMemoryException in place of JavaScript's RangeError. A removal unlinks the chunks it empties. 11 of the 12 new conformance cases fail on the commit before.
# Conflicts: # tests/eQuantic.UI.Server.Tests/Budgets/served-runtime.json
…e main of #668 Main's runtime is 206,012 bytes gzipped, inside the 1% its record of 204,101 allows. This branch adds 1,806 more: the builder's indexer, length and capacity as .NET's chunks give them, AppendFormat, AppendJoin, CopyTo, Equals, the constructors' refusals, and the chunk sizes and refusals the review measured. Together they pass the record's 1%, so the record moves to 207,818.
There was a problem hiding this comment.
🟡 Changes recommended
Permanecem divergências funcionais em igualdade, atribuição por indexador, capacidades extremas, construtor com intervalo e spans explícitos.
2 open findings
What changed in this PR
Amplia a compatibilidade de StringBuilder no transpiler e runtime com semântica próxima ao .NET.
Changes:
- Implementa membros, indexadores, capacidade e construtores.
- Reutiliza os lowerings de
string.Formatestring.Join. - Adiciona especificações, documentação e testes de conformidade.
| File | Description |
|---|---|
tests/eQuantic.UI.Server.Tests/Budgets/served-runtime.json |
Atualiza o orçamento do runtime. |
tests/eQuantic.UI.Conformance.Tests/StringBuilderConformanceTests.cs |
Adiciona conformidade bilateral. |
tests/eQuantic.UI.Compiler.Tests/Strategies/StringBuilderStrategyTests.cs |
Testa novos lowerings e recusas. |
tests/eQuantic.UI.Compiler.Tests/Coverage/diagnostics.baseline.txt |
Registra o novo emissor de EQ2108. |
tests/eQuantic.UI.Compiler.Tests/Coverage/bcl-surface.baseline.txt |
Atualiza a auditoria da BCL. |
src/eQuantic.UI.Runtime/src/utils/string-statics.ts |
Compartilha mensagem de intervalo. |
src/eQuantic.UI.Runtime/src/utils/string-builder.ts |
Implementa a compatibilidade ampliada. |
src/eQuantic.UI.Runtime/src/utils/string-builder.spec.ts |
Testa membros e capacidade. |
src/eQuantic.UI.Runtime/src/utils/exceptions.ts |
Registra novas hierarquias de exceção. |
src/eQuantic.UI.Compiler/CodeGen/Strategies/Types/StringBuilderStrategy.cs |
Baixa membros e bloqueia APIs incompatíveis. |
src/eQuantic.UI.Compiler/CodeGen/Strategies/Primitives/StringStaticStrategy.cs |
Expõe lowerings reutilizáveis. |
src/eQuantic.UI.Compiler/CodeGen/Strategies/Expressions/Indexer.cs |
Inclui o indexador do builder. |
openspec/specs/transpiler-bcl/spec.md |
Incorpora o requisito principal. |
openspec/changes/archive/2026-10-07-a-builder-has-the-members-a-page-reaches/tasks.md |
Registra tarefas concluídas. |
openspec/changes/archive/2026-10-07-a-builder-has-the-members-a-page-reaches/specs/transpiler-bcl/spec.md |
Arquiva o delta da especificação. |
openspec/changes/archive/2026-10-07-a-builder-has-the-members-a-page-reaches/proposal.md |
Arquiva a proposta. |
docs/LEDGER.md |
Registra a correção. |
docs/DIAGNOSTICS.md |
Documenta EQ2108 para builders. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…and a capacity past an array's length is .NET's OutOfMemoryException From Copilot's first round on #748. The span fence exempted every params parameter, so a span passed whole in the normal form, AppendJoin(separator, span), reached JoinCall; only the values a call expands into a params span (ArgumentKind ParamArray or ParamCollection) cross now. And a capacity the builder only records was accepted past Array.MaxLength, where .NET allocates the chunk whole and throws OutOfMemoryException ("Array dimensions exceeded supported range."), measured on .NET 10 for the constructors, the Capacity setter and EnsureCapacity.
# Conflicts: # tests/eQuantic.UI.Server.Tests/Budgets/served-runtime.json
…e main of #672 Main records 209,149 bytes gzipped after #672's hot reload. This branch adds the builder's members on top (its indexer, length and capacity as .NET's chunks give them, AppendFormat, AppendJoin, CopyTo, Equals, and the refusals the reviews measured), which passes the record's 1%, so the record moves to 212,035.
# Conflicts: # tests/eQuantic.UI.Server.Tests/Budgets/served-runtime.json
…ents, and is out of memory past the browser's longest string Copilot's second round on #748 found two defects, and measuring them on .NET 10 found four more in the same model: - A named argument reached the twin in the order written: CopyTo(count: 2, destination: a, destinationIndex: 1, sourceIndex: 0) read the count as the source index. A call that names an argument is lowered through ParameterTemplate, each still evaluated in the order written, and new StringBuilder(capacity: 50, value: "ab") through the constructor's template. - Only String.repeat mapped a RangeError to OutOfMemoryException. Every text the builder makes now goes through one guard, and is built before the chunks change, so a refusal leaves the builder as it was. - An interpolated Append($"…") appended the whole text at once, reading every hole first: $"{b.Length}{b.Length}" held "00" where .NET holds "01". It appends each part in turn, as .NET's handler does, a hole's alignment padding in an append of its own (appendAligned), and a handler's plain hole now flows into text as a plain interpolation's does (ValueFlow), where an enum reached the builder as its camelCase key. - AppendLine(value) appends the line's end after the value: a 100-char line's capacity is 200, not 101, and a newline past the maximum is refused with the value already in. - Append(StringBuilder) is appendBuilder, which checks the maximum before it copies and refuses as "Capacity exceeds maximum capacity. (Parameter 'Capacity')". - A Replace past the maximum keeps the chunks it replaced first, as .NET replaces chunk by chunk.
…string, in the spec and the ledger
…e main of #690 Main's runtime is 214,048 bytes gzipped, inside the 1% its record of 213,928 allows. This branch adds 2,100 more: the builder's indexer, length and capacity as .NET's chunks give them, AppendFormat, AppendJoin, CopyTo, Equals and the refusals the reviews measured, and this round's appendBuilder, appendAligned, the Replace that refuses chunk by chunk and the guard that turns the browser's RangeError into OutOfMemoryException. Together they pass the record's 1%, so the record moves to 216,148.
edgarmesquita
pushed a commit
that referenced
this pull request
Oct 10, 2026
Merging main left this pull request's entry above #748's, which landed before it. The list is in the order things land, so it moves to the end, unchanged.
| case { Name: "AppendFormat" }: | ||
| return $"{receiver}.append({StringStaticStrategy.FormatCall(inv, inv.ArgumentList.Arguments, context)})"; | ||
| case { Name: "AppendJoin" }: | ||
| return $"{receiver}.append({StringStaticStrategy.JoinCall(inv, method, context)})"; |
Comment on lines
+169
to
+172
| var value = InterpolatedStringStrategy.Hole(hole, context, padded: false); | ||
| chain.Append(hole.AlignmentClause is { } alignment | ||
| ? $".appendAligned({value}, {alignment.Value})" | ||
| : $".append({value})"); |
| @@ -1,4 +1,4 @@ | |||
| { | |||
| "gzipBytes": 213928, | |||
| "gzipBytes": 216148, | |||
edgarmesquita
pushed a commit
that referenced
this pull request
Oct 10, 2026
edgarmesquita
pushed a commit
that referenced
this pull request
Oct 10, 2026
With main at 76122dd merged, the served runtime is 860,910 bytes and 218,383 gzipped, 74 bytes past the 1% the budget allows over its record of 216,148. Main alone measures 860,330 bytes and 218,228 gzipped, so this pull request's twin costs 580 bytes, 155 gzipped: a Flexible refusing a negative weight, basis or shrink and a basis that is not finite, a Spacer refusing a weight below one, a zero weight's basis of auto, and a basis written through px. The other 2,080 bytes of the move came from main since #748 recorded 216,148, inside its tolerance. Written with EQ_UPDATE_RUNTIME_BUDGET=1.
Merged
8 of 9 tasks
edgarmesquita
added a commit
that referenced
this pull request
Oct 11, 2026
…he translated surface (#715) Closes #569 ## What One measurement instead of one fix per report. `NullArgumentConformanceTests` calls every member of the BCL surface eqc translates with a null for each of its reference parameters, on .NET and in the browser, and compares what the call did: whether it threw and the exception's type, its `ParamName`, its message where the SDK composes it, and the value a call that returned answered, a string, a bool, a char, a number or an array of those. - The surface is derived, never listed. Its types are the ones the BCL audit's committed record names (`bcl-surface.baseline.txt`), its members are what reflection finds on them, constructors, indexers and generic methods included, and eqc's own diagnostics say which of them it translates. A guard fails where a member the audit grades `native` or `eq` takes a reference parameter and no null reaches it on both sides. - Each probe runs a control, the same call with the null replaced by a canonical value. The canonical text holds white space around and inside it, which is what a null separator or a null set of trimmed characters stands for in .NET. Where .NET answers the control and the browser answers it otherwise, the member differs whatever the argument, and the gap is recorded as `control` rather than charged to the null. A probe whose other arguments .NET refuses on their own measures those arguments, and is counted rather than compared. - The gaps left are `null-argument-gaps.baseline.txt`, which may only shrink: a new gap fails, an entry whose aspect moves fails, and an entry that starts to agree fails until it is removed. Each entry carries its aspect and a reason, and the file is regenerated with `EQ_UPDATE_NULL_ARGUMENT_BASELINE=1`. - The twins that read a null through null refuse it by name now, as .NET does: LINQ's `Max`, `Min` and `ToDictionary`, a sequence a LINQ operator reads through `seq`, named by the parameter it binds to (`second`, `inner`, a `Zip`'s `first`) where every one was `source`, `CompareTo(object)`, which answers 1 for a null, `new Guid(text)`, which names its text `g`, `char.GetUnicodeCategory(text, i)`, and the cancellation pair's `Register` and `CreateLinkedTokenSource`. For someone using the SDK: a null handed to one of those members is caught by `catch (ArgumentNullException e)`, and `e.ParamName` names .NET's parameter. Nothing an app writes changes. The public surface gains `Eq.GuidOf`, the developer surface does not move, and the transpiled twins that concatenate sequences are regenerated: their `seq` calls name `first` and `second`. ## Why A .NET method that cannot take a null refuses it with an `ArgumentNullException` naming the parameter. Several twins read the argument first, so the browser threw a `TypeError`, which the runtime reads as a `NullReferenceException`, and since #561 a typed catch lets that through. #561 fixed the seven its review named, by hand, and the rest of the surface had never been looked at. What the measurement leaves, 402 probes, each with its reason: - 213 are members lowered to JavaScript's own method: LINQ over a list (168), an array's or a list's method, a string's search, a spread. They read a null through null, and refusing it by name means a guard at every call. That is a design decision, so I left it out of this pull request. - 84 are provider overloads that eqc writes with the name `CultureInfo`, which the runtime does not export, so the call is a ReferenceError in the browser and nothing fences it at build time. eqc reads a culture only where it formats or parses a number, and a culture held in a local fails the same way. - 26 are `Convert` of an object or of a string (#401), 21 are members the runtime does not have (most of the sorted set's set operations, `CopyTo` on the queue, the stack and the sorted set, and `TryParse` of a `DateTime`, a `DateTimeOffset`, a `TimeOnly` and a `TimeSpan`), 6 are collection constructors whose twin reads a null as no collection, and 3 drop a culture or a calendar argument. - The other 49 are lowerings that are wrong whatever the argument, found on the way: `Convert.ToBase64String` and `ToHexString` answer `String()` of the array, `FirstOrDefault(defaultValue)` lowers the default as a predicate, the `Zip` of three sequences lowers as the one with a selector, `SequenceEqual` with a comparer answers its first sequence, `ToLookup` with a comparer reads it as an element selector, `Average` over longs adds a BigInt to a number, `LastIndexOf(value, start, count)` ignores its count and `LastIndexOf(value, start)` reads its start as JavaScript does (#724), six `String.Split` overloads hand their separators to JavaScript's `split` whole (#753), `new decimal(bits)` and `new Guid(bytes)` name classes the runtime does not export. The list was 443 when this pull request opened. Main moved under it since: #748 gave the builder its members, which took the 22 probes of #679 out and left two whole-array `AppendFormat` ones among the lowerings, and #660 refuses a constructor that takes a calendar at build time, 8 probes. Some cheap fixes stay in the list on purpose, because their files are in open pull requests: the collection constructors and the `Contains` and `Zip` twins (#707), `string.Format` and `string.Concat` with a whole array (#637), and the `ToHashSet` and `ToDictionary` shapes (#707). ## OpenSpec `a-null-argument-is-refused-as-dotnet-refuses-it`, archived, adds a requirement to `transpiler-exceptions`: a null argument is answered as .NET answers it, measured over the translated surface. ## Proof The surface is 30 types and 823 members, 1205 probes, each a member and the parameter handed null: 874 are compared on their null, 32 are counted because .NET refuses their other arguments, 293 are members eqc refuses at build time, 6 bind another overload as written and are left out, and 250 need an argument no canonical value can write. Every one of the 256 lines the audit grades `native` or `eq` that take a reference parameter is reached. A/B against main (7636da4), with main's version of every source file this changes, built with 0 errors: `NullArgumentConformanceTests` fails 17 of its 26 tests there. The theory fails with exactly the 67 probes the fixes close, and 13 of the 14 cases one canonical value cannot reach fail too (a negative or false receiver of `CompareTo(object)`, a set handed to `Concat`, a sequence as a `Zip`'s receiver, a null selector caught by a typed catch). And 3 of the 7 cases of a receiver with an effect fail there for the answer itself, a double's, a char's and a bool's. The 14th is the control that a value still compares, and the four checks of the instrument pass on both sides. On this branch, 26 of 26 pass. The fixes took the baseline from 510 entries to 443, and it holds 402 now, for what main and the wider measurement moved since. Copilot's first round found two. A receiver of `CompareTo(object)` that its comparison reads once never ran when the argument was a null literal, the null test answering before it reached the receiver. Every `CompareTo(object)` reads its receiver twice now, which makes the writer bind a receiver with an effect and run it first. With the strategy as it was before that round, 3 of the 7 cases of `TheReceiverOfCompareToObject_RunsBeforeItsNullIsRead` fail, an int's, a double's and a char's receiver, and here the 7 pass. And the measurement compared no array and its canonical text held no white space, so a split by null passed whatever the browser made of it. It compares an array of strings, chars, bools or numbers now, over a text that holds white space, and that is what found the six `String.Split` probes (#753) and moved `LastIndexOf(value, start)` to `control` (#724). Each suite alone, read by its exit code and its total, on this branch's head, a tree the merge of main 7636da4 left as it was: Compiler 1841, Web 1021 against this branch's wiki page, Server 367, the runtime's 2316 in 170 files (`tsc` and vitest through the embedded bun), and Conformance 4731 with 1 skipped. No run aborted. The served runtime is 218,583 gzipped bytes, 183 more than the 218,400 main serves, measured the same way. ## Checklist - [x] `dotnet test` on the affected test projects, and `dotnet build src/eQuantic.UI.Runtime -t:TestRuntime` - [ ] This PR's checks include a run named CI with jobs in it - [x] `./scripts/check-openspec.sh` passes: the change validates strictly and is archived - [x] `dotnet build samples/DefaultUIDashboard` - [x] A broken contract has a line in the migration notes: none is broken - [x] The wiki's SupportedFeatures page in English and Portuguese, on the wiki branch `fix/a-null-argument-is-refused-as-dotnet-refuses-it` - [x] One `docs/LEDGER.md` line for this event, citing #569 - [x] The diff reviewed by its author before the PR opened, and Copilot's first round answered: two findings, both fixed - [x] No "widget" in prose --------- Co-authored-by: Edgar Mesquita <edgar@equantic.tech>
edgarmesquita
pushed a commit
that referenced
this pull request
Oct 11, 2026
Merges 20066eb, the third of the five steps that bring this branch up to main eb33f44. It carries #666, #672, #688, #653, #699, #689, #690, #696, #733 and #748, and #748 is the one that meets this branch's source. How each source conflict was resolved: - ValueFlow.cs: both sides taught FlowsIntoText the hole of an interpolated string a handler takes (sb.Append($"...")), this branch by the hole's syntax (an AppendFormatted with no alignment and no format clause) and #748 by the bound operation (an argument of the handler's call that is the call's only argument written). #748's rule is kept, since it reads the operation the hole is, and the hole's text is still this branch's: a value that flows into text goes through StringConversion, which writes it in the page's culture. - InterpolatedStringStrategy.cs: #748 moved a hole's lowering into Hole(), which the builder's part by part append calls too, and this branch had changed the number kind the hole passes. Main's Hole() is kept with this branch's kind in it: FormatKind.Of where a specifier is written, and FormatKind.OfText with none, every number but a double. - StringBuilderStrategy.cs: #748 rewrote the dispatch (the span and GetChunks fences, named arguments bound through ParameterTemplate, AppendFormat, AppendJoin, the interpolated append part by part, appendBuilder and equalsBuilder), and this branch had written the value of Append, AppendLine and Insert in the culture through its own bound call. Main's dispatch is kept, and the value goes in front of it through the same ParameterTemplate.Call, its convert writing the value argument through StringConversion and every other argument as it is, each evaluated in the order it is written. A value that is text already passes as it is, as before (ValueInTheCulture, IsText). BoundArguments.Call, which this branch added for that value alone, goes: nothing else calls it. - docs/DIAGNOSTICS.md: EQ2108 keeps this branch's Convert.ToString and gains #748's builder clause, EQ2109 stays retired, and EQ2110 keeps this branch's reading-only meaning. - diagnostics.baseline.txt: EQ2108 gains #748's StringBuilderStrategy, EQ2109 stays out, and EQ2110 stays this branch's, ParseCulture alone. - bcl-surface.baseline.txt takes main's side and is regenerated after the last step. The runtime's twins in conflict take main's side, and every twin is regenerated after the last step. docs/LEDGER.md merged as a union, and this branch's line moves back to the end of the events.
edgarmesquita
pushed a commit
that referenced
this pull request
Oct 11, 2026
Regenerated from the merged compiler, as the merges left them on either side. - The twins (EQ_UPDATE_TRANSPILED=1, SharedComponentTranspilationTests) and the BoundReceivers fixture: a record twin's toString writes $eq.text.record(this, 'Name', [members]) with each member's kind or function, a number's text goes through the culture's formatter with its kind, and CodeMetrics' TypeStyle hydrates its FluidSize (#665). tsc is clean over them. - The BCL audit (EQ_UPDATE_BCL_BASELINE=1): eq=272, fenced=90, invalid-probe=6, native=665. The members this branch writes through the culture's formatter move from native to eq beside #748's: a builder's Append and Insert of a number, a bool or an object, which printed what the browser held, among them. The served runtime's budget is left at main's record. It is regenerated when the suites run.
edgarmesquita
pushed a commit
that referenced
this pull request
Oct 11, 2026
…head Eighteen pins of the compiler suite read the shape one side of the merge wrote, and take the merged one. - StringBuilderStrategyTests (#748) pinned a builder's interpolated append, AppendFormat and Append(object) as they were before the culture: a number's hole and a concatenated number write $eq.text.format with their kind, a format's values are boxed with $eq.text.asNumber, and a builder typed as an object is appended as its text. Eight cases, each read against what the merged compiler emits. - CultureCrossingTests matched a date's construction as $eq.time.dateTime(, and #660 builds a DateTime and a DateTimeOffset through a factory, $eq.time.dateTime.of(. Its pattern takes both, for the ten cases that build one.
edgarmesquita
pushed a commit
that referenced
this pull request
Oct 11, 2026
The line's numbers were measured against main at dc4a410. Against main as it is with #660, #748 and the queue landed (98c2b18's tree), 176 of the 222 conformance cases fail there and 46 already held, where it was 187 and 35: #660 taught a date's picture its offset and #748 a builder's interpolated holes their text since. The served runtime grows 3,834 gzipped bytes, 223,575 against the 219,741 main serves, and ten entries leave #715's null-argument baseline.
9 tasks done
edgarmesquita
added a commit
that referenced
this pull request
Oct 11, 2026
Closes #454 Closes #455 Closes #469 Closes #470 Closes #471 Closes #634 Closes #570 ## What A number and a date print in the browser as .NET prints them on the server, in the culture the page was rendered in, whatever path C# writes them through. - The format culture travels with every page, whether or not the app has a string to translate (#471). The server writes the request's culture as .NET holds it, its `NumberFormatInfo` and `DateTimeFormatInfo`, into `__EQ_CULTURE__.format`, and a switch with no reload fetches the same document from `/_equantic/culture/{name}.json`. A page with no culture installed formats in the invariant culture, never in its host's locale. The string catalogs carry strings only. - A number with no specifier is the culture's text wherever C# writes it: a concatenation, an interpolation hole, `ToString()`, `Convert.ToString`, `string.Format("{0}")`, `string.Join`, a `StringBuilder` (its value written by position or by name, or an interpolated string handed to it) and a record's text (#454). An integer never prints `-0`, which JavaScript makes of `-1 / 2`. - The compiler tells the formatter what a number is (#455): a double refuses `D`, `X` and `B` as .NET does, a `nint` and a `nuint` are 64-bit integers, and the per mille sign, an exponent's signs and the percent sign are the culture's own. - `N`, `F`, `C` and `P` are laid out from the culture's data, its patterns, symbols, separators, group sizes and default digits (#634). `Intl` laid them out, and wrote ar-EG's own digits where .NET writes ASCII ones, a no-break space where .NET's currency pattern has a plain one, and two digits for `N` where .NET reads three on ICU. - `DateOnly`, `TimeOnly` and `DateTimeOffset` print through the formatter a `DateTime` prints through, each with the specifiers its type takes (#469), and a custom picture writes the culture's `/` and `:`, the offset for `z`, `zz` and `zzz`, and the era for `g` (#470). - A calendar's names are the format data's, one copy, where they travelled twice. - A plain class or struct that declares no `ToString`, and derives none, writes its type's full name as .NET does (`My.App.Outer+In`), where the twin wrote `[object Object]` or a record's text (#570). A record keeps its synthesized text, and a generic type is left as it was. 46 of the runtime's own types write their name now. EQ2109 is retired. The public surface of `eQuantic.UI.Compiler` replaces `Eq.AsInteger` and `Eq.AsSingle` with `Eq.AsNumber` and gains `TypeSymbolExtensions.IsDate`, and `eQuantic.UI.Web` gains `CultureFormatBridge`. The developer surface does not move. ## Why The server renders a page in the request's culture, and the browser wrote numbers and dates its own way, so the SSR markup and the hydrated page printed one value two ways: `1.5` where the server wrote `1,5` in pt-BR, a currency with a no-break space where the server wrote a plain one, a `DateOnly` in a pattern nobody asked for. On an app with no `.resx`, the browser formatted in its host's locale, and the conformance suite passed or failed by the machine it ran on. ## OpenSpec `a-number-and-a-date-print-in-the-pages-culture`, archived in this pull request. It creates `page-culture` (the format culture a page is in, how it travels, the invariant culture with none installed, a standard number format laid out from the data, a calendar's names) and extends `transpiler-bcl` (a number's text with no specifier, a number's kind) and `runtime-dates` (the date types and a custom picture). Its design says what is not here: the culture a server action runs in (#580), a `DateTimeOffset`'s local time (#626, which #660 has landed since), and a non-Gregorian default calendar. ## Proof A/B against main (98c2b18, with #660, #748 and the whole queue landed), the same 222 conformance cases on both sides, both executed, with main's side given the culture as main installs it on a page with a catalog, its best case. 176 fail on main and all 222 pass here: 49 of the 70 number texts in five cultures, 7 of the 11 number kinds, all 6 standard layouts, 74 of the 75 date type texts, 38 of the 55 date pictures and 2 of the 5 harness cases. The other 46 already held on main and stay as pins. Against main at dc4a410 it was 187 of 222: #660 has taught a date's picture its offset and its kind since, ten cases, and #748 a builder's interpolated holes their text, one. Main's side was built on #708's head 78215e9, whose tree is main's (8833abd on both). The default text's seven cases, run through the module graph an app's build writes in TypeScript and in JavaScript, fail in both on the head before them (446542b) and pass here. The format subset is generated from .NET and replayed through the runtime byte for byte, in seven cultures, ar-EG and fr-FR among them. It folded the no-break spaces on both sides, which is how #634 passed it. The suites ran on a7155cc, a tree the merge of main 98c2b18 left as it was, since that merge records #708's squash with the ours strategy: Compiler 1898, Web 1021 against this branch's wiki pages, Server 374, Conformance 5087 with 1 skipped, and the runtime's 2355 in 170 files (`tsc` and vitest through the embedded bun). The only commit after it changes the ledger line's numbers, and the Web suite, the one that reads the ledger, ran again on it: 1021. The served runtime is 223,575 bytes gzipped against the 219,741 main serves (3,834 more, +1.74%, main's bundle built and measured the same way, and its record says the same), and its budget is regenerated to what this branch serves: the layout of N, F, C and P and the date types' formatter, the twins, whose numbers now say their kind, and the runtime types that now write their full name. Main moved under this branch more than once, and its pull requests changed the same files for their own reasons. Each merge keeps both behaviours. - #660 taught the formatter a `DateTime`'s kind. This branch had rewritten the date code around one moment per value, so the kind and the zone's offset moved into it: the round trip and `K` write `Z`, a local time's offset or nothing, `z` writes the offset, and `U` converts by the kind and stays inside the calendar. #660's own cases pass on this branch's formatter. - #661 printed a nullable float through `$eq.num.single`. Here the culture's formatter writes it with the kind `single`, the same digits in the culture's symbols. Every binding the emitted code declares starts with a `$`, the record text's arrow included. - #748 rewrote the builder. Its dispatch stays, a hole of an interpolated append is decided by the bound operation as #748 decides it, and the value `Append` and `Insert` write goes through the culture's text in front of it. So `Append(0.1f)`, `Append(1e21)`, `Append(-0.0)`, `Append(1e-7)` and `Append(Tone.Warm)` write what .NET writes, the rows #750 lists, measured through this branch's served runtime. Its last row stays: an enum held as an `object` still prints its key. - #731 gave a data twin's record text its members' number kinds and a delegate for its `ToString` method group. This branch had given the same text a descriptor per member. They are one `recordText(value, name, members)` now, each member its name alone or its name with a kind or a function, and `recordTextGroup` takes the same list. - #654 names a nested record's twin by its owner, and its text still prints its C# name. - #715's null-argument baseline loses 10 of its 401 entries and gains none. Seven now answer as .NET does: `Convert.ToString` of an object and of a string, with and without a provider, and a null format handed to `DateOnly`, `TimeOnly` and `DateTimeOffset.ToString(format, provider)`. Three are probes the build now refuses with EQ2108, a provider held in a variable, as it already refuses a `DateTime`'s. Two reasons that no longer said what the measurement shows are rewritten by hand. The merge with #667 met two things. Its builder refuses a null string handed to a ranged overload, as .NET does, and this branch wrote every bound value through the culture's text, which made that null an empty string. A value that is text already (a string, a char, a char[], a builder) now passes as it is, and only a number, a bool, an enum or an object is written in the culture. It also pinned an exception's interpolated message as it was before the culture, and that message now prints the number's hole in the culture. And the Windows runner failed one case on the previous head, where `AppendLine` writes the host's `"\r\n"`: the case reads `"\r\n"` as `"\n"` on both sides. And it runs, on this head: the dashboard sample, built with no warning and served from its built binary, wrote `R$ 1.234,50 · 98,2% · 13/08/2026` on its localization screen in pt-BR, byte for byte what the server rendered, with a plain space after `R$` on both sides, and wrote it again when the browser rendered the screen after a click. It switched to es with no reload by fetching `/_equantic/culture/es.json` and wrote exactly the server's es text, `1.234,50 ¤ · 98,2 % · 13/8/2026` (es is a neutral culture, with the generic ¤), and its charts' axes after a client navigation read as the server rendered them (`50.000`, `100.000`), with no console error. ## Checklist - [x] `dotnet test` on the affected test projects, and `dotnet build src/eQuantic.UI.Runtime -t:TestRuntime` if TypeScript changed (the embedded Bun runs `tsc`, then `vitest run`) - [x] This PR's checks include a run named `CI` with jobs in it (`build-packages`, `test (ubuntu-latest)`, `test (windows-latest)`, `openspec`, `session-start`, `wiki-checkout`). A workflow whose expression does not parse creates zero jobs, and the ruleset's required checks then keep the PR blocked. GitHub lists the workflow by its file path instead of `CI`. While GitHub Actions has no credits, the same run is on eQuantic Space (`eqs runs ls`) - [x] `./scripts/check-openspec.sh` passes: the OpenSpec change validates strictly, and it is archived before the merge - [x] `dotnet build samples/DefaultUIDashboard` (and `PhotonDesktop` / `WalletMobile` if the native track changed): CI's `samples` job builds all three on macOS only (#149), the other hosts are yours - [x] A broken contract has a line in the migration notes (we are in preview: break freely, hide nothing) - [x] The documentation changed with the behaviour: this repository's Markdown here, and the wiki page in English AND Portuguese, in one commit on a branch of the [wiki repository](https://github.com/eQuantic/equantic-ui/wiki) named exactly like this pull request's branch, merged into the wiki's master when this merges (the twin lives at `locale/pt-BR/<Page>-pt-BR.md` there) - [x] One `docs/LEDGER.md` line for this event, citing the issue - [x] The diff reviewed by its author before the PR opened (in Claude Code, `/code-review high`), every Copilot thread answered and resolved, and a new round asked for (`gh pr edit <n> --add-reviewer @copilot`) only after a defect, and three rounds at most - [x] No "widget" in prose: the project's word is *component* --------- Co-authored-by: Edgar Mesquita <edgar@equantic.tech>
5 of 6 tasks
edgarmesquita
added a commit
that referenced
this pull request
Oct 11, 2026
Closes #756 The bump to `0.2.0-preview.61`, cut from main at 9305100 (#637) at Edgar's request. It carries forty-five pull requests since `.60`: a component that hears the server on a typed topic (#647), a page that requires its authorization and a Server Action that refuses another site's request (#686, #737), a layout that follows its box and type that follows the window (#648, #665), a control's press and focus on every target with a 24dp floor under a pointer (#617, #628), the code editor's completion (#618, #653, #733), a hot reload under `dotnet watch` that keeps the page's state (#666, #672), the image optimizer on SkiaSharp (#730), the places where the producers drew one tree differently, with Photon's hit-testing and its text layout (#688, #690, #699, #739, #741, #749), a number and a date printed in the page's culture (#637), and a long family of browser answers made .NET's (#579, #588, #594, #607, #608, #621, #654, #656, #660, #661, #667, #668, #689, #696, #697, #705, #707, #708, #709, #715, #731, #748, #752), with a page that answers HEAD (#610) and the conformance harness's own comparison (#609). ## What this commit does - `Directory.Build.props`: `0.2.0-preview.60` → `0.2.0-preview.61`. - `./scripts/public-api.sh ship`: nine projects, 462 declarations join the shipped surface and 43 retire, and every `Unshipped` is empty afterwards. Checked after the fold, per project and compared as sets: the lines that left `Shipped` are exactly the 43 `*REMOVED*` lines declared on main, and the lines that joined are exactly its 462 declarations, so an orphan could not vanish unnoticed. The line diff reads +463 −549: the version line, 462 lines into `Shipped` and 43 out of it, and the 505 that leave the `Unshipped` files. - The analyzer accepts the fold: the solution builds with no RS0016 or RS0017. And it read the files: one bogus declaration appended to an `Unshipped` failed the build with RS0017. - Against `v0.2.0-preview.60` the `Shipped` diff has those 43 lines gone and no other, so no pull request rewrote a shipped line in place this time, as #512 did in `.60`. ## What the retirements are Read out of the `*REMOVED*` lines, each paired with the line that joined under the same member and attributed by the commit that wrote it, not from the log. Only `ServerActionHub` left with nothing in its place, and the rest changed shape: - `ServerActionHub` and its constructor (#647): the hub, its route `/_equantic/hub` and the `AddSignalR()` call `AddUI` made are gone. An app that injected `IHubContext<ServerActionHub>` publishes through `IServerEventPublisher`, and one that maps hubs of its own calls `AddSignalR()` itself. - `ICodeCompletionProvider.CompleteAsync` (#618) takes a `CodeCompletionContext` and answers a `CodeCompletionList`. `CurlyBraceLanguage.Keywords` was the protected abstract set a language overrode, and it is the public list a completion offers now. - `Drawing`'s constructor, `Drawing.Width` and `UI.Drawing` (#648) hold the width as a `SizeValue`. `new Drawing(art, 240)` converts, and a read into a `float` becomes `drawing.Width.Value`. - `ImageOptimizer()` and `BlurPlaceholderGenerator()` (#730) take the `ImageOptimizationOptions`. `UseImageOptimization` registers both, so only an app that built one with `new` meets it. - `HitRegion` (#690) gains `Drawn` after `Bounds`, so building one and deconstructing one both change. `CodeRegion` (#653), `SheetRegion` and `TextRegion` (#690) gain optional members at the end, so only a positional deconstruction changes. - `ServerActionsMiddleware`'s constructor (#737) takes an `IOptionsMonitor<ServerActionsOptions>`. `UseServerActions()` builds it, so only an app that constructs the middleware itself meets it. - Four signatures widened at the end by optional parameters, which compiles as before from source: `UI.Positioned` by the four fractions and the two shifts (#648), `UI.Pressable` by `canRequestFocus` (#653), `WebRealizer.Lower` by `density` (#688) and `PhotonRealizer.Realize` by `sizedLike` (#690). - `eQuantic.UI.Compiler`, which no app references, 21 lines: eleven of #661 (`JsTemplate`'s `Annotate`, with its constructor, its `Deconstruct` and the factory that took it, and six LINQ strategies that crossed to the IR and lost their text `Convert`), four of #608 (`ValueMember`, `ComponentDependencyResolver`'s constructor and `RecordTypeEmitter.CanEmit`), two of #637 (`Eq.AsInteger` and `Eq.AsSingle`), two of #654 (`UserDefinedOperators.Binary` and `Unary`), and `HashSetStrategy` (#594) and `RangeIndexerStrategy` (#707) crossing to the IR. One break is an addition, which no `*REMOVED*` line shows. `CurlyBraceLanguage.ReservedWords` (#618) is a new abstract member, so a language derived from it renames its `Keywords` override to `ReservedWords`. I listed every member that joined an interface or an abstract class already shipped, and it is the only one an implementer has to write: `ICodeLanguage.Keywords` and `ICodeCompletionProvider.ResolveAsync` have default implementations. The additions are 118 in `eQuantic.UI.Code` (the completion), 116 in `Primitives` (the server events vocabulary, `FluidSize`, `Positioned`'s fractions and shifts, `BoxStyle.Pressed`, `GridTrack.AutoFill`, `Touch.MinPointerTarget`), 98 in `Server` (server events, the origins a Server Action allows), 86 in the compiler, 19 in `Native.Components`, 12 in `Web`, 7 in `Components`, 4 in `Native.Framework` and 2 in `Images`. ## The rest of the surface `developer-surface.baseline.txt` gains two configuration sections since `v0.2.0-preview.60`, `EQuantic:ServerActions` (#737) and `EQuantic:ServerEvents` (#647), and loses nothing: no MSBuild property or template parameter came or went. What an app meets with no signature moving: - Three new build errors. EQ1009 (#608, #621, #708) stops two constructors that take the same number of arguments, in a class, a record or a struct that reaches the browser, where the twin kept one and a `new` that meant another ran it. EQ2014 (#708) stops an exception built or tested where a type argument it carries is a type parameter. EQ2013 (#647) stops a `ServerTopic<T>` built over a type parameter, which only new code can meet. - Refusals widened under existing codes: EQ1007 by a second indexer and a sequence over two types (#707, #708), EQ2004 by a stored `Range`, a method group over a .NET extension and a .NET collection's own `GetEnumerator()` (#656, #707, #708), EQ2007 by a comparer handed to `ToDictionary`, `ToLookup`, `GroupBy` or `Distinct` (#707), and EQ2010 by an extension method of a server-only class and a type nested in one (#654, #705, #731). - Two refusals a running app meets on the server: a Server Action called from another origin answers 403 until the app lists that origin (#737), and a class-level `[Authorize]` guards the page as well as its actions (#686). - `image/avif` in the optimizer's `Formats` stops the app at startup (#730), and a `Spacer` of weight below 1 or a negative `Flexible` weight throws where it is written (#741). - EQ2109 is retired and EQ2110 is about reading a number now, since a number written as text follows the page's culture on both sides (#637). The rest are browser answers made .NET's and drawings made alike across the producers, listed in the tag. ## The wiki's version marks `WikiVersionMarkTests` compares a mark only once a tag holds its symbol, so the `.61` tag is when this release's marks are judged for the first time. Emulated before the tag, with the test's own pattern and lookup and a `.61` at 9305100: 145 marks, no mismatch. The thirteen it judges first resolve to `.61`, as written: `StartFraction`, `GridTrack.AutoFill`, `canRequestFocus`, `DENSITY_COOKIE`, `Drawing`'s `SizeValue` width, `WithPageAuthorization`, `AllowServerActionOrigins`, `IServerEvents`, `MapGetAndHead`, `UseServerEvents`, `TwinIsData` on the tokens, `BoxStyle.Pressed` and `ScrolledThreshold`. ## The tag's gate, rehearsed `gate-templates` runs only on a `v*` tag, so a template that does not build costs a release attempt. Rehearsed on main at 9305100, before this branch: 50 packages packed at `1.0.0-dev`, the three web and the four native template shapes scaffolded and built in Release against that feed alone, with no warning and with every eQuantic package restored at the version packed, the published `runtime.js` identical byte for byte to the Server package's, and the dashboard template served, hydrated and navigated in a browser with no console error. ## Not in this release No pull request is open. Forty-eight bugs are, none of them a regression against `.60`. Three issues came out of this stretch and wait for a decision or their turn: #753 (`string.Split` with an array of separators, a count or a null separator), #754 (an app's exception class in a snippet compiled on its own) and #755 (whether generic code should carry its type arguments at run time, which EQ2014 fences). The release notes travel in the annotated tag. The wiki's Upgrading page carries the distilled entry in both languages, on the wiki branch `chore/0.2.0-preview.61`, which merges into the wiki's master when this merges and before the tag. The `docs/LEDGER.md` line for the release follows the tag, as `.60`'s did, because it names the commit the tag lands on. ## Checklist - [x] The diff reviewed by its author before the PR opened: a version line and a mechanical fold, verified as sets above - [x] `dotnet test` on this branch: Compiler 1898, Server 374, Conformance 5087 with one skipped, Web 1021, and the runtime's own 2355 in 170 files, none aborted - [ ] This PR's checks include a run named CI with jobs in it - [x] `./scripts/public-api.sh ship` run, every `Unshipped` empty, the retirements read from the diff - [x] The documentation changed with the release: the Upgrading entry in English and Portuguese, on the wiki branch `chore/0.2.0-preview.61`, with the wiki guards passing against it (9 of 9, and the vocabulary guard fails on that page when a word it refuses is written into it) - [x] Copilot's rounds answered: one round, approval recommended with no finding and no thread, so the loop ends there Co-authored-by: Edgar Mesquita <edgar@equantic.tech>
7 of 9 tasks
edgarmesquita
added a commit
that referenced
this pull request
Oct 11, 2026
<!-- claude-projects-attribution: {"github_login":"edgarmesquita"} -->
_Requested by **DomusAvus** · [project
thread](https://claude.ai/code/project/chan_01HYkHRWGXCkeLtg24Q261sW?thread=cmsg_01HYkHRWGXCkeLtg24Q261sWLavRoGRpXHMLSoMBdziF1J)_
Closes #753
Closes #724
Closes #750
## What
Before: `string.Split` was JavaScript's `split` of its first argument.
`" abcd ef ".Split(new[] { 'a', 'd' })` answered the whole string,
because the array became the text `a,d`. A null separator was the text
"null", and an empty separator, or an empty array, cut the text into its
characters. The count was dropped, `TrimEntries` was ignored, and
`RemoveEmptyEntries` was read from its spelling, so options held in a
variable were lost. `IndexOf` and `LastIndexOf` of a string from a
start, and for a count, were JavaScript's searches, which take no count
and read `lastIndexOf`'s start as where a match may begin:
`"abcabc".LastIndexOf("bc", 4)` answered 4, and `LastIndexOf("a", 5, 2)`
answered 3.
After: every `Split` overload answers what .NET 10 answers:
- one separator (a char or a string) splits at each of its places, and a
null or empty one splits nothing;
- a set of separators (chars or strings, as an array, a collection
expression or one by one) splits at any of them, the first in the set's
order winning at a place, and a null or empty set splits at .NET's white
space;
- the count caps the entries, and the last one takes the rest;
- `TrimEntries` and `RemoveEmptyEntries` apply wherever the value comes
from;
- a negative count and an undefined option throw .NET's exceptions, in
.NET's order.
`IndexOf` and `LastIndexOf` of a string from a start and for a count
give .NET's answers (1 and -1 above), and refuse a range outside the
string and a null value with .NET's exceptions.
How:
- **The twins.** `$eq.text.split` (one separator) and
`$eq.text.splitAny` (a set), in the new `utils/string-split.ts`, follow
.NET 10's `SplitInternal`, `SplitWithoutPostProcessing` and
`SplitWithPostProcessing` step for step. `Split()` is `splitAny` with no
set, so `$eq.text.splitOnWhiteSpace` goes.
- **The lowering.** eqc picks the twin by the bound overload and passes
the count and the options in their parameters' places, as the values
they are, through `ParameterTemplate`, so named arguments land in their
places and run in the order written. Chars a params parameter takes one
by one become an array. A span handed whole fails the build (EQ1004); a
collection expression is an array in the browser.
- **The searches.** The start and count overloads with no comparison go
to the runtime's existing searches by ordinal. That matches the overload
without a start, which already searches by ordinal here where .NET
searches by the current culture (#532).
- **With no model.** The spelling places what it can. A second `Split`
argument held in a name could be either the count or the options, so it
is a build error rather than a guess, as `string.Format`'s first
argument already is.
#750: `StringBuilder.Append` and `Insert` of a float, a double and an
enum already print as .NET prints them since #637, and are pinned now.
The rows the issue lists for an `object` that holds a float or an enum
still print what the browser holds. That is boxing losing the value's
type everywhere (`o.ToString()`, `$"{o}"`, `"" + o`), not the builder,
so it moves to #768, beside #552's identity half.
## Why
#715's null-argument measurement found #753 and #724, and #748's review
found #750. All three are Bugs under #565.
Each .NET answer was measured with a net10.0 probe, and the split's
order of checks was read from dotnet/runtime's `release/10.0`
`String.Manipulation.cs`.
## OpenSpec
`openspec/changes/archive/2026-10-11-a-string-splits-searches-and-appends-as-dotnet`,
archived in this pull request, changes `transpiler-bcl` in two places:
- it adds "A string splits as .NET splits";
- it modifies "A string's search checks its start and its count as .NET
does" to cover the overloads with no comparison.
## Proof
- **Conformance.** `StringSplitAndSearchConformanceTests` is new, with
73 cases run on .NET and in bun: 55 fail at 1850922 and all pass here.
The builder's new theory has 10 cases, all passing on main too, kept as
pins. The full conformance suite passes, with one test skipped. 10
entries leave `null-argument-gaps.baseline.txt` (the `Split` array
overloads and the four searches), and none is added.
- **Runtime.** `dotnet build src/eQuantic.UI.Runtime -t:TestRuntime`
passes: tsc, then 2399 vitest tests, including the new
`string-split.spec.ts`.
- **Compiler.** Compiler.Tests passes, 1914 tests. It includes new cases
for the bound lowering, the span refusal and the no-model spelling. The
BCL audit moves ten verdicts from `native` to `eq`, and the
introduced-functions baseline loses the filter arrow the old
`RemoveEmptyEntries` lowering wrote.
- **Transpiled library.** Four shared components split through the twin
now: `CodeCompletionView`, `CodePatch`, `MarkdownParser` and
`MermaidParser`. Their pins change only on the five lines that call
`Split`.
- **Other suites.** Web.Tests 1021 once the four pins were regenerated
(its wiki guards run against this change's wiki branch), Server.Tests
374, Design.Tests 104, and `dotnet build samples/DefaultUIDashboard` all
pass.
- **Review.** An independent review traced the twin against .NET's
source and the lowering against `ParameterTemplate`. It found no defect
on the model path. On the no-model path it found two silent misreadings
(options held in a name read as the count, and a comparison held in a
name read as a start), both fixed here, each with a case.
- **Runtime size.** The served runtime grows about 680 gzipped bytes
(bun's bundle, gzip -9: 225,409 on main, 226,084 here). The budget's
record moves to 226,675 in a commit that says what the bytes bought.
## Checklist
- [x] `dotnet test` on the affected test projects, and `dotnet build
src/eQuantic.UI.Runtime -t:TestRuntime` if TypeScript changed (the
embedded Bun runs `tsc`, then `vitest run`)
- [ ] This PR's checks include a run named **CI** with jobs in it
(`build-packages`, `test (ubuntu-latest)`, `test (windows-latest)`,
`openspec`, `session-start`, `wiki-checkout`). A workflow whose
expression does not parse creates zero jobs, and the ruleset's required
checks then keep the PR blocked; GitHub lists the workflow by its file
path instead of `CI`. While GitHub Actions has no credits, the same run
is on eQuantic Space (`eqs runs ls`)
- [x] `./scripts/check-openspec.sh` passes: the OpenSpec change
validates strictly, and it is archived before the merge
- [x] `dotnet build samples/DefaultUIDashboard` (and `PhotonDesktop` /
`WalletMobile` if the native track changed) — CI's `samples` job builds
all three on macOS only (#149); the other hosts are yours
- [x] A broken contract has a line in the migration notes (we are in
preview: break freely, hide nothing). No app-facing contract breaks. The
Compiler's public surface replaces the `Eq.SplitOnWhiteSpace` constant
with `StringSplit` and `StringSplitAny`; these are lowering targets that
no app writes
- [x] The documentation changed with the behaviour: this repository's
Markdown here, and the wiki page in English AND Portuguese, in one
commit on a branch of the [wiki
repository](https://github.com/eQuantic/equantic-ui/wiki) named exactly
like this pull request's branch, merged into the wiki's master when this
merges (the twin lives at `locale/pt-BR/<Page>-pt-BR.md` there). The
commit is written (SupportedFeatures, EN and pt-BR), but this session
cannot push to the wiki. It waits as a patch for
`fix/a-string-splits-searches-and-appends-as-dotnet` there, and applies
after the two patches before it
- [x] One `docs/LEDGER.md` line for this event, citing the issue
- [ ] The diff reviewed by its author before the PR opened (in Claude
Code, `/code-review high`); every Copilot thread answered and resolved;
a new round asked for (`gh pr edit <n> --add-reviewer @copilot`) only
after a defect, and three rounds at most
- [x] No "widget" in prose; the project's word is *component*
---------
Co-authored-by: Edgar Mesquita <edgar@equantic.tech>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Closes #679
It is a sub-issue of #565 (the transpiler's fences hold on every path).
What
Every
StringBuildermember a page reaches answers as .NET's does in the browser, or fails the build where it cannot cross:AppendFormatandAppendJoinappend whatstring.Formatandstring.Joinwrite, through the same lowering, bound by the same parameters and under the same culture policy.Capacityis .NET's. The runtime keeps the sizes of the chunks .NET allocates, soCapacityfollows every edit: an append fills the last chunk and opens one as large as the text so far (up to 8,000) or as what is left, an insert or a longer replace makes room asMakeRoomdoes (in place in a small chunk with room, otherwise a chunk of at least 16, one per source chunk for a replace), a removal unlinks the chunks it empties, and a shorterLengthkeepsmin(Capacity, max(Length * 6 / 5, the last chunk)).MaxCapacity,EnsureCapacity, theCapacitysetter and the constructors that take a capacity, a maximum or a range answer and refuse as .NET's do, in .NET's order. Text that fits the last chunk goes in pastMaxCapacity, as on .NET.Charsindexer is the twin'sitemandsetItem, refused past the text as .NET refuses it.Lengthcuts the text or fills it with\0.Equals(StringBuilder)compares the text whatever the capacity, whileEquals(object)stays identity.CopyTocopies into achar[].AppendandAppendLinewith an interpolation (EQ2108),GetChunksand an overload over a span or a memory (EQ1004). Aparamsspan is the values written one by one, and crosses.Append($"…")orAppendLine($"…")appends each part in turn, as .NET's interpolation handler does, so a hole that reads the builder sees the parts before it ($"{b.Length}{b.Length}"held "00" where .NET holds "01"), each part grows the chunks, a refusal keeps the parts before it, and a hole prints as a plain interpolation's does, where an enum reached the builder as its camelCase key.AppendLine(value)appends the newline after the value,Append(StringBuilder)checks the maximum before it copies, and aReplacepast the maximum keeps the chunks it replaced first.OutOfMemoryException, not JavaScript'sRangeError.For an app:
sb.AppendFormat("{0}-{1}", 1, 2),sb[0],sb.Capacityandsb.Length = 4work in the browser, where they were TypeErrors, undefined or writes nobody read, andsb.Append($"{sb.Length}…")writes what .NET writes. Nothing an app writes changes, and the public surface does not move.Why
The BCL audit graded every
StringBuildermembernative, which only said that a strategy answered. The runtime's builder hadappend,appendLine,insert,remove,replace,clear,toStringand alengthgetter, and eqc wrote every other member under its own name, so a page reached TypeErrors and undefined values behind a green build (#679, found while fixing #650 in #667).OpenSpec
openspec/changes/archive/2026-10-07-a-builder-has-the-members-a-page-reaches, archived intoopenspec/specs/transpiler-bcl.Proof
StringBuilderConformanceTests, both sides executed: 31 of the first 88 new cases failed on the base, the rest pinning members that answered by accident.Capacitymatches .NET 10 on 41 measured sequences./code-review high) found nine things, each measured on .NET 10 withdotnet fsibefore it was fixed: the room an insert or a replace opens (Capacity16 on .NET where the browser said 6), the ranged constructor's refusals, the constructor's order of refusals,CopyTo's order, the refusal of an insert pastMaxCapacity(requiredLength, andOutOfMemoryExceptionfor a counted insert), text that fits the last chunk pastMaxCapacity,OutOfMemoryExceptionin place of aRangeError, and emptied chunks left in the list. 11 of its 12 new conformance cases fail on the commit before.AppendFormatandAppendJoinareeq,GetChunksis fenced), and EQ2108 gainsStringBuilderStrategyas a reporting site with its documented meaning, whichdocs/DIAGNOSTICS.mdnow names.TestRuntime(2,262) and Server (318, the budget included), with Compiler, Conformance and Web rerunning.CopyTo(count: 2, destination: a, …)read the count as the source index): a call that names an argument goes throughParameterTemplate, each still evaluated in the order written. OnlyString.repeatmapped aRangeError: every text the builder makes goes through one guard, built before the chunks change. In V8 (Node 24), append, insert and replace past the engine's longest string threwRangeError: Invalid string lengthon the commit before and throwOutOfMemoryExceptionnow, the builder unchanged. The four more: an interpolated append part by part,AppendLine's newline as an append of its own,Append(StringBuilder)'s check of the maximum, and aReplacethat refuses chunk by chunk. On the commit before, 14 of the 16 new conformance cases, 10 of the 12 new compiler cases and the 3 new runtime cases fail, the runtime's under V8 and JavaScriptCore alike. The cases that pass there pin what must not change:Append(object)holding a builder is a string's append, a builder appended to itself is its text, and a string's append stays one append.Append(T)of a float, a double or an enum still prints what the browser holds, filed as A value a builder appends prints as .NET prints it #750.paramsoverload through (only the values a call expands into it cross now, EQ1004 otherwise), and a capacity pastArray.MaxLengthwas accepted where .NET throwsOutOfMemoryException(measured for the constructors,CapacityandEnsureCapacity). The one new compiler case and the three new conformance cases fail on the commit before.Checklist
dotnet teston the affected test projects, anddotnet build src/eQuantic.UI.Runtime -t:TestRuntimeif TypeScript changed (the embedded Bun runstsc, thenvitest run)build-packages,test (ubuntu-latest),test (windows-latest),openspec,session-start,wiki-checkout). A workflow whose expression does not parse creates zero jobs, and the ruleset's required checks then keep the PR blocked. GitHub lists the workflow by its file path instead ofCI. While GitHub Actions has no credits, the same run is on eQuantic Space (eqs runs ls)./scripts/check-openspec.shpasses: the OpenSpec change validates strictly, and it is archived before the mergedotnet build samples/DefaultUIDashboard(andPhotonDesktop/WalletMobileif the native track changed) (CI'ssamplesjob builds them)GetChunks, a span overload and a provider other than the current culture fail the build where they failed in the browser.docs/DIAGNOSTICS.md, the ledger), and the wiki page in English AND Portuguese, in one commit on a branch of the wiki repository named exactly like this pull request's branch (SupportedFeatures.md)docs/LEDGER.mdline for this event, citing the issue/code-review high); every Copilot thread answered and resolved; a new round asked for only after a defect, and three rounds at most