Page MenuHomePhabricator

Bug 2009001 - Support chained references in attr when type() syntax is used. r=emilio
ClosedPublic

Authored by sukil on Feb 11 2026, 5:13 PM.
Referenced Files
Unknown Object (File)
Sat, Apr 11, 11:36 PM
Unknown Object (File)
Sat, Apr 11, 8:14 AM
Unknown Object (File)
Fri, Apr 10, 12:01 PM
Unknown Object (File)
Fri, Apr 10, 4:53 AM
Unknown Object (File)
Thu, Apr 9, 10:49 PM
Unknown Object (File)
Thu, Apr 9, 10:44 PM
Unknown Object (File)
Thu, Apr 9, 9:52 PM
Unknown Object (File)
Thu, Apr 9, 5:57 PM

Details

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
dshin added inline comments.
testing/web-platform/meta/css/css-values/attr-cycle.html.ini
2

Yes 2/3 failing tests occur because they expect substitution of the <attr-value> even if there's a cycle in the fallback (currently we catch the cycle in fallback which makes our attr invalid)

This is interesting, because Firefox and Safari are stricter in this regard compared to Chrome, even without attr. e.g.:

#foo {
  --z: 10px;
  --x: var(--y);
  --y: var(--z, var(--x));
  width: var(--y);
}

width will be 10px for Chrome, but not for Safari and Firefox. Glancing at the spec, I don't think it defines one way or the other?

EDIT: Ah, this spec discussion seems relevant: https://github.com/w3c/csswg-drafts/issues/11500
Seems like we resolved to not evaluate cycles in fallbacks if it's not taken. Probably requires references to be split into main references and fallback references for this to work...

2

(Here's the mapping) to the failing tests)

testing/web-platform/meta/css/css-values/attr-cycle.html.ini
2

I think the other failing test is related to traversing too deep (i.e. if they don’t use type() syntax we can stop traversing its references), which causes an incorrect substitution.

(FWIW, same should apply for raw-string - we should just be not populating references for these, I'd think)

servo/components/style/custom_properties.rs
1894

s/variable/attr/g

servo/components/style/properties/cascade.rs
262

So that concern I don't think should be an issue as long as we handle the"shortcut" cases like we do for variables here and here.

Namely - trivial attr() should be already populated, and attr() referencing others should get substituted when it needs to be, because references get processed First-In-Last-Out in the stack here.

servo/components/style/custom_properties.rs
1335

comment for linking purposes

servo/components/style/properties/cascade.rs
258

@emilio, after discussing with @sukil -

<style>
#foo {
  width: attr(bar type(*));
}
</style>
<div id=foo bar="attr(baz type(*))" baz="attr(bar type(*))"></div>

We have only one non-custom property that does participate in a cycle.
I would think we want something like builder.cascade_attr() that does weave into the current substitution/cycle detection infra for custom properties, though.
... But agree that we should have that hide behind might_have_non_custom_dependency (Renamed to might_have_non_custom_or_attr_dependency and extend the check to WithVariables + any_attr.

Addressed some comments with stuff in cascade.rs

servo/components/style/custom_properties.rs
503

Not a huge fan of what seems to be an implementation detail of CustomPropertiesMap leaking out. i.e. IndexMap being used is not known in this source file.
Perhaps better if we move this to custom_properties_map and name it just AllSubstitutionFunctions
We also should hide substitution_functions externally because if anyone decides to remove values, this will break, and have an insertion function that handles the index array insertion.

504

Isn't this just substitution_functions.size()?

1836

Probably is_attr || registration. /*...*/ is fine

testing/web-platform/meta/css/css-values/attr-cycle.html.ini
9

Out of curiosity (Since cycle detection isn't the main objective of this review, bug 1997338 is), what starts failing here?

sukil requested review of this revision.Feb 27 2026, 5:44 PM
sukil updated this revision to Diff 1213946.

Addressed comments :)

testing/web-platform/meta/css/css-values/attr-cycle.html.ini
9

I added a test to verify that same var and attr names have collision issues. Removing it, for now at least, want to think on that test some more.

This seems to make sense, though I need a bit more time to digest. Some style-related nits in the meantime.

servo/components/style/custom_properties.rs
478

Extra whitespace (Linting in general?)

1175

Maybe

let local_name = if cfg!(feature = "gecko") {/*...*/} else {/*...*/};
testing/web-platform/tests/css/css-values/attr-all-types.html
31 ↗(On Diff #1213946)

Any reason for this change?

sukil marked 2 inline comments as done.Mar 2 2026, 2:53 PM
sukil added inline comments.
testing/web-platform/tests/css/css-values/attr-all-types.html
31 ↗(On Diff #1213946)

Yes so we were passing the following test for the incorrect reasons because width: 3 isn't valid right?

test_valid_attr('width', 'attr(data-foo type(*))', 'attr(data-bar type(<number>), 11)', '3');

So width: attr(data-foo type(*)) became approximately width: 1264px and width: 3 became width: 1264px --- which allowed us to pass the test without correctly substituting.

A few more nits, but overall this makes sense.
Though, we should run mach try perf --rebuild 10 --show-all with 'shippable 'speedometer 'firefox 'windows | 'linux2404 | 'osx1500. As @emilio mentioned, we're touching the hot path, so want to make sure we want to avoid potential immediate post-landing fallout.

servo/components/style/custom_properties.rs
491

Probably should just DOM attributes.
Is the fact that it may be incomplete specific to attr()? Before CustomPropertiesBuilder::build() is called, var() refs may be incomplete as well, no?

1171

Given how it's used, it should probably be Result<(), ()>

1176

var() ref uses seen to skip lookup and insertion if the property was already referenced. Maybe worth something similar here.

1323

I think this is almost there, maybe should mention why we need specific handling for attr().
Something like: "attr() in non-custom properties may then reference var(), which we need to track to support chained references and detect cycles"

1611–1613

can we move this down around line 1579 where custom properties get assigned as well?

1980

(Aside, for bug 1997338)

It's likely that we will want to extend references_from_non_custom_properties to store names, as well as type (var or attr). Non-custom property references are only significant in the sense that it can loop back to properties in SingleNonCustomReference (Because attr() -> var() -> registered length property -> uses em/lh).

2173

Should add comment to the effect of "Short circuit - attr() is always treated as if unregistered"

testing/web-platform/tests/css/css-values/attr-all-types.html
31 ↗(On Diff #1213946)

Er... Yeah, you're right. That is a strange test.
I think that's an uncontroversial change upstream, but let's add the details of this (How it used to set an invalid value) into the commit message.

sukil marked 8 inline comments as done.EditedMar 2 2026, 10:25 PM

Addressed nits (few questions)

servo/components/style/custom_properties.rs
491

I've rewritten it to now (hopefully) emphasis that even post CustomPropertiesBuilder::build(), this map could remain incomplete (i.e. attrs using attr-unit are excluded).

1175

Had difficulties getting this to work with compiler type mismatch issues. Though mayhaps I was doing some silly stuff.

1176

We do have an seen.attr get used in update_attributes_map() to skip seen attrs. In traverse we also check if context.map.get_attr(&next.name).is_none() {...} before calling map_attribute_name_to_value(). Lmk if that sounds sufficient enough.

Thanks for your patience. We're pretty close.
BTW, fairly confident that this will conflict with @diegociudadreale's namespace changes, may want to rebase and deal with that right now.

servo/components/style/custom_properties.rs
491

Ah, uh, missed the comment in 1505. Any reason we shouldn't just substitute it right there? It just seems simpler, and this seems like a trip hazard from the caller perspective (It's supposed to contain _Computed_ substitution functions, after all).

492

Is there an issue making this OwnMap? Inherited stuff should not be an issue (Because you can't really declare inheriting attrs)

1175

Right.. That's fun. You'd think it should still work given one branch gets compiled out.. but oh well

1176

Ok, so maybe this function should either return the value Result<ComputedRegisteredValue, ()>, or should use like IndexMap::entry 1, and return if we did a lookup or not.

(Note substitution_functions simply being used to insert at the end, which generally tells me "it should just return the value and have the caller handle it")

sukil marked an inline comment as done.
sukil marked 6 inline comments as done.Mar 4 2026, 8:21 PM

Addressed comments and rebased. Regarding the results of the perfcompare, how do we generally interpret them?

servo/components/style/custom_properties.rs
492

Done :)

1176

Right that makes sense, updated now to let caller handle it.

Ok, we're at a good place, but some PerfCompare numbers warranted a bit more scrutiny around iter_declarations - I think we can do a bit better? Should try to run another round of speedometer changes with that.

servo/components/style/custom_properties.rs
537

Flatten, perhaps?

1362–1372

I wonder if you can move maybe_note_attr_dependency here - basically:

if refs.any_attr {
  self.update_attributes_map(attribute_tracker, &v.value.variable_value);
  if !refs.any_var {
    return;
  }
}

I think that's logically equivalent, and may quell some of the concerning signs of regression I see on PerfCompare :(

sukil marked 2 inline comments as done.
sukil marked 2 inline comments as done.Mar 5 2026, 9:28 PM
servo/components/style/custom_properties.rs
537

Flatten unfortunately seems not implemented for Option<&Option<T>>, I alternatively used and_then().

FWIW I think now that we dig into the variable value unconditionally, might_have_non_custom_or_attr_dependency makes a lot less sense, since we're basically doing the same checks maybe_note_non_custom_dependency would do...

My guess is that just the Arc<UnparsedValue> dereference there is enough to pollute the cache in that test-case... But it's just a guess really. @dshin have you tried to avoid it (just remove the any_attr check?)

servo/components/style/custom_properties.rs
1335

I don't think this is going to be it, but maybe change this to:

if let PropertyDeclaration::WithVariables(v) = decl {
    return matches!(id, LonghandId::LineHeight | LonghandId::FontSize) || v.value.variable_value.references.any_attr;
}

It should shave a branch in the common case?

servo/components/style/custom_properties_map.rs
225

Why can't this be IndexMap<Key, ComputedRegisteredValue, FxBuildHasher>? You only insert Some(..) right?

sukil added inline comments.
servo/components/style/custom_properties_map.rs
225

Right! We disallow removing elements so we can just directly store ComputedRegisteredValue.

servo/components/style/custom_properties_map.rs
243

The extra clone() here is a bit unfortunate btw, would be nice to avoid...

servo/components/style/custom_properties_map.rs
243

I was able to address this (TIL & yay for Equivalent trait!), but to no effect :(

servo/components/style/custom_properties_map.rs
243

It ends up looking like:

#[derive(Clone, Debug, Hash, Eq, PartialEq)]
struct KeyRef<'a>(&'a Name, SubstitutionFunctionKind);

impl<'a> Equivalent<Key> for KeyRef<'a> {
    fn equivalent(&self, key: &Key) -> bool {
        *self.0 == key.0 && self.1 == key.1
    }
}
//...
self.0.get(&KeyRef(name, kind))
servo/components/style/queries/condition.rs
412

Going from inherited customproperties to all substitution functions seem pretty incorrect here.
(Doesn't hit this case, though)

servo/components/style/queries/condition.rs
412

Going from inherited customproperties to all substitution functions seem pretty incorrect here.
(Doesn't hit this case, though)

I guess it doesn't really change behavior because at the point we're handling container queries the builder really only has the inherited properties. But would be good to be explicit and use inherited_custom_properties() + None

Rebased and applied comment suggestions

dshin added a project: testing-approved.

(Incorporated changes make sense)

This revision is now accepted and ready to land.Mon, Mar 23, 9:31 PM