Repository navigation
fix: evaluate range pairs, and answer a condition the runtime cannot decide with unknown - #426
YevheniiKotyrlo wants to merge 18 commits into
Conversation
|
Outdated: #426 is now re-written on top of #425, so the two no longer conflict. The original noteCross-referencing #425 and #426, which conflict — flagging it now rather than at merge time. They are independent siblings ( The hazard is in the
Taking #426's arm wholesale therefore un-fixes #425's range container queries. Taking #425's wholesale loses #426's three-valued handling, where The resolution that is correct in both directions evaluates the interval and returns One more line worth keeping whichever way it lands: #426's boolean-context arm uses A two-case test pins both halves, and neither branch passes it alone:
|
|
Outdated: #426 is now re-written on top of #425, so the two no longer conflict. The original noteCorrection to my note above — I undercounted the conflict set, and the omission was the load-bearing file.
A second resolution needs the same care, in Taking #426's arm onto #425's types does not compile — ["!", query.type === "condition" ? query.condition : (["?"] as MediaCondition)]And one more, in |
Device evidence — before / afterUNFIXED — The control and the pink bar paint, the two blue ones do not. A range pair and FIXED — The control and both blue bars paint, the pink one does not. A range pair is its two comparisons,
Android 16 emulator (API 36, GLES on the host GPU), 1140×2400 @ 480dpi, both frames from one run. Keywords in any case, on
|
| query | where | main |
this PR |
|---|---|---|---|
(orientation: LANDSCAPE) |
600x400 screen | no match | match |
not (orientation: LANDSCAPE) |
600x400 screen | match | no match |
(orientation: Landscape) |
400x600 screen | match | no match |
(prefers-color-scheme: DARK) |
dark scheme | no match | match |
@container (orientation: LANDSCAPE) |
400x200 container | no match | match |
@container not (orientation: LANDSCAPE) |
400x200 container | match | no match |
Unknown keywords and the input features, on main and here
A probe renders each query against the screen or container named, on main and on this head. Chromium 153, Firefox 155 and WebKit 26.6 answer every row but the TV ones as this PR does, the iOS and Android rows in their touch contexts and the macOS and Mac Catalyst rows on a desktop; MQ5 §7.1 and §7.2 give a touchscreen's pointer as coarse and its hover as none, and a TV's, whose remote is no pointing device, as none for both, which is what Chrome answers on a device with no touchscreen, mouse or stylus (TouchDevice.java); no engine here runs as a TV.
| query | where | main |
this PR |
|---|---|---|---|
(orientation: bogus) |
400x600 screen | match | no match |
not (orientation: bogus) |
600x400 screen | match | no match |
@container not (orientation: bogus) |
400x200 container | match | no match |
(hover: hover) |
iOS or Android | match | no match |
(hover: hover) |
iOS or Android, after a hover event | match | no match |
(hover: none) |
macOS | match | no match |
(hover) |
macOS | no match | match |
(pointer: coarse) |
iOS or Android | no match | match |
(any-pointer: coarse) |
iOS or Android | no match | match |
(pointer: fine) |
macOS | no match | match |
not (pointer: fine) |
macOS | match | no match |
(pointer: fine) |
Mac Catalyst | no match | match |
(hover: hover) |
a TV | match | no match |
(pointer: none) |
a TV | no match | match |
Evaluation cost — main against this PR
media-evaluation.test.ts evaluates each query a million times after 200,000 of warm-up, seven runs a side per round, each side in a jest process of its own at high priority on the same sixteen logical processors. One warm-up round, then six rounds, the side that goes first swapping each round. ns per evaluation, medians of the rounds with interquartile ranges:
| query | main (eda2b286) |
this PR (328c8da2) |
per-round difference |
|---|---|---|---|
(width >= 560px) |
104.8 (103.4–105.5) | 98.0 (97.3–101.5) | −6.1 (−6.7 to −3.8) |
(prefers-color-scheme: dark) |
8.0 (8.0–8.0) | 6.8 (6.8–6.8) | −1.2 (−1.3 to −1.2) |
(width >= 560px) and (width < 800px) |
263.8 (262.2–265.1) | 216.1 (210.1–232.1) | −49.4 (−52.6 to −45.7) |
not ((width >= 9999px) or (orientation: landscape)) |
503.1 (442.9–562.2) | 439.1 (406.3–449.6) | −66.4 (−146.8 to 0.1) |
Windows 11 Pro 10.0.26300, AMD Ryzen 9 9950X3D, Node 26.8.1, jest 29.7.0.
…vewind#426 nativewind#426 answers every media feature in a boolean context from its current value. This branch keeps the one arm its own feature needs.
3d2e1eb to
1b6f6f5
Compare
Both evaluators answered a range pair such as (400px < width < 800px) with false, though the compiler emits it faithfully. Media and container queries now share compareMediaFeature, so each operator has one meaning, and testMediaFeatureInterval answers a pair as its two comparisons.
…alse MQ5 §3.1 evaluates a media condition in three-valued logic: a term the runtime cannot decide is unknown, its negation is unknown, and a rule applies only when the whole condition is true. Both runtimes answered such a term false, so its negation matched: not (fictional-feature: 3) and not (pointer: fine) applied everywhere. A media feature value the compiler cannot resolve compiles to null, and both runtimes evaluate in Kleene logic, answering unknown for a null operand, a feature they do not measure, an operand they cannot order and a container style() query. A container condition holding a size feature the runtime does not know, or a value the compiler cannot resolve, selects no container (CSS Conditional 5 §5.4), so the compiler emits no rule for it. A feature in a boolean context is evaluated (MQ5 §2.4.2). The runtime answers the features whose value is the same on every React Native screen, (hover: none) no longer matches, a device with no color scheme preference matches light, and an <mq-boolean> value compiles to its integer.
1b6f6f5 to
a05beba
Compare
…ot one of its own lightningcss passes any identifier through as a feature's value, so `(orientation: bogus)` compiled to a comparison that is false, and its negation matched everywhere. A keyword the feature does not take now compiles to an unknown term in a media query, and to no rule in a container query, as an unresolvable value already does, so neither the term nor its negation matches, as in Chromium, Firefox and WebKit.
…paring it A keyword is ASCII case-insensitive (css-values-4 §4.1). The keyword check folded it while the compiled comparison kept it as written, so `(orientation: PORTRAIT)` never matched and `not (orientation: PORTRAIT)` matched in portrait. The value is folded where the compiler reads it, which both the check and the comparison then use.
…ver emits lightningcss compiles every `min-` and `max-` media feature to a range on `width` or `height`, so no compiled condition names them.
Its new lines hid a duplicated union and an unused import behind the file-wide `eslint-disable`, and the compiler's operator default wrote a `never` into its error message.
…h, and answer hover by input A container term the compiler cannot encode, such as (width > 10in), is unknown rather than dropping the whole query with its `or` siblings, and `=` with an operand the runtime cannot order is unknown, as the other operators are. A condition no device can make true, such as a style() query or an unknown keyword alone or negated, compiles to no rule. An unknown media feature named like an Object.prototype member compiles again. hover and any-hover answer none on iOS and Android until a pointer hovers an element, and hover from then on and on other platforms (MQ5 §7.2, §7.3), so Tailwind's hover: keeps working wherever a pointer hovers. A held color scheme is compared as held, so a null one is unknown. The two files that kept a file-wide lint disable lint clean without it.
…y, as the components read it The jest helper resolves to the package's source in a consumer's suite while the components it renders resolve to the build, so a reset through a relative import missed the signal they read.
…d test an unresolved bound
…nput, and share one walk
…§7 and phones' browsers do
…ebkit-device-pixel-ratio A media query's reader is a module constant that takes the getter, a container's takes one context object, and the walk folds and and or itself, so evaluating a term allocates nothing. -webkit-device-pixel-ratio reads PixelRatio.get() as resolution does. The nested conditions are readonly, the container tests assert the colour absent before layout, and the condition test's afterEach, which the Jest preset's reset made dead, goes.


Problem
On
main:(400px < width < 800px)as an interval, and both runtimes answer every intervalfalse.false, sonot (fictional-feature: 3)applies everywhere. A keyword a feature does not take fares no better: lightningcss passes any identifier through as a feature's value, the media runtime reads(orientation: bogus)as portrait, and the container runtime answers itfalse, so@container not (orientation: bogus)applies in every container. MQ5 §3.1 makes each term unknown and its negation unknown, and Chromium, Firefox and WebKit apply neither.orthe term disappears, sonot ((width: env(safe-area-inset-top)) or (width > 9999px))applies; underandthe whole block disappears. All three engines answer the opposite in both.not (fictional-feature > 10px)and(fictional-feature > 10px) or (width > 0px)apply, where §5.4 selects no container.(width),(hover),(color),(prefers-color-scheme)(MQ5 §2.4.2).(hover: hover)and(hover: none)both match on every device;pointerandany-pointerare never measured, sonot (pointer: fine)applies on every device;color,color-index,monochrome,grid,scripting,updateand-webkit-device-pixel-ratioare never measured; and(grid: 0)never matches, becausegrid's value compiles to a boolean.(prefers-color-scheme: DARK)never matches, nor does@media IOSon iOS, and the media runtime reads every orientation keyword butlandscapeas portrait, so(orientation: LANDSCAPE)and(orientation: Landscape)match in portrait and never in landscape. A keyword is ASCII case-insensitive (css-values-4 §4.1), as is every token of a media query (MQ4 §3), and all three engines match each keyword as its lowercase form.Solution
evaluateCondition, and compare through one function,compareMediaFeature, so each connective and each operator has one meaning, and a range pair is its two comparisons. The compiler parses a feature of either kind with one function,parseQueryFeature.kleene.ts. A term is unknown when the runtime does not measure its feature, when its feature holds anenv(), which no browser takes as a media feature's value, when the compiler could not otherwise resolve its operand (which now compiles tonull), or when the runtime cannot order it (anemorvwbound, acalc()). A condition lightningcss's types do not list, such as the<general-enclosed>a newer lightningcss's error recovery emits, compiles to an unknown term rather than failing the stylesheet. The compiler makes a term unknown when its keyword is not one Media Queries 4 or 5 gives the feature:orientation,hover,any-hover,pointer,any-pointer,prefers-color-scheme,prefers-reduced-motion,scriptingandupdate. A rule applies only when its condition istrue, and the compiler emits no rule for a condition that the terms it already knows are unknown, and the operands it could not resolve, leave no way to be true; a term only the runtime finds unknown, such as an unmeasured feature, still compiles.env()value compiles to no rule, as Chromium and WebKit drop it (§5.4), where Firefox resolves theenv(); any other value the compiler cannot resolve leaves its term unknown, so anorkeeps its other terms. Astyle()query compiles to an unknown term.noneandno-preferenceare false (MQ5 §2.4.2, §12), and every other value is true.coloris 8,color-index,monochromeandgridare 0,scriptingisenabledandupdateisfast. An<mq-boolean>compiles to its integer (MQ4 §5.3).-webkit-device-pixel-ratio, which-webkit-min-device-pixel-ratiocompiles to, reads the pixel ratioresolutionreads (Compatibility Standard).hover,any-hover,pointerandany-pointeranswer for the primary input (MQ5 §7.1, §7.2), as Safari and Chrome answer on a phone or tablet whether or not a mouse is attached: iOS and Android answernoneandcoarse; a TV, whose remote is no pointing device,noneto both, as Chrome answers a device with no touchscreen, mouse or stylus (TouchDevice.java); Mac Catalyst and every other platformhoverandfine. React Native reports no other input device, soany-hoverandany-pointeranswer for the primary input too. Tailwind'shover:, which guards:hoverwith(hover: hover), then applies where the primary input hovers, as in those browsers.min-width,max-width,min-heightandmax-heightarms go: lightningcss compiles each to a range onwidthorheight.#451's
container-rejectiontest expectednot ((width > 100px) and style(--theme: dark))to apply at no width. In a 50px container the conjunction is false whateverstyle()answers, so its negation is true, and all three engines apply it; I changed that expectation.Tests
src/__tests__/native/compare.test.ts: the shared comparison, range pairs, operands it cannot order, the boolean-context rule, and an empty or decided conjunction and disjunction, which evaluates no further terms.src/__tests__/native/kleene.test.ts: negation, conjunction and disjunction over the three values.src/__tests__/native/condition-semantics.test.tsx: each condition rendered against a screen or a 400x200 container, andhover,any-hover,pointerandany-pointeron each platform, on a TV and on Mac Catalyst, and on iOS after a hover event, and the iOS platform type written in capitals. The undecidable, boolean-context, device-feature, pixel-ratio and keyword cases render as in Chromium 153, Firefox 155 and WebKit 26.6, the iOS and Android input cases as in their touch contexts, the macOS and Mac Catalyst ones as on a desktop, the TV ones as MQ5 §7.1 and §7.2 give a device whose only input is a remote, and the §5.4 container cases as in Chromium and WebKit.src/__tests__/compiler/condition-semantics.test.ts: which blocks the compiler emits for an unresolved value, an unknown container feature, astyle()query, a condition no device can make true, and a condition or an operator lightningcss's types do not list.src/__tests__/vendor/tailwind/states.test.tsxandgroups.test.tsx: Tailwind'shover:andgroup-hover:apply on Mac Catalyst and not on a touchscreen.container-queries.test.tsxassert the colour a case expects absent, wheretoHaveStyle(undefined)matched any style.main, 107 of the 215 cases in the twocondition-semanticsfiles andcontainer-rejection.test.tsxfail.Verification
Every
ci.ymljob passes on this head merged intomainwith #477's harness fix, on Ubuntu with Node 22.18.0 withyarn testat 1598 passed, and the iOS dev build on macOS 27.0.1 with Xcode 27.0, where CI pins 26.6.Evaluating a media condition costs the same as on
mainor less:(prefers-color-scheme: dark)6.8 ns against 8.0,(width >= 560px)98 against 105. The runs and the device evidence, before and after, are in this comment.Known limits
(min-width)as(width). MQ4 §2.4.4 makes(min-width)a syntax error, but here it now matches as(width)does.emorvwbound and acalc()are unknown; fix(compiler): read a media query's em and rem against the font size rem inlines #494 resolves anemand aremin a media query. Astyle()query is unknown too, since the runtime does not read the custom property it names.inline-sizeandblock-sizeare unknown until fix(native): measure inline-size and block-size container queries #480 measures them, and a ratio is until fix: evaluate aspect-ratio media and container queries #425 compiles it.null, whichcolorScheme.get()reads as the system's, is unknown; fix(native): make colorScheme.set move every reader of the scheme #415 resolves both through one function.(any-hover: hover)and(any-pointer: fine)off, and a stylus(any-pointer: fine), where Safari and Chrome turn them on; Chrome on Android also answers(any-pointer: fine)on a device that takes a stylus with none attached (TouchDevice.java). An Android device in desktop mode, such as a Chromebook, answers as a touchscreen, where Chrome answershoverandfine.Platformtells neither apart.not (prefers-reduced-motion: reduce)applies nowhere, wheremainapplies it everywhere; feat(native): evaluate prefers-reduced-motion so motion-reduce / motion-safe work on native #388 measures reduced motion.Merge order
Range pairs moved here from #425, so this no longer builds on it. It shares lines with #388 (
native/conditions/media-query.ts, where it addsprefers-reduced-motionand drops the evaluator's file-wideeslint-disabletoo), #412 (compiler/compiler.ts), #415 and #425 (native/conditions/media-query.ts), #459 (compiler/compiler.types.ts,native/conditions/media-query.ts), #463 (native/conditions/container-query.tsandguards.ts, whosetestContainerQuerysignature the container tests call), #472 (vendor/tailwind/states.test.tsx) and #480 (native/conditions/container-query.ts); whichever lands second, I re-cut. #494 builds on this one.src/utilities/ascii-case.tsis the file #447 adds, byte for byte, so the two merge in either order.