refactor(test): use <+ template writing in test files - #4111
Merged
Conversation
Mechanical sweep of test and benchmark files only; no library code and no
change to any expected output.
Two shapes:
* `buf.write_string("...\{x}...")` -> `buf <+ "...\{x}..."`. The old form
built a throwaway `StringBuilder` + `String` for the interpolation and
then copied the result into `buf`; the template writes the pieces straight
through.
* Runs of `write_string`/`write_char`/`write_object` collapsed into one
template, e.g. `write_object(i); write_string(": "); write_object(x);
write_string("\n")` -> `buf <+ "\{i}: \{x}\n"`. Adjacent literal chunks are
concatenated by the desugarer at compile time, so the literal part of a run
becomes a single `write_string`.
Test bodies use the plain `\{x}` hole rather than the inline-writer form
`\{l => l.write_object(x)}`; `\{x}` coerces to `&Show` and allocates a box
per hole, which is the right trade in a three-element assertion but not in
library code.
Split out of the library-side sweep so it can land on its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bobzhang
added a commit
that referenced
this pull request
Aug 19, 2026
Replaces `write_string`/`write_char`/`write_object` sequences and
`write_string("...\{x}...")` calls with the `<+` template-writing operator
in library code. The test-file half of this sweep is #4111.
Three shapes appear, chosen by what the old code did:
* `buf.write_string("...\{x}...")` -> `buf <+ "...\{x}..."`. The old form
built a throwaway `StringBuilder` + `String` for the interpolation and
then copied it into `buf`; the template writes the pieces straight through.
* Adjacent literal writes -> one template. The desugarer concatenates
adjacent literal chunks at compile time, so `write_string("a")` +
`write_string("b")` becomes a single `write_string("ab")`.
* `write_object(x)` -> `\{l => l.write_object(x)}`, not `\{x}`. Plain
`\{x}` desugars to `write_string_interpolation(x)`, which coerces `x` to
`&Show` and allocates a trait object per interpolation (~40% slower on
both native and js). The inline-writer form compiles to exactly the same
code as the `write_object` call it replaces.
`json`'s `escape`, `v128`'s `u64_hex` and `BytesView`'s byte escaping grew
writer variants (`escape_to` / `u64_hex_to`) so that their callers stream
into the target logger instead of materializing an intermediate `String`.
`BytesView::escape_to` also de-duplicates the escaping loop that `Show` and
`ToJson` each had a copy of; it is generic over the logger rather than
taking `&Logger`, so `ToJson`'s concrete `StringBuilder` keeps direct
dispatch (taking `&Logger` there costs ~5% on `Bytes::to_json`).
Measured on `json/stringify_bench_test.mbt` (added here), escape-heavy
document, n=200:
native 152.0 us -> 122.8 us (1.24x), indent=2 158.4 us -> 131.5 us
js 193.6 us -> 175.8 us (1.10x), indent=2 206.3 us -> 186.2 us
`builtin/tuple_show.mbt` is deliberately left alone: its `write_object`
runs would need the inline-writer form to stay allocation-free, and a
16-element tuple would become a single ~450 character string literal that
`moon fmt` cannot break.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Refactors MoonBit test and benchmark files to use the <+ template-writing operator instead of write_string / write_char / write_object sequences (and string-interpolation passed into write_string), reducing intermediate string allocations while preserving existing snapshot/expected outputs.
Changes:
- Replace
buf.write_string("...\{x}...")-style interpolation (which materializes an intermediate string) withbuf <+ "...\{x}..."in tests/benches. - Collapse multi-call write sequences into single
<+templates (often adding\nliterals directly in the template). - Minor formatting adjustments to keep the test output identical while simplifying write logic.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| strconv/quickcheck_test.mbt | Use <+ templates to build structured numeric strings without intermediate to_string() writes. |
| strconv/double_differential_test.mbt | Collapse per-line output assembly into single <+ templates for probe rendering. |
| sorted_set/set_test.mbt | Replace per-element interpolated write_string with <+ in iteration output. |
| sorted_map/map_test.mbt | Use <+ templates for map iteration formatting in tests. |
| set/linked_hash_set_test.mbt | Convert iterator element formatting to <+ templates. |
| priority_queue/priority_queue_test.mbt | Use <+ templates for priority queue iteration formatting. |
| option/option_test.mbt | Replace multi-step newline writing with "<+ \"\\{x}\\n\"" in iterator tests. |
| json/number_bench_test.mbt | Collapse float-number JSON element writes into a single <+ template. |
| immut/vector/vector_test.mbt | Convert vector iteration formatting to <+ templates. |
| immut/sorted_map/utils_test.mbt | Collapse key/value formatting sequences into <+ templates (including Repr cases). |
| immut/sorted_map/map_test.mbt | Replace interpolated write_string calls with <+ in iterator tests. |
| immut/priority_queue/priority_queue_test.mbt | Convert iteration formatting to <+ templates in immut priority queue tests. |
| deque/deque_test.mbt | Replace to_string() + newline writes with a single <+ template. |
| builtin/stringview_test.mbt | Use <+ for per-iteration formatted output in core StringView test. |
| builtin/linked_hash_map_wbtest.mbt | Convert debug-entry formatting to <+ template within whitebox tests. |
| builtin/iter_test.mbt | Use <+ templates for mapped iterator output with newlines. |
| builtin/array_test.mbt | Collapse rev_eachi write sequences into a single <+ template. |
| bigint/bigint_nonjs_wbtest.mbt | Switch Show output implementation in wbtest fixture to <+ template writing. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Contributor
Author
Collaborator
Coverage Report for CI Build 6200Coverage remained the same at 90.718%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
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.
Part 1 of 2, split out of #4110. Test and benchmark files only — no
library code is touched, and no expected output changes.
18 files, −85 / +52 lines.
What changes
buf.write_string("...\{x}...")→buf <+ "...\{x}...". The old form builta throwaway
StringBuilder+Stringfor the interpolation and then copiedthe result into
buf; the template writes the pieces straight through.Runs of writes collapse into one template:
arr.rev_eachi((i, x) => { buf.write_object(i) buf.write_string(": ") buf.write_object(x) buf.write_string("\n") }) // becomes arr.rev_eachi((i, x) => buf <+ "\{i}: \{x}\n")Adjacent literal chunks are concatenated by the desugarer at compile time,
so the literal part of a run becomes a single
write_string.Note on the hole form
These use the plain
\{x}hole, which desugars towrite_string_interpolation(x)and coercesxto&Show— one box per hole.That is the right trade in a three-element assertion, but not in library code,
which is why #4110 uses the inline-writer form
\{l => l.write_object(x)}there instead. The reasoning and benchmarks for that choice live in #4110.
Scope boundary
Strictly
*_test.mbt/*_wbtest.mbtfiles.builtin/fixedarray.mbtalso hasa
<+rewrite confined to an in-filetest {}block, but it is a library file,so it stays in #4110 rather than here.
Verification
moon check --deny-warn --target allmoon teston wasm / wasm-gc / js / native — 7458 / 7459 / 7403 / 7375passed, 0 failed
moon fmtclean