Repository navigation
fix: evaluate range pairs, and answer a condition the runtime cannot decide with unknown - #426
YevheniiKotyrlo wants to merge 2 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, a negative resolution 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 156, Firefox 157 and WebKit 27.2 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 |
(min-resolution: -1dpi) |
a 2x screen | match | no match |
not (max-resolution: -1dpi) |
a 2x screen | match | no match |
not (-1dppx < resolution < 1dppx) |
a 2x screen | 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 |
Viewport lengths and the features every screen answers alike, on main and here
A probe renders each query on a 600x400 screen, or in a 400x200 container on it, on main and on this head. Chromium 156, Firefox 157 and WebKit 27.2 answer every row but the last two as this PR does, Firefox answering (color-gamut) false. Firefox alone answers video-dynamic-range, as this PR does, and no engine video-color-gamut, which MQ5 §8 gives the values of color-gamut for a screen's video.
| query | where | main |
this PR |
|---|---|---|---|
(width > 50vw) |
600x400 screen | no match | match |
not (width: 100vw) |
600x400 screen | match | no match |
(50vw < width < 150vw) |
600x400 screen | no match | match |
@container (width > 50vw) |
400x200 container | no match | match |
(overflow-block: scroll) |
600x400 screen | no match | match |
(color-gamut: srgb) |
600x400 screen | no match | match |
(color-gamut) |
600x400 screen | no match | match |
(dynamic-range: standard) |
600x400 screen | no match | match |
(video-dynamic-range: standard) |
600x400 screen | no match | match |
(video-color-gamut: srgb) |
600x400 screen | no match | match |
Where a test departs from an engine
Each says so beside its rows: ios is this library's platform type, which the engines read as a type no device has; Firefox resolves env() in a container query and evaluates the known half of an or (Mozilla bug 1874687); Firefox answers the boolean context of color-gamut and dynamic-range false; lightningcss prints a calc(NaN) as 5.10424e38, which the compiler reads as infinity where the engines read 0; and an em bound, a container's em, a style() query, p3, rec2020 and high are unknown here, as the body's Known limits name.
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 (2421ea77) |
this PR (40bfda27) |
per-round difference |
|---|---|---|---|
(width >= 560px) |
101.2 (100.8–102.0) | 98.8 (98.7–101.7) | −2.0 (−2.6 to 0.0) |
(prefers-color-scheme: dark) |
8.0 (7.9–8.0) | 10.9 (10.8–10.9) | 3.0 (2.8 to 3.0) |
(width >= 560px) and (width < 800px) |
245.1 (244.2–250.7) | 212.4 (211.8–213.2) | −32.8 (−37.0 to −31.9) |
not ((width >= 9999px) or (orientation: landscape)) |
440.2 (403.6–471.1) | 370.8 (361.4–411.5) | −49.6 (−112.7 to −24.2) |
(400px < width < 800px) |
7.2 (6.0–8.2) | 119.6 (118.2–129.0) | 112.7 (110.5 to 122.5) |
(width > 50vw) |
10.8 (8.3–12.7) | 266.8 (231.4–286.7) | 256.2 (223.9 to 275.1) |
main answers a range pair and a viewport length false without resolving their bounds; here each costs its comparisons. The color scheme's 3 ns follows the size of the evaluator's switches: two case labels more or fewer move it by that much, whichever features they name. Windows 11 Pro 10.0.26300, AMD Ryzen 9 9950X3D, Node 26.12.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
1b6f6f5 to
a05beba
Compare
f560c39 to
460329a
Compare
…decide with unknown Media and container queries now evaluate through one walk and one comparison in three-valued logic (MQ5 §3.1): a term the runtime cannot decide is unknown, its negation too, and a rule applies only when its condition is true, as in Chromium, Firefox and WebKit. - A range pair is its two comparisons, where both runtimes answered it false. - A term is unknown when its feature is unmeasured, its value is outside the feature's syntax (a keyword or a type Media Queries 5 does not give it, env(), a negative resolution) or its operand is unresolved, which compiles to null so JSON carries it. A condition the compiler's unknown terms leave no way to be true compiles to no rule, as does a container condition CSS Conditional 5 §5.4 discards. - A number the compiler emits past the largest float compiles to the largest safe integer, since JSON writes Infinity as null. - A feature in a boolean context is false only for zero, none and no-preference. - A viewport length in a condition resolves against the window. - hover, any-hover, pointer and any-pointer answer for the primary input, as Safari and Chrome do on a phone; color, color-index, monochrome, grid, scripting, update, overflow-block, overflow-inline, scan, color-gamut, dynamic-range, their video- forms and -webkit-device-pixel-ratio answer as every React Native screen does. - A keyword and a media type match in any case.
460329a to
40bfda2
Compare
…integer A length rounds after its clamp, to 9007199254740990, as the (min-width: 1e40px) row pins.


Problem
On
main:(400px < width < 800px)and every other intervalfalse.false, sonot (fictional-feature: 3)and@container not (orientation: bogus)apply everywhere. MQ5 §3.1 makes the term and its negation unknown, and Chromium, Firefox and WebKit apply neither.not ((width: env(safe-area-inset-top)) or (width > 9999px))applies, where the engines apply it nowhere.not (fictional-feature > 10px)applies.(width),(hover),(prefers-color-scheme)(MQ5 §2.4.2).(hover: hover)and(hover: none)both match on every device;pointerand the features every screen answers alike are never measured, so(pointer: coarse),(overflow-block: scroll)and(color-gamut: srgb)never match.(prefers-color-scheme: DARK)and@media IOSnever match (css-values-4 §4.1, MQ4 §3).(min-resolution: -1dpi)applies on every screen, where css-values-4 §7.4 excludes it and the three engines leave the feature unknown.(width > 50vw)never matches andnot (width: 100vw)always does.Solution
evaluateCondition, and one comparison,compareMediaFeature, in three-valued logic (kleene.ts, MQ5 §3.1). A rule applies only when its condition istrue; one the compiler's unknown terms leave no way to be true compiles to no rule.env(), a negative resolution — or when the compiler cannot resolve its operand, which compiles tonullsoJSON.stringifycarries it. A number past the largest float compiles to the largest safe integer for the same reason.style()query compiles to an unknown term.noneandno-preferenceare false, and every other value is true.color8;color-index,monochromeandgrid0;scriptingenabled;updatefast;overflow-blockandoverflow-inlinescroll;scanmatching neither of its values; andcolor-gamut,dynamic-rangeand theirvideo-formssrgbandstandard, withp3,rec2020andhighunknown (MQ5 §6.4, §6.5, §8).-webkit-device-pixel-ratioanswers asresolution(Compatibility Standard). An<mq-boolean>compiles to its integer.hover,any-hover,pointerandany-pointeranswer for the primary input (MQ5 §7.1, §7.2), as Safari and Chrome do on a phone or tablet: iOS and Androidnoneandcoarse, a TVnoneto both, Mac Catalyst and other platformshoverandfine. Tailwind'shover:then applies where the primary input hovers.#451's
container-rejectiontest expectednot ((width > 100px) and style(--theme: dark))to apply at no width. At 50px the conjunction is false whateverstyle()answers, so its negation applies, as in all three engines, and I changed that expectation.Tests
compiler/condition-semantics.test.ts: what each condition the native tests render compiles to, a value of another type, a non-finite operand, and a condition lightningcss's types do not list.native/condition-semantics.test.tsx: each condition rendered against a screen or a container, the input features on each platform, and the unresolved and overflowing operands in memory and after a JSON round trip.compare.test.ts,kleene.test.tsandunits.test.tsxcover the shared helpers.vendor/tailwind/states.test.tsxandgroups.test.tsx:hover:andgroup-hover:on Mac Catalyst and on a touchscreen. These and bothcontainer-queries.test.tsxassert the colour a case expects absent, wheretoHaveStyle(undefined)matched any style.main, 223 of the 505 cases in the twocondition-semanticsfiles andcontainer-rejection.test.tsxfail.Verification
Every
ci.ymljob passes on this head merged intomain:yarn testat 1887 passed on Ubuntu with Node 22.18.0, and the iOS dev build on Xcode 27.1, where CI pins 26.6. Evaluating a condition costs within a few nanoseconds ofmain. The runs, the engines' answers and the device evidence are in this comment.Known limits
(min-width)as(width)(parcel-bundler/lightningcss#1366), drops thenotof an equality range, printingnot (width = 100vw)as(width = 100vw), true on every screen, and prints acalc(NaN)as an overflowing number.emin a media query is fix(compiler): read a media query's em and rem against the font size rem inlines #494's, a container'sinline-sizeandblock-sizefix(native): measure inline-size and block-size container queries #480's, a ratio fix: evaluate aspect-ratio media and container queries #425's, a heldnullcolor scheme fix(native): make colorScheme.set move every reader of the scheme #415's and reduced motion feat(native): evaluate prefers-reduced-motion so motion-reduce / motion-safe work on native #388's. Anemin a container query, a math function, astyle()query,prefers-contrast,prefers-reduced-transparency,inverted-colors,device-width,device-heightand the viewport lengths butvwandvhare each a change of its own.Platform.ios.js,Platform.android.js), so an attached mouse leaves(any-hover: hover)and(any-pointer: fine)off, and a stylus(any-pointer: fine), where Safari and Chrome turn them on.isMacCatalystis set at build time (RCTPlatform.mm), so an iPad app on a Mac answers as a touchscreen, and react-native-windows'isTVis always false (Platform.windows.js), so an Xbox answershoverandfine.PlatformnorAccessibilityInforeports a screen's gamut or dynamic range, forced colors or data saving, sop3,rec2020,high,forced-colorsandprefers-reduced-dataare unknown.Merge order
This shares lines with #388, #412, #415, #425, #459, #463, #472, #480 and #492; whichever lands second, I re-cut. #494 builds on this one, and
src/utilities/ascii-case.tsis #447's file byte for byte.#493's error recovery hands the media compiler feature values outside their syntax, which this answers as unknown.