Repository navigation
fix(compiler): scope single-definition variable inlining to the declaring block - #413
Open
YevheniiKotyrlo wants to merge 5 commits into
Open
YevheniiKotyrlo wants to merge 5 commits into
YevheniiKotyrlo wants to merge 5 commits into
Conversation
This was referenced Aug 14, 2026
YevheniiKotyrlo
marked this pull request as ready for review
August 15, 2026 19:13
Contributor
Author
…ring block Re-cut onto main (a5002c5). Main renders an animated component's style as an array, so the parity check flattens it. Main rejects a selector naming :root and a class before the inliner sees it, so the :root .theme case waits on that check.
The inliner counted declarations with a visitor that cannot see the rule around them, so a declaration inside an @supports block the compiler later drops still counted, and was the value that folded. The inliner now takes its census from its own walk of the rules the compiler keeps: - an @supports block is decided with supportsConditionValid, as the compiler decides it, so a dropped block declares nothing - `*` declares on every element, so `inherits: false` does not narrow it - a registered `*` property that nothing declares folds to its initial value - a declaration is removed once nothing in the stylesheet still reads it, so a reference the fold cannot reach keeps its declaration A folded value reaches the compile-time parsers, so a transparent shadow is dropped there too, as the runtime already drops one it reads from a variable.
YevheniiKotyrlo
force-pushed
the
fix/single-definition-inliner-scope
branch
from
October 11, 2026 12:35
b77fafb to
dcef09c
Compare
This was referenced Oct 11, 2026
This branch has not been deployed
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.


Problem
inlineVariablesfolds a custom property declared once into everyvar()that reads it, then deletes the declaration. It counts declarations with a visitor that cannot see the rule around them, so the fold ignores where the declaration applies. Onmain:.parent { --x: 10px } .child { width: var(--x) }compiles.childtowidth: 10, so an element carrying only.childgets a width. Chromium, Firefox and WebKit give it none (css-variables-1 §2). Whether.childfolds also depends on unrelated rules: a second--xanywhere in the sheet turns the fold off.@supportsblock still declares. Tailwind registers its utility variables with@propertyand repeats each initial value in an@supportsfallback for browsers without@property. The compiler drops that block (supportsConditionValid), but the visitor counts the declaration inside it, and that declaration is the value that folds. With@property --x { syntax: "*"; inherits: false; initial-value: 10px }, a fallback* { --x: 20px }and.b { width: var(--x) },mainfolds20. The same sheet renders10withinlineVariables: false, and in Chromium and Firefox.inherits: falseis ignored.:root { --x: 20px }folds20into.balthough the registration says other elements hold the initial value..a { --c: 0.5; color: rgb(255 0 0 / var(--c)) }loses--c, because the fold does not reach inside an unresolved colour, and the alpha comes out empty.Folding also decides which parser a value meets, and three parsers disagree with the runtime:
0renders nothing unfolded, becauseparseUnparsedread0as an absence;<ratio>keeps the text16 / 9where the property parser gives16/9;Solution
The inliner takes its census from its own walk over the rules the compiler keeps. It folds a value into a
var()only when every element the reference's rule matches holds that value:*, or:root,:hostandhtmlfor a property that inherits, outside@mediaand@container.An
@supportsblock is decided withsupportsConditionValid, asextractRuledecides it, so a kept block is unconditional and a dropped one declares nothing. A registered*-syntax property that nothing declares folds to its initial value (css-properties-values-api-1 §3.3). A declaration is removed once nothing left in the stylesheet reads it, so one another rule reads at runtime stays.inlineVariables.excludestill keeps a variable a separately compiled stylesheet orvars()needs.The three parsers now agree with the runtime:
0andfalseare values, a<ratio>gets the descriptorparseAspectRatiogives it, and both paths drop a transparent shadow through oneisTransparentColor.Existing expectations change in three places:
transparent shadow is preservedpinned the compile-time path keeping it. Both paths drop it now.scale-0andscale-50fold now, so they get the compiler'sscaleXandscaleYinstead of the runtime'sscale. Both render the same scale.variables.test.tsxread"green"and"red"where they read"#008000"and"#f00". The colour now reaches the element by runtime inheritance instead of a fold into a rule that does not declare it, and the runtime keeps a named colour as written. React Native reads both.Tests
src/__tests__/compiler/inline-variables.test.ts: where a value folds and where it must not, by selector, conditional rule,@supports, registration and nesting, and when a declaration is removed or kept.src/__tests__/native/variable-inlining.test.tsx: what the runtime paints for the same stylesheets.src/__tests__/native/variable-fold-parity.test.tsx: every value kind lightningcss can put in a custom property, rendered with the fold on and off. Each divergence the fold does not cause is pinned with its reason.src/__tests__/native/box-shadow.test.tsx: transparent shadows dropped on both paths, and a transparent list still replacing an earlier one.On
main, 44 of the 168 cases in the six files fail; all pass here. Sixteen mutations, one per rule, each fail their own cases.The scope cases agree with Chromium 153, Firefox 155 and WebKit 26.6:
*withinherits: falsereads20px.:rootwithinherits: falsereads the initial10px.10px.In the Tailwind-shaped fallback, Chromium and Firefox drop the block as the compiler does. Playwright's WebKit keeps it.
Verification
On Windows with Node 26:
yarn lintcleanyarn typecheckcleanyarn test --maxWorkers=2 --coverage: 1421 passed, 4 failed. The 4 are the babel cases that also fail onmainon this machine.yarn buildcleanyarn example expo export --platform webexportedNo existing issue tracks this. I searched for
inlineVariables,variable inlining,custom property scope,@property initial-valueand@supports.Device evidence, before and after: #413 (comment)
Known limits
.bunder:root { --x: 20px }withinherits: falsestill resolves20. The compiler now leaves that reference to the runtime instead of folding it, which is the prerequisite.vars()write cannot reach a reference folded in its own block.excluderestores it.<length>or<color>, is left to the runtime, which reads the same value from the registration.var()left in the sheet makescompile()throw (compile()fails with "failed to deserialize; expected an object-like struct named Specifier, found ()" on lightningcss >= 1.31 (any unresolvedvar()); the 1.30.2 guard doesn't cover it #470). This leaves the cross-block referencesmainfolded, so more stylesheets meet that untilcompile()fails with "failed to deserialize; expected an object-like struct named Specifier, found ()" on lightningcss >= 1.31 (any unresolvedvar()); the 1.30.2 guard doesn't cover it #470 is fixed.Merge order
This overlaps #389, #391, #411 and #420; I'll re-cut whichever lands after the others.
It also shares lines with #412 (
compiler/compiler.ts,compiler/inline-variables.ts), #417 (compiler/declarations.ts,utilities/index.ts), #443 (__tests__/compiler/inline-variables.test.ts); whichever lands second rebases.Base
Re-written on
main(a5002c5).