Repository navigation
fix: evaluate range pairs, and answer a condition the runtime cannot decide with unknown - #426
YevheniiKotyrlo wants to merge 11 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 before a pointer hovers as this PR does, the iOS and Android rows in their touch contexts and the macOS rows on a desktop; MQ5 §7.1 and §7.2 give a touchscreen's pointer as coarse and its hover as none.
| 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, before a pointer hovers | match | no match |
(hover: hover) |
iOS or Android, once a pointer hovers | match | 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 |
…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


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,scriptingandupdateare never measured; and(grid: 0)never matches, becausegrid's value compiles to a boolean.(orientation: LANDSCAPE)and(prefers-color-scheme: DARK)never match, and(orientation: Landscape)matches in portrait. A keyword is ASCII case-insensitive (css-values-4 §4.1), and all three engines match each as its lowercase form.Solution
compareMediaFeature, so each operator has one meaning, and a range pair is its two comparisons.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 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()). 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 its unknown terms and unresolved operands leave no way to be true.env()value, compiles to no rule, as Chromium and WebKit drop it (§5.4); 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).hover,any-hover,pointerandany-pointeranswer by the input the device has shown (MQ5 §7.1, §7.2, §7.3): a touchscreen cannot hover and points coarsely, so iOS and Android answernoneandcoarseuntil a pointer hovers an element with a:hoverstyle, andhoverandfinefrom then on, whereany-pointeralso keeps the touchscreen'scoarse; every other platform answershoverandfine. Tailwind'shover:, which guards:hoverwith(hover: hover), keeps working wherever a pointer hovers.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, and the boolean-context rule.src/__tests__/native/kleene.test.ts: negation, conjunction and disjunction over the three values, and short-circuiting.src/__tests__/native/condition-semantics.test.tsx: each condition rendered against a screen or a 400x200 container, andhover,pointerandany-pointeron each platform before and after a pointer hovers an element. The undecidable, boolean-context, device-feature 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 and the others as on a desktop, 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 and a condition no device can make true.main, 93 of the 178 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 1556 passed, and the iOS dev build on macOS 27.0.1 with Xcode 27.0, where CI pins 26.6.Device evidence, before and after: #426 (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.(hover: hover)and(pointer: fine)wait for a hover event, so a block either guards stays off until a pointer hovers an element with a:hoverstyle.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 #425 (both
media-query.tsfiles), #388 (bothmedia-query.tsfiles and the jest helper, where it folds a keyword the same way and drops the evaluator's file-wideeslint-disabletoo), #415 (native/conditions/media-query.ts), #459 (compiler/compiler.types.ts,native/conditions/media-query.ts), #480 (native/conditions/container-query.ts), and #482 and #484 (native/react/interaction.ts); whichever lands second, I re-cut. #494 builds on this one.