Repository navigation
perf(view): reduce linkTo/URLFor overhead on JVM engines - #4192
Conversation
… on develop) Characterization spec that pins the exact output of a linkTo/URLFor corpus so the planned per-call cost reductions (de-dup $args, literal Replace in $urlForSubstituteVariables, lighter $tag) stay byte-identical. Two mechanisms: - End-to-end GOLDEN strings for engine-independent cases (deterministic URLs; $tag already sorts attributes alphabetically, so tag output is order-stable). These guard the de-dup-$args call-flow change and the $tag change, and assert the internal $argsResolved sentinel never leaks into output. - A verbatim REFERENCE COPY of $urlForSubstituteVariables with a sub-corpus that includes regex-backref values (\1, $1) in a pattern variable. That behaviour is engine-specific (the pattern has no capture groups), so the live function is asserted equal to the frozen reference on the running engine rather than a fixed golden. Guards the literal-Replace change. No production change yet; this is the gate the optimization commits must keep green. Draft for 4.3 (#4151). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
…d them (#4151 W1) linkTo runs $args(name="linkTo") then calls URLFor, which ran $args(name="URLFor") again on the same struct — a second generic normalisation pass (the $get( "showErrorInformation") check and the empty reserved/required/combine loops). On Adobe that pass measured ~200us. URLFor gains a declared boolean $argsResolved param (default false). linkTo sets it true before calling URLFor. When set, URLFor skips the generic $args pass but STILL applies its own app-level defaults via structAppendDefaults(functions.URLFor), so set(functionName="URLFor", …) overrides keep reaching linkTo URLs. Declaring $argsResolved as a real argument keeps the sentinel out of the generated query string. Byte-identical: the differential gate now also asserts URLFor($argsResolved=true) equals URLFor($argsResolved=false) (incl. with an app-level override) and that the sentinel never appears in output. before/after (Lucee 7): linkTo 56 -> 54 us/call; the larger win is on Adobe, where the duplicated $args pass is far costlier. Draft for 4.3 (#4151). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
…d of regex (#4151 W2) $urlForSubstituteVariables ran two regex operations per route variable — ReFind to test the `[prop]` / `[*prop]` placeholder presence and ReReplace to substitute the value. Route variable names are alphanumeric tokens with no regex metacharacters, so literal Find/Replace of the exact placeholder is equivalent and cheaper (the regex is the per-call hot spot on RustCFML and Adobe especially). Presence is now Find("[prop]") / Find("[*prop]"); substitution replaces the first matching literal placeholder with first-occurrence Replace (matching ReReplace's one scope). A value containing a backslash or `$` is kept on the ReReplace path, because those are regex-replacement metacharacters whose behaviour (no capture groups here) is engine-specific — the frozen reference-copy in the differential spec pins that per engine, and this fallback preserves it exactly. Byte-identical: the differential gate's $urlForSubstituteVariables reference-copy test (incl. \1 / $1 key values) stays green. before/after (Lucee 7): URLFor 26 -> 24, linkTo 54 -> 52 us/call; larger on RustCFML/Adobe where the per-variable regex is costlier. Draft for 4.3 (#4151). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
…per attribute (#4151 W3) $tag called $tagAttribute per attribute, and each $tagAttribute called $get("encodeHtmlAttributes"). $tag now resolves that flag once and passes it in; $tagAttribute gains an optional encodeHtmlAttributes argument and only falls back to $get when a caller omits it (not a signature default, so the $get is never evaluated at parse time). The attribute sort/ordering is unchanged. Byte-identical: the differential gate's $element goldens (5-attribute and skipStartingWith cases) stay green. before/after (Adobe 2023, where $get is costliest): $element 256 -> 235 us/call, linkTo 375 -> 354; flat on Lucee (its $get is ~free). Draft for 4.3 (#4151). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
|
rev1-r3 review @ 6052f79 (draft, per commit): CHANGES (one spec addition), otherwise approve I reviewed this commit by commit against the #4151 design notes.
Requested before un-drafting. W2's new branch, which picks
That's the only new control flow in the PR that the gate doesn't exercise. The numbers are honest: a JVM-only win, about 6% on Adobe |
gate The reference-copy probes for $urlForSubstituteVariables were all plain [prop], so W2's literal-Replace path for wildcard [*prop] placeholders and its "replace the earliest of [prop]/[*prop]" branch had no oracle (rev1-r3). Add probes: a wildcard [*path] with a slash value (encode on and off), both [key] and [*key] in each order, and a wildcard value carrying a backslash (the ReReplace fallback). Each asserts the live $urlForSubstituteVariables equals the frozen develop reference on the running engine. Gate stays green on the W1+W2+W3 branch. Draft for 4.3 (#4151). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
|
rev1-r3 re-review @ 9b23f8d: APPROVE (merge-ready once matrix The delta since
That closes my only gap. W1, W2 and W3 are as reviewed at |
Wheels Test Results216 871 tests 212 747 ✅ 1h 15m 53s ⏱️ Results for commit c3f688a. ♻️ This comment has been updated with latest results. |
…outes The full compat suite runs route-manipulating specs before this one, leaving the route table (and the application-scoped urlForCache) in a state the corpus did not expect — the matrix failed with "Could not find the `posts`/`editPost` route". Isolated runs passed because the default app routes were intact. Install a known route table in beforeEach ($clearRoutes + a mapKey wildcard and root, then $setNamedRoutePositions) and restore the real routes in afterEach, resetting the urlForCache around each test (mirrors linksSpec.cfc). Drop the url_route_root case: the named-route lookup is sensitive to route-cache state a redefined table doesn't fully rebuild here, and its pattern substitution is already covered by the $urlForSubstituteVariables reference-copy test. Verified: isolated 3/0/0, and the whole view suite 608/0/0 (so it is stable after linksSpec clears/redefines routes). Draft for 4.3 (#4151). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
|
rev1-r3 re-review @ 30e712f: CHANGES (the restore is incomplete) Installing a known route table is the right fix, but the save and restore only cover
That's exactly the "state left behind for later specs" class this commit is fixing, just moved downstream. Fix: mirror
#4183 compatibility (orch1's question). #4183 (open: index dynamic routes by method and first two path segments) adds another route-derived structure. When both are on develop, this spec must also save and restore #4183's index, or rely on its staleness guard to rebuild it when Dropping |
The differential spec's afterEach restored only application.wheels.routes, but beforeEach also mutates two sibling caches: $setNamedRoutePositions() *appends* into namedRoutePositions and mapper().end() rebuilds staticRoutes. Restoring only routes left those two dirty, so a later spec in the full suite inherited stale indices -- the same stale-index class this gate exists to catch. Snapshot and restore all three (routes + staticRoutes + namedRoutePositions) and clear all three in $clearRoutes(), mirroring linksSpec. Clearing namedRoutePositions before the append also makes URLFor(route="root") deterministic again, so url_route_root is restored to the golden corpus. Remove the url.capture debug branch. Verified on Lucee 7: view suite 608/0/0; full core suite 6516 pass with only pre-existing TestClient loopback-port artifacts (unrelated). Refs #4151 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
|
Addressed @rev1-r3's CHANGES @ 30e712f in 4bf5e64. The catch (correct): the differential gate's Fix (mirrors
Verified on Lucee 7:
Fresh compat-matrix dispatched: run 37120749549 on 4bf5e64. Note per @rev1-r3: #4183 adds another route index — whoever merges second must extend this snapshot/restore to cover it. Filing a follow-up 4.3 issue for a shared |
|
rev1-r3 re-review @ 4bf5e64: APPROVE (merge once matrix The delta since
Standing note for orch1: when #4183 lands, whichever of #4183 and #4192 merges second should add #4183's route index to this save, clear and restore, unless #4183's staleness guard rebuilds it from |
…dobe 2023)
A quoted named argument in a direct call -- `c.URLFor("$argsResolved" = x)` --
is a compile error on Adobe CF 2023 (`MissingNameException`: "each parameter
must have a name"), which zeroed the entire adobe2023 matrix leg across every
database. Adobe misattributes it to the enclosing describe() line, and Lucee
6/7, BoxLang and Adobe 2025 all accept the quoted name, so Lucee-only local
verification missed it (invariant #16 family).
Build the args in a struct and pass them through argumentCollection, which
sidesteps named-argument-name parsing entirely and is byte-identical in effect.
Verified locally, view suite 608/0/0 on all three: adobe2023, adobe2025, lucee7
(Adobe containers restarted after the edit to clear cached classes).
Refs #4151
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
adobe2023 compile fix — d857ffeThe compat-matrix on c.URLFor(controller = "posts", ..., "$argsResolved" = resolved)— is a compile error on Adobe CF 2023 ( Fix: build the args in a struct and pass via var probe = (resolved) => {
var pArgs = {controller = "posts", action = "index", params = "q=a b&x=y", encode = true};
pArgs["$argsResolved"] = resolved;
return c.URLFor(argumentCollection = pArgs);
};Verified locally (Adobe containers restarted post-edit to clear cached classes), view suite 608/0/0 on adobe2023, adobe2025, and lucee7. Fresh compat-matrix: run 37122451378 on d857ffe. |
|
rev1-r3 delta @ d857ffe: code OK. The verdict follows the per-leg read of matrix The fix replaces the quoted named argument |
|
rev1-r3 final verdict @ d857ffe: APPROVE (matrix read leg by leg, adobe2023 explicit) Matrix
The previously zeroed adobe2023 leg is healthy: full test counts on every database, and Code verdict unchanged from my reviews of W1, W2 and W3. The PR stays a draft for orch1's 4.3 timing. |
…t 16c rev1-r3 noted the only measured case for 16c was a single-expression arrow, while the entry claimed "any direct call" -- and Adobe traps can be closure-sensitive (16b-ext). Probed the quoted-named-arg call on adobe2023 (62023) in four isolated directory scopes: single-expression arrow -> compile FAIL block-bodied arrow -> compile FAIL regular function() closure -> compile FAIL plain statement in a method -> compile FAIL So the trap is context-independent, not closure-sensitive -- "any direct call" is correct. Controls confirm the boundaries: an unquoted named arg, a quoted $-key struct literal, and the argumentCollection workaround all compile on adobe2023. Reworded both entries to state the measured contexts and boundaries explicitly. Refs #4192 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
… 2023) (#4199) * docs(reference): document invariant 16c (quoted named-arg call, Adobe 2023) A named argument whose name is a quoted string literal in a direct call -- obj.method("name" = value) -- is a compile error on Adobe CF 2023 only (MissingNameException), while Lucee 6/7, BoxLang and Adobe CF 2025 all accept it. This is the inverse footprint of its siblings: 16a fails both Adobe engines, 16b/16b-ext are Adobe 2025 only, and this one is the lone 2023-only shape -- so an Adobe 2025 smoke or Lucee-only local run does not cover it. Add the shape, the verified-engine boundaries, the argumentCollection workaround, and the 62023 bisect probe to both CLAUDE.md (invariant 16c) and .ai/wheels/cross-engine-compatibility.md. Surfaced by linkToUrlForDifferentialSpec (#4192), which zeroed every adobe2023 leg until the quoted named arg was moved onto a struct key. Refs #4192 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(reference): record measured call-context boundaries for invariant 16c rev1-r3 noted the only measured case for 16c was a single-expression arrow, while the entry claimed "any direct call" -- and Adobe traps can be closure-sensitive (16b-ext). Probed the quoted-named-arg call on adobe2023 (62023) in four isolated directory scopes: single-expression arrow -> compile FAIL block-bodied arrow -> compile FAIL regular function() closure -> compile FAIL plain statement in a method -> compile FAIL So the trap is context-independent, not closure-sensitive -- "any direct call" is correct. Controls confirm the boundaries: an unquoted named arg, a quoted $-key struct literal, and the argumentCollection workaround all compile on adobe2023. Reworded both entries to state the measured contexts and boundaries explicitly. Refs #4192 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…r-cost-4151 Signed-off-by: Peter Amiri <peter@alurium.com>
…4151) Signed-off-by: Peter Amiri <peter@alurium.com>
|
rev1-r3 re-review @ c3f688a: code APPROVE (final sign-off after matrix run 37266376316) The delta since my final approval at
|
|
rev1-r3 review @ c3f688a: APPROVE (final; matrix run 37266376316 read leg by leg)
|
…iew specs) (#4202) * test(wheelstest): add shared $snapshotRoutes/$restoreRoutes/$clearRoutes helpers (#4194) Route specs that redefine the table must restore it in afterEach or they leak stale state into later specs. routes, staticRoutes, namedRoutePositions, the urlFor caches (application- AND request-scoped) and #4183's router index are separate structures, so a partial restore is the #4192 bug class (and formsSpec already restored only routes + namedRoutePositions, dropping staticRoutes). Add three helpers to wheels.WheelsTest: - $snapshotRoutes() -> deep-copy snapshot of every route-derived structure present (#4183 fields behind StructKeyExists, so no rebase needed); - $restoreRoutes(snapshot) -> exact restore, deleting managed keys that were absent at snapshot time; - $clearRoutes() -> empty the table + invalidate every route cache (shared, replacing the per-spec shims). PR1 of a staged migration: helpers + a round-trip self-test (SnapshotRoutesSpec) + migrate the view/* specs (links, urls, paginationHelpers, forms). dispatch/mapper/global/middleware/security/hardener follow in area PRs. Design: shared-docs state/dev1-design/evidence/4194-*. Updates the CLAUDE.md / consumer-ai "Test-specific gotchas" line (and the app template) that said $clearRoutes is not inherited -- it now is. Verified locally, view 605/0/0 + wheelstest 267/0/0 on lucee7, adobe2023 AND adobe2025. Refs #4194 Refs #4183 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * docs(test): move route-state helper idiom to consumer .ai/testing.md Keep docs/consumer-ai/CLAUDE.md under its 8192-byte budget: replace the inline $snapshotRoutes()/$restoreRoutes()/$clearRoutes() worked example with a one-line pointer and move the full idiom to .ai/testing.md. Re-sync the cli/lucli/templates/app copies so the consumer-docs check stays green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#4203) * test(wheelstest): add shared $snapshotRoutes/$restoreRoutes/$clearRoutes helpers (#4194) Route specs that redefine the table must restore it in afterEach or they leak stale state into later specs. routes, staticRoutes, namedRoutePositions, the urlFor caches (application- AND request-scoped) and #4183's router index are separate structures, so a partial restore is the #4192 bug class (and formsSpec already restored only routes + namedRoutePositions, dropping staticRoutes). Add three helpers to wheels.WheelsTest: - $snapshotRoutes() -> deep-copy snapshot of every route-derived structure present (#4183 fields behind StructKeyExists, so no rebase needed); - $restoreRoutes(snapshot) -> exact restore, deleting managed keys that were absent at snapshot time; - $clearRoutes() -> empty the table + invalidate every route cache (shared, replacing the per-spec shims). PR1 of a staged migration: helpers + a round-trip self-test (SnapshotRoutesSpec) + migrate the view/* specs (links, urls, paginationHelpers, forms). dispatch/mapper/global/middleware/security/hardener follow in area PRs. Design: shared-docs state/dev1-design/evidence/4194-*. Updates the CLAUDE.md / consumer-ai "Test-specific gotchas" line (and the app template) that said $clearRoutes is not inherited -- it now is. Verified locally, view 605/0/0 + wheelstest 267/0/0 on lucee7, adobe2023 AND adobe2025. Refs #4194 Refs #4183 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * test(mapper): migrate mapper-area specs to the shared route helpers (#4194 PR2) Replace each spec's hand-rolled route save/restore with $snapshotRoutes() / $restoreRoutes() and drop the local $clearRoutes shims (now inherited from wheels.WheelsTest, added in #4202). Several of these restored only a subset -- mapperSpec, mapperModernSpec, mapper/MapperSpec, mapper/MapperHardenerShouldSpec, mapper/MapperNullPatternSpec and mapper/MapperRobustnessSpec each dropped namedRoutePositions and/or staticRoutes -- so the shared helper also closes those latent leaks. PR2 of the staged migration; stacked on #4202 (the helper is not on develop yet). Depends on #4202; merge order #4202 then this. Verified locally: full core suite clean on lucee7 (6518 pass; the only failures are the known TestClient port-mapped loopback artifacts, 0 real), adobe2023 (6532/0/0) and adobe2025 (6532/0/0); 13 mapper bundles present. Refs #4194 Signed-off-by: Peter Amiri <peter@alurium.com> * docs(wheelstest): address review nits on route-state helper docs - WheelsTest.cfc docblock: drop the stale 'once it merges' wording now that #4183 has landed; dynamicRouteIndex / routeTableGeneration are present. - .ai/testing.md snippet: define 'var g = application.wo;' so the beforeEach example compiles as written. - .ai/testing.md prose: name the dynamic route index and route-table generation in the capture list (the helper already captures both). - Re-sync the cli/lucli/templates/app copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ute helpers (PR3) (#4204) * test(wheelstest): add shared $snapshotRoutes/$restoreRoutes/$clearRoutes helpers (#4194) Route specs that redefine the table must restore it in afterEach or they leak stale state into later specs. routes, staticRoutes, namedRoutePositions, the urlFor caches (application- AND request-scoped) and #4183's router index are separate structures, so a partial restore is the #4192 bug class (and formsSpec already restored only routes + namedRoutePositions, dropping staticRoutes). Add three helpers to wheels.WheelsTest: - $snapshotRoutes() -> deep-copy snapshot of every route-derived structure present (#4183 fields behind StructKeyExists, so no rebase needed); - $restoreRoutes(snapshot) -> exact restore, deleting managed keys that were absent at snapshot time; - $clearRoutes() -> empty the table + invalidate every route cache (shared, replacing the per-spec shims). PR1 of a staged migration: helpers + a round-trip self-test (SnapshotRoutesSpec) + migrate the view/* specs (links, urls, paginationHelpers, forms). dispatch/mapper/global/middleware/security/hardener follow in area PRs. Design: shared-docs state/dev1-design/evidence/4194-*. Updates the CLAUDE.md / consumer-ai "Test-specific gotchas" line (and the app template) that said $clearRoutes is not inherited -- it now is. Verified locally, view 605/0/0 + wheelstest 267/0/0 on lucee7, adobe2023 AND adobe2025. Refs #4194 Refs #4183 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> * test(mapper): migrate mapper-area specs to the shared route helpers (#4194 PR2) Replace each spec's hand-rolled route save/restore with $snapshotRoutes() / $restoreRoutes() and drop the local $clearRoutes shims (now inherited from wheels.WheelsTest, added in #4202). Several of these restored only a subset -- mapperSpec, mapperModernSpec, mapper/MapperSpec, mapper/MapperHardenerShouldSpec, mapper/MapperNullPatternSpec and mapper/MapperRobustnessSpec each dropped namedRoutePositions and/or staticRoutes -- so the shared helper also closes those latent leaks. PR2 of the staged migration; stacked on #4202 (the helper is not on develop yet). Depends on #4202; merge order #4202 then this. Verified locally: full core suite clean on lucee7 (6518 pass; the only failures are the known TestClient port-mapped loopback artifacts, 0 real), adobe2023 (6532/0/0) and adobe2025 (6532/0/0); 13 mapper bundles present. Refs #4194 Signed-off-by: Peter Amiri <peter@alurium.com> * test(dispatch,global,security): migrate leak-class specs to shared route helpers (#4194 PR3) Migrate the remaining partial-clear / leak-class route specs onto $snapshotRoutes() / $restoreRoutes() and drop their local $clearRoutes shims: - dispatch/findMatchingRouteMegaSpec (partial-clear shim) - global/urlforSpec (partial-clear shim; keeps its URLRewriting + obfuscateUrls saves, which are not route-table state) - security/PaginationXssSpec (never saved staticRoutes -- the formsSpec leak class; its $clearRoutes CALL stays, now inherited) PR3 of the staged migration; stacked on #4203 (helper chain not on develop yet). Depends on #4203 (and #4202); merge order #4202, #4203, then this. Verified locally, full core suite clean on lucee7 (6518 pass; only the known TestClient port-mapped loopback artifacts, 0 real), adobe2023 (6532/0/0) and adobe2025 (6532/0/0). Refs #4194 Signed-off-by: Peter Amiri <peter@alurium.com> * test(global): keep urlForCache present before the route-state snapshot Migrating urlforSpec onto the shared $snapshotRoutes/$restoreRoutes helpers brought urlForCache into the restored key set. urlForCache is a lazily-created persistent app-scope memo (routing.cfm), so when it was absent at snapshot time $restoreRoutes deleted it on the afterEach — cascading so every test ended with it gone. The issue-2955 'memo lives in application scope so it survives across requests' test then found the memo absent (Expected [false] to be true). Ensure urlForCache exists before the snapshot in the outer beforeEach, so the full-state restore preserves the key instead of deleting it — matching the framework's treatment of it as a persistent memo. All assertions are unchanged; the issue-2955 block still manages the cache contents via its own save/restore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
What
Three byte-identical micro-optimizations to the
linkTo()/URLFor()/$tag()path, which the .rcx report flagged as a large share of a list page's render cost. Each is a separate commit with its own before/after.Refs #4151 (does not close it — see "RustCFML" below).
The three wins
$args(W1) —linkToalready normalises its arguments, thenURLForre-ran a second generic$argspass.URLForgains a declared$argsResolvedparam; whenlinkTosets it,URLForskips the generic pass but still applies its ownset(functionName="URLFor", …)defaults. Declaring the flag as a real argument keeps the sentinel out of the query string.$urlForSubstituteVariablesran two regex ops per route variable (ReFind+ReReplaceon[prop]/[*prop]). Route tokens have no regex metacharacters, so it now uses literalFind/Replace(first-occurrence, matchingReReplace'sonescope). A value containing\or$stays on theReReplacepath (those are replacement metacharacters whose behaviour is engine-specific).$tagcalled$get("encodeHtmlAttributes")per attribute via$tagAttribute; it now resolves it once and passes it in ($tagAttributefalls back to$getfor any external caller). The attribute sort/ordering is unchanged.Before / after —
linkTo(controller/action/key), µs/call$elementon Adobe 2023: 256 → 235 (~8%).RustCFML (why this is "Refs", not "Closes")
These wins remove slow
$getand regex-compile overhead — costs that are high on the JVM. RustCFML's cost is in O(index) primitives (Mid/Find/string building/struct copies) that these changes don't touch, so itslinkTois unchanged at 78µs. A follow-up re-profiles RustCFML's 78µs at the primitive level to find #4151's real fix (or feed the RustCFML upstream batch).Gate
A differential/characterization spec (
linkToUrlForDifferentialSpec) pins the output byte-identical: golden strings for the engine-independent corpus (deterministic URLs;$tagsorts attributes), a reference-copy of$urlForSubstituteVariablesasserting the live function equals frozen develop per-engine (incl.\1/$1key values), andURLFor($argsResolved=true) == URLFor(false)incl. an app-level override + a no-$argsResolved-leak check. It passes on develop unchanged and stays green across all three commits.Full compat matrix + before/after on all 5 engines. (The
url.capturebranch in the spec is a temporary golden-capture aid; removed before merge.)🤖 Generated with Claude Code