refactor: separate C++23 module interface from implementation - #545
Merged
Conversation
…ism measured first 110 .cppm files carry both interface and implementation, so every body edit changes the BMI and every importer recompiles. Average transitive downstream is 10.7 modules; platform.cppm's is 63. Before planning the edit, a throwaway probe established that mcpp orders .cpp implementation units via P1689 dyndep and emits no BMI for them, and that the three shapes this codebase actually needs all hold: a non-exported helper declared in the interface and defined in the implementation unit is callable from an exported template instantiated downstream; an extern module-linkage global works the same way; export/default-args/constexpr have to stay put. Records the classification rules, the globals-ordering invariant, the 84.9%/15.1% free-function vs class-body split, the inline/LTO trade-off to measure rather than assume, and main's cold-build baseline (68.85/55.14/64.12s).
… sizing analysis behind it split.py transforms one .cppm that carries its own implementation into a standard (.cppm interface, .cpp implementation) pair. It scrubs comments and literals to a length-preserving mask, scans one brace level into items, and classifies each: types/templates/constexpr/inline/const stay whole in the interface; non-template function definitions and dynamically-initialised namespace-scope variables split into a declaration plus a definition; #if conditionals are emitted to both units so a definition never loses its guard. Doing it with one tool rather than 110 hand edits is what makes the diff reviewable -- every file is transformed by the same rules, and gcc verifies the result. Three facts established by probe before the rules were written: - an implementation unit implicitly sees everything its interface declares, including non-exported namespace aliases, type aliases and types, so nothing has to be duplicated - `export` must not appear in the implementation unit, and a default argument must appear only in the declaration - namespace-scope `static` has to lose the keyword: internal linkage cannot span two units, and module linkage already keeps the name module-private analyze.py sizes the job: 84.9% of the 46,253 lines sit outside class bodies.
…ules (phase 1)
Every .cppm carried its own function bodies, so an implementation edit changed
the BMI and recompiled every importer -- 10.7 modules on average, 64 TUs for
platform.cppm.
Phase 1 moves namespace-scope implementations into module implementation units
(`module M;` in a .cpp), which produce no BMI. 81 files, 779 entities, 26,606
body lines. The interface surface drops from 46,253 lines to 19,633 (42% of
what it was); that is the part every importer has to read.
Done by one tool (.agents/tools/module-split/split.py) rather than 81 hand
edits, so every file is transformed by the same rules and the diff is
reviewable. Five rules came out of compiler feedback rather than from reading
the standard first, and each names a real hazard:
- a deduced return type (`auto f() {...}` with no trailing `-> T`) cannot move:
callers in other units would have nothing to deduce from
- an out-of-line member definition cannot be split, because a member function
cannot be *declared* at namespace scope -- so the out-of-line `= default`
assignment operators in xvm/types and libxpkg/types/type stay put
- an anonymous namespace moves WHOLE: internal linkage cannot span two units,
and mirror/forms' constants proved it by vanishing from their callers
- `operator=` is a NAME, not an initialiser; reading the '=' as one turned
four defaulted operators into `extern ... ::operator;`
- namespace-scope `static` loses the keyword: internal linkage cannot span two
units either, and module linkage already keeps the name module-private
Verification: mcpp build clean, and mcpp test 38/38 binaries passed, 0 failed.
Not verified here, and CI has to carry it: 1,228 lines of the moved code sit
behind _WIN32/__APPLE__ guards that a Linux build never compiles.
…ions out of the interface (phase 2)
Two changes, both aimed at what is left in the BMI after phase 1.
**Class members.** A body defined inside a class is implicitly inline, so it
stayed in the interface and an edit to it still recompiled every importer.
286 members / 3,845 body lines move to out-of-line definitions in the
implementation unit. `config.cppm` is the case that pays: 93% class body and 34
direct importers.
Out-of-lining is not a text move, and each rule here is one gcc refused:
- the declarator-id becomes `C::name` and everything after it resolves in class
scope, but the RETURN TYPE precedes it and does not -- so a nested return type
gets qualified, and the definitions are emitted INSIDE their namespace rather
than fully qualified at file scope (`Segment` does not resolve there)
- `override`/`final` are declaration-only; a virt-specifier outside a class is
an error
- `: registry_ { registry }` -- a brace-initialised member-init list was being
read as the function body, producing `Ctor(args); {}`
- a class inside `#if defined(_WIN32)` must have its outlined members re-wrapped
in the same condition, or its Windows bodies compile on Linux
**Module-private declarations.** A non-exported helper's declaration only needs
to be in the interface if something still THERE names it -- an exported
template, an inline or constexpr body, a class member body. Otherwise it is
module-private and belongs in the implementation unit: in the interface it sits
in the BMI, so changing its signature would recompile every importer for
nothing. Deciding this needs care in two places gcc found:
- a qualified call `detail_::binding_error_(...)` IS a reference; a lookbehind
rejecting ':' missed the common form
- inside `export namespace`, an entity is exported even though its own
declaration does not say so
- when the declaration is dropped, the definition becomes the only declaration
and must carry the default arguments the declaration used to hold
Interface surface: 46,253 -> 14,819 lines, 32% of the original.
Two members keep their bodies because moving them makes gcc@16.1.0 segfault,
both recorded with the evidence in ICE_SKIP. The second was found by bisecting
config.cppm's 85 movable members (bisect-member.sh): #34 is
`static Config& instance_() { static Config inst; return inst; }`, whose
function-local static is where the module-attached `Config` is first completed.
Moving it crashes cc1plus in an unrelated translation unit, on a different type,
in a different module -- so the message names nothing that changed.
…d dedenting Two defects in the phase-2 tool, both found by reading its output rather than by a compiler. **Re-running stacked copies.** outline.py appends its section to the .cpp. That was fine while the generated files were untracked -- `git clean` removed them between runs -- and silently wrong once they were committed: clean leaves a tracked file, and split.py does not rewrite the ones outline.py creates itself. Three regenerations left three definitions of `TaskManager::TaskManager` in task.cpp and two of `UsageSkill::name`. It now truncates at its own banner before writing, and regen.sh removes generated files by diffing the tree against the base ref instead of trusting `git clean`. **Indentation.** A member body carries the class's indentation, so 3,845 lines of out-of-line definitions came out one level too deep with their closing brace hanging in mid-air, and `.strip()` had eaten the space before the opening brace. Bodies are now dedented by the member's own indent. Verified on the regenerated tree: no duplicate banners, and the only repeated declarator-ids are genuine overloads (`Config::display_path` on path and string, `Installer::Installer` on IndexManager and PackageCatalog). Build clean.
`test_cli_spec_parity.py`'s reverse pass reads the hand-written argv loops looking for flag literals the spec does not publish. It named `src/core/subos.cppm`, `src/core/xself.cppm` and `src/cli.cppm` -- and those loops now live in the implementation units, so the pass matched **0** literals instead of 64. The guard failed loudly rather than reporting green on an empty scan, which is the reason this was caught at all. It now names the modules by stem and reads both sides, since an interface unit and its implementation unit are one module and an argv loop can sit in either. Verified: 240 option spellings executed, 64 parser literals checked. Two other layout dependencies on `.cppm` were audited and still hold, because `VERSION` is `static constexpr` and stays in the interface: tools/linux_release.sh's version sed, and test_version_consistency.py. test_windows_header_hygiene.py already globbed both extensions and reports 10 modules including <windows.h> with NOMINMAX first -- the generated implementation units inherit the global module fragment intact.
Moving a module-private declaration out of the interface deleted the comment above it -- 230 comment lines across the tree, explaining helpers whose bodies are still there. Counting comment lines before and after is what found it; the compiler had nothing to say. The lead comment now follows whichever side keeps the declaration: the interface when it is still referenced there, the implementation unit when it is not. Phase 1 now conserves comments (10,290 -> 10,543). The surplus is the comment above an #if, which is emitted to both units along with the directive -- both units need to know why the guard is there, and an unexplained `#if defined(_WIN32)` in the implementation unit would be worse than a duplicated sentence. Also adds the report skeleton: method, fan-out, the two ICEs, and what this run does not verify (1,228 lines behind _WIN32/__APPLE__ guards, the musl toolchain, e2e). Measurements are appended once both sides have run.
Nothing about moving implementations may add or remove an export. This compares the exported-name set of every interface unit against the pre-split tree: 961 identifiers before, 961 after, 0 lost, 0 gained. The check has to be depth-aware, and the first version was not. A line-anchored regex over an `export namespace` body also matches the LOCAL VARIABLES inside the function bodies that live there, so it reported 914 "lost exports" whose names were `1`, `a`, `acc`, `activeBin` -- an artefact of the bodies moving, not a lost export. It now scans with the splitter's own item scanner and counts only top-level declarators.
…mments preserved
CI found what a local dev build cannot: `x86_64-linux-musl` (gcc@15.1.0-musl)
rejected `catalog.cpp`, where a `std::views::transform` pipeline moved into a
module implementation unit --
use of 'constexpr auto std::ranges::views::__adaptor::operator|(...)'
before deduction of 'auto'
-- while gcc@16.1.0 accepts the identical body. Reproduced locally with
`mcpp build --target x86_64-linux-musl`: exactly one error, one file.
The reverse has bitten this project before: `views::split | ranges::to` compiled
under 15.1.0-musl and made a whole module fail with "Bad file data" under 16.1.0,
blaming an unmodified TU (.agents/docs/2026-08-06-subos-architecture-proposal.md
§590). Range adaptors in modules are fragile in BOTH directions across these two
versions, so the two targets now share one compiler major rather than trading one
breakage for the other. `x86_64-linux-musl` + gcc 16.1.0 is already an installed,
supported combination, and aarch64-linux-musl was on 16.1.0 already.
Verified: `mcpp build --target x86_64-linux-musl` clean in 32.29s, and the
static binary runs (`xlings 2026.8.11.2`, ELF statically linked).
This needed no source exclusion, so the toolchain-skip hook added while
diagnosing it is removed rather than left behind empty.
Also regenerates the tree so dropped declarations keep their doc comments:
comment lines 10,316 -> 10,612. The surplus is the comment above an `#if`, which
goes to both units with the directive.
My own local loop was the gap, not CI: several rounds cleared only
`target/*/gcm.cache` and kept the object files, so a stale catalog.o hid this on
the dev toolchain too. A cold `rm -rf target` and the second target both have to
be built before believing a green build.
… too
macOS CI failed on the phase-2 push having passed on the phase-1 one. Every TU
instantiating a zero-argument `log::` template died inside libc++'s <print>:
formatter<basic_format_string<char, basic_string<char>>, char>
-- call to deleted constructor
which is `std::print` no longer seeing its `FILE*` overload and treating
`stdout` as the format string. Traced to log.cppm:66 via
`log::info<>` at migrate.cpp:19 and `log::println<>` at uninstall.cpp:134.
The build stops on first failure, so two objects is a floor, not the scope.
The only delta to log.cppm in phase 2 was the module-private declaration rule
dropping three declarations -- `gFile_`, `gColor_`, `color_on_()`. A template
left in the interface is instantiated in the IMPORTER, and what that
instantiation can see depends on the interface's reachability set, which
dropping declarations changes in ways the standard leaves to the implementation.
So the rule now skips any module whose interface still holds a template. Six
files, none of them a build-time bottleneck: interface 14,819 -> 14,832 lines.
Honest about the evidence: I could not reproduce this locally. A minimal probe
with the same shape -- GMF <cstdio>, a template calling stream-less std::print,
zero-arg instantiation from another module's implementation unit -- compiles
clean under clang 20.1.7 on Linux, and the error trace runs through Xcode's
_stdio.h, so it needs macOS libc. CI is the only loop, and CI is what verifies
this.
Locally verified: cold gcc@16.1.0 build clean (24.21s), musl gcc@16.1.0 clean
(24.22s).
…iation anchor The previous fix was wrong. Restoring log.cppm's dropped declarations changed nothing: macOS failed again on c94e199 with the identical error in the identical two objects, so that hypothesis is disproved and its guard is reverted rather than left in the tree unjustified. The real anchor is `Config::print_paths()`, whose body carries seven `std::println("...")` calls -- the overload that takes no stream. libc++ implements it as `print(stdout, fmt, args...)`, so the FILE* overload has to be visible; when it is not, `stdout` is deduced AS the format string and you get a deleted `formatter<basic_format_string<...>>`. An in-class body is implicitly inline, so it IS in the BMI, and it can be the only thing instantiating `std::print`/`std::println` for every importer of the module. Outlining it moved that instantiation point out of the interface, and clang 20 on macOS then stopped finding the FILE* overload in `migrate.cpp` and `uninstall.cpp` -- both of which import `xlings.core.config`, both otherwise untouched since the push macOS passed. This is the same shape as the two gcc ICEs already recorded: moving a body moves where a template is first instantiated, and the failure surfaces somewhere else entirely, naming nothing that changed. Phase 1 correctly does NOT carry this rule, and the asymmetry is the reason it passed macOS while phase 2 did not: a non-inline namespace-scope body need not be in the BMI at all, so it was never anchoring anything. Cost: 3 members stay inline, interface 14,832 -> 14,853 lines. Verified locally: cold gcc@16.1.0 clean (26.11s), musl gcc@16.1.0 clean, export surface still 961 identifiers, 0 lost 0 gained. macOS remains CI-only.
…units <cstdio> Root cause, found with a local clang loop instead of 13-minute CI cycles: the fragile construct is the STREAM-LESS `std::print` / `std::println` overload inside a template that stays in the interface. libc++ implements `print(fmt, args...)` as `print(stdout, fmt, args...)`, so the FILE* overload has to win overload resolution at the point of instantiation -- and for a template in an interface that point is in the IMPORTER. Once enough bodies leave the interface, clang 20 stops picking it: `stdout` is deduced AS the format string and the error is a deleted `formatter<basic_format_string<...>>`, reported against files that did not change (migrate.cpp, uninstall.cpp). Two mechanical rules replace the two guesses that came before, both of which are reverted: - 23 call sites across 6 interfaces name their stream. [print.fun] defines the two-argument form AS `print(stdout, ...)`, so this changes no behaviour and removes the deduction entirely. Bodies that move to an implementation unit are left alone -- nothing instantiates those from another TU. - an implementation unit whose text ends up naming `stdout`/`stderr`/`FILE` gets `<cstdio>` in its global module fragment. `Config::print_paths()` lands in config.cpp, whose interface has no fragment at all. With those, no member needs an exclusion for this: the print-anchor rule and the template-reachability guard are both gone, and the 3 members the anchor rule had pinned are outlined again (interface back to 14,819 lines). **The local loop is the real lesson.** clang 20.1.7 on Linux reproduces the macOS failure exactly -- main builds clean, the split does not -- so this was diagnosable in 40 seconds all along. A minimal probe of the same shape does NOT reproduce it; it needs the whole project. `build/bench/clang_variant.sh` builds any variant with clang in an isolated copy, and `clang_bisect.sh` binary-searches a file's members. One of my own bugs is worth recording: `open(f,'w').write(ensure_cstdio(open(f).read()))` truncates the file before the read runs, so every unit taking that path came out empty -- surfacing as undefined symbols at link time, never as an error in the file that was emptied. Verified: gcc@16.1.0 clean, musl gcc@16.1.0 clean, clang@20.1.7 clean, export surface 961 identifiers with 0 lost and 0 gained.
`classify()` took an `in_class` argument it never read, and its docstring listed five return values when there are six. Removed and spelled out; regenerating afterwards reproduces the committed tree byte for byte, which is the check that the cleanup changed no behaviour -- and incidentally that regen.sh is deterministic. The report carries method before numbers: how each measurement is taken and why (a plain `touch` of a .cppm does not work -- mcpp preserves a BMI's timestamp when a recompile is byte-identical, so it would skip every downstream unit and flatter the before-picture), the probe set spanning both fan-out and the two kinds of body, and `compare_segment` as a control that must NOT improve. It also corrects a number from the previous commit message: the stream naming is 11 sites across 4 interfaces, not 23 across 6. The 23 was the count of stream-less prints in the pre-split .cppm set, and 12 of those belong to bodies that moved to a .cpp and were never normalised. Section 8 records what each of the four toolchains rejected, including the two wrong fixes and why they are reverted rather than left in, and section 9 is the gate this work should have started with: a cold `rm -rf target`, the musl target, and clang -- all runnable locally in under a minute each.
…ool defects review found Self-review of the PR, and it found real things. **The split copied imports instead of moving them.** split.py duplicates the interface's import list into the implementation unit -- correct for the implementation, wrong for the interface, which was left importing modules only the moved bodies used. 153 of 554 interface import edges (28%) named nothing in the interface, and each one makes the interface's BMI depend on a module it does not use, so an edit there recompiles this interface for nothing. That caps the benefit the split exists to deliver. trim-imports.py is phase 3 and runs last, because deciding this needs every interface in its final form. Interface 14,819 -> 14,666 lines. It took one compiler-driven iteration. An aggregator module declares nothing and only `export import`s -- `xlings.runtime` re-exports event/event_stream/ capability -- so its export set read as empty and its importers were over-trimmed: `interface.cppm` lost `Event`, `EventStream` and `CancellationToken`. The export sets are now closed over re-exports, partitions included. **cold.sh has never run from where it was committed.** Its root was `dirname/../..`, which was right in build/bench and points at `.agents/` from `.agents/tools/module-split/`. Broken since the first commit of this branch, and the report cites it. **The report cited two scripts that are not in the repo.** `clang_variant.sh` and `measure_side.sh` lived in build/, which is gitignored, so the local gate the report documents could not be run. Both are now committed as `clang-variant.sh` / `measure-side.sh` with their paths corrected; logs still go to build/. The tools are 755 like the rest of .agents/tools. **The unverified surface was overstated by 69%.** "1,228 lines behind _WIN32/__APPLE__ guards" is really 728: the old counter flagged any block whose condition mentioned those macros, including `#if !defined(_WIN32)`, which is true on Linux -- 208 of the miscount was platform/unix.cpp's POSIX code. Recomputed by evaluating the conditions. **And a correction to the report's own reasoning.** It claimed the control probe "must show no improvement". It improves 2.5x, because the downstream interfaces it still rebuilds are smaller. The control separates rebuild-cost from rebuild-set; the moved probes get both and land at 4.6-12.1x. The gap is what the move bought. Verified after the trim: gcc@16.1.0 clean, musl gcc@16.1.0 clean, llvm@20.1.7 clean, export surface still 961 identifiers with 0 lost and 0 gained, and 1,262 function definitions conserved exactly. Unit suite re-running.
`main`'s baseline is in, so the report carries numbers instead of intentions. Cold build 56.40s -> 27.28s, a 2.07x median-to-median win, and the direction was not a given: translation units roughly doubled. One implementation edit goes 4.6-12.1x depending on fan-out, and on main such an edit cost about as much as building the whole project from scratch -- 54-63s against a 56.40s cold build, because before the split every body edit WAS an interface edit. **`mcpp test` is 1.16x SLOWER**, 953.81s -> 1104.83s, and that is the honest headline the rest of the report has to be read against. The mechanism is pinned, not guessed: +4.72s per test binary, 38 x 4.72 = the entire 179s gap, and the overhead is near-constant regardless of the test file's own size (min +3.10, median +3.74, max +4.37). A compile-side cost would scale with the file; a link-side cost would not. Each test binary now links ~202 objects instead of ~112. So the split trades link time for compile time. Link once and you win 2x; link 39 large static binaries and you lose 16%. The follow-up belongs to the test harness -- 38 binaries each statically linking the whole project -- not to the split. Also records what the self-review found (§12), all of which built green first: the copied imports, cold.sh never running from its committed path, two cited scripts that were not in the repo, the 69% overstatement of the unverified surface, the backwards control-probe claim, and a closure bug in the new tool. Plus the five invariants it checked and found clean, as numbers. Binary size +0.10% at -O0, which is the measured answer to the inline trade-off rather than an assumption about it.
…tion for it Re-measured on the final tree. `mcpp test` was the one number going the wrong way -- 1.16x slower than main -- and phase 3 turns it into 1.03x FASTER: 953.81s on main, 1104.83s before the trim, 921.73s after. Per test binary: main median 21.18s, branch 24.92s before the trim (+3.74s), and 21.07s after (-0.11s). The penalty did not shrink, it disappeared. **And that falsifies what I wrote about the cause.** I called it link cost: the overhead was near-constant per binary regardless of the test file's size, and each binary now links ~202 objects instead of ~112. That argument does not separate the two candidates, because BMI loading is ALSO near-constant per TU -- it depends on the import closure, not on the file being compiled. Phase 3 is the controlled experiment. It changes only interface import edges; the object count each binary links is identical before and after. Had linking been the cause the penalty would have survived. So the cause was every test TU loading BMIs for modules nothing in the interfaces named -- 28% more edges than needed. The split does not meaningfully trade link time for compile time; it was carrying a defect that looked exactly like that trade. Everything else improved with it too: cold build median 27.28s -> 26.09s (2.16x against main), `parse_sudo_env` 4.46/4.60 -> 4.39/4.35 (12.4x), and the control 17.24/17.54 -> 15.31/15.24, which is the same effect one level down -- the downstream interfaces it still rebuilds now carry smaller closures. All six CI workflows green on the trim commit.
Both version sites, since the release workflow reads only config.cppm while mcpp.toml drifts -- `tests/scripts/test_version_consistency.py` fails the PR when they disagree. `YYYY.M.D.N` with N starting at 1; `.0` is reserved for a milestone release. Verified against the binary the bump actually produces, not the one at the path I already had: a version change moves the mcpp fingerprint, so target/67501c87.../bin/xlings kept reporting 2026.8.11.2 while the new build sat at target/466b5e30.../bin/xlings. Contract scripts re-run against the new one -- cli parity 240 spellings / 64 literals, generated command reference, docs examples, windows header hygiene.
Follows the house shape: 一句话 first, then the premises this round overturned, recorded rather than quietly corrected. Three of them were mine: - "TU count doubles so the cold build gets slower" -- it got 2.16x FASTER - "the test-suite regression is link cost" -- falsified by phase 3, which changes only import edges and left the object count per binary identical - "the control probe must not improve" -- it improves 2.9%, and that is exactly what makes it useful: it separates rebuild-set from rebuild-cost Also the four toolchains and what each rejected, the invariants as numbers, and the one thing deliberately left alone (1,953 lines of `inline` bodies, which main's own tree carries identically).
The control probe reads 2.5x on the phase-1-2 tree (43.58 -> 17.24) and 2.9x on the final one (43.58 -> 15.24). Both were in the docs with neither labelled, so the two looked like a contradiction. The ratio that carries the argument -- the gap between the control and the moved probes -- is unchanged either way.
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.
What
Split every module that carried its own implementation into the standard pair:
X.cppm— module interface unit (export module M;): types, declarations, templates,constexpr. Produces the BMI.X.cpp— module implementation unit (module M;): the bodies. Produces no BMI.No directory moves, no behaviour changes, no new product code.
Why
Today every function body lives in the interface, so any body edit changes the BMI and everything that imports the module recompiles.
xlings.platformxlings.core.palettexlings.libs.jsonxlings.core.logxlings.core.configAverage transitive downstream: 10.7 modules.
Phase 1 (this checkpoint)
81 files, 779 entities, 26,606 body lines moved. The interface surface — the part every importer reads — drops from 46,253 → 19,633 lines (42%).
Transformed by one tool (
.agents/tools/module-split/split.py) so the rules are uniform and the diff is reviewable, with gcc as the verifier.Verification
mcpp buildcleanmcpp test— 38/38 binaries passed, 0 failedNot covered locally: 1,228 lines of the moved code sit behind
_WIN32/__APPLE__guards that a Linux build never compiles. The macOS and Windows CI jobs are the gate for those.Still to come on this branch
config.cppmis 85 members with 34 importers)main(cold + incremental) and the binary-size side of dropping implicitinlineTrade-off being measured, not assumed
Moving a body out of the interface drops its implicit
inline. Without LTO the release build loses those cross-TU inlining opportunities, so this branch reports binary size alongside build time rather than claiming the change is free.