Bug 1273706 - Part 23: Update CSSVariableResolver for custom properties. r?heycam draft
authorJonathan Chan <jyc@eqv.io>
Thu, 18 Aug 2016 15:30:38 -0700
changeset 402912 0f5db4ea19451ed623a8cf4b997dd5ad9269a32f
parent 402911 e782649cef6c9959c6d3f95a0b5a72a764f7480d
child 402913 7004b9c2decdac3c331351f6c764f63bcf17182d
push id26775
push userjchan@mozilla.com
push dateThu, 18 Aug 2016 22:38:41 +0000
reviewersheycam
bugs1273706, 10144
milestone51.0a1
Bug 1273706 - Part 23: Update CSSVariableResolver for custom properties. r?heycam We also add a new |mHasUninherited| field to the |nsStyleVariables| style struct. Some custom properties are uninherited, but we assume that most will be inherited (all unregistered custom properties). Thus we keep the Variables style struct as an inherited style struct but we set a flag when we set an uninherited variable. This is true iff the variables style struct contains values for uninherited variables, and is set by CSSVariableResolver (CSSVariableResolver.cpp:173). We know which variables are uninherited because we pass around a CSSVariableRegistrations, and the CSSVariableRegistration for a particular variable contains mInherited. Here are the cases in WalkRuleTree: (1) We specify no information (CheckVariablesCallback returns eRuleNone) but a parent specifies all the information (nsRuleNode.cpp:2422). This is impossible, because we only set startStruct (nsRuleNode.cpp:2346) if "We found a rule with fully specified data," and this only happens for inherited style structs when the struct is CacheableWithoutDependencies (COMPUTE_END_INHERITED, nsRuleNode::SetUsedDirectly, nsRuleNode::PropogateDependentBit). This doesn't happen for variables, because as before, we always run conditions.SetUncacheable() in ComputeVariablesData (nsRuleNode.cpp:10144). All in all the variables style struct won't be cached on the rule node. (Like all inherited style structs, it will be cached on the style context, nsStyleContext.cpp:460). (2) No variables were specified (CheckVariablesCallback returns eRuleNone if no variables were specified and eRulePartialMixed if any were specified, as before) (nsRuleNode.cpp:2449). startStruct will be null as in (1). This could be problematic if we had uninherited variables, because we made the variables style struct inherited! If we didn't do anything, we would end up just sharing with the parent's variables style struct at nsRuleNode:2481 in the original code. This patch intercepts this and checks if the parent variables style struct we were going to has uninherited variables (nsRuleNode.cpp:2483). If it does, we don't share, and proceed the next case: (3) Supposing we're not the root, we end up computing the variables style struct using ComputeVariablesData (nsRuleNode.cpp:2489). Now CSSVariableResolver will do its work. If we don't specify anything for a variable, we check whether it's inherited or uninherited and act accordingly (CSSVariableResolver.cpp:156). Another thing to consider is variables being uninherited being handled properly on restyles. For example, registering a custom property in window.onload, after everything has been styled. My understanding is that the restyle manager recomputes the style structs and then sees if there is a difference to determine whether or not to restyle, and that this process happens recursively. In this case, the problem was that nsStyleContext::CalcStyleDifference compared mVariables in nsStyleVariables but not mHasUninherited. So even if one of a parent style struct's variables became uninherited, CalcStyleDifference would return that they were the same, and as a result not restyle child style contexts. So nsStyleContext.cpp:960 now compares mHasUninherited as well. MozReview-Commit-ID: LGlJ6qBRcga
layout/style/CSSVariableDeclarations.cpp
layout/style/CSSVariableResolver.cpp
layout/style/CSSVariableResolver.h
layout/style/CSSVariableValues.cpp
layout/style/nsCSSParser.cpp
layout/style/nsRuleNode.cpp
layout/style/nsStyleContext.cpp
layout/style/nsStyleStruct.cpp
layout/style/nsStyleStruct.h
--- a/layout/style/CSSVariableDeclarations.cpp
+++ b/layout/style/CSSVariableDeclarations.cpp
@@ -301,38 +301,24 @@ CSSVariableDeclarations::AddVariablesToR
     CSSVAR_PRINTF("Considering adding variable %s.\n", NS_ConvertUTF16toUTF8(name).get());
     size_t varID = iter.UserData();
     size_t declID = mUsedDeclarations[varID];
     if (declID == (size_t) -1) {
       CSSVAR_PRINTF("Not adding because no usable declaration.\n");
       continue;
     }
     const Declaration& decl = mDeclarations[declID];
-    switch (decl.mType) {
-    case Type::Initial:
-      // Values of 'initial' are treated the same as an invalid value in the
-      // variable resolver.
-      aResolver->Put(name, EmptyString(),
-                     eCSSTokenSerialization_Nothing,
-                     eCSSTokenSerialization_Nothing,
-                     false);
-      break;
-    case Type::Inherit:
-      break;
-    case Type::Unset:
-      break;
-    case Type::TokenStream:
-      // At this point, we don't know what token types are at the start and end
-      // of the specified variable value.  These will be determined later during
-      // the resolving process.
-      aResolver->Put(name, decl.mExpr,
-                     eCSSTokenSerialization_Nothing,
-                     eCSSTokenSerialization_Nothing,
-                     false);
-      break;
+    if (decl.mParsed) {
+      CSSVAR_PRINTF("Adding as parsed!\n");
+      aResolver->PutSpecifiedParsed(name, decl.mExpr, decl.mValue,
+                                    decl.mValueType, decl.mContext,
+                                    decl.mFirstToken, decl.mLastToken);
+    } else {
+      CSSVAR_PRINTF("Adding as unparsed.\n");
+      aResolver->PutSpecifiedUnparsed(name, decl.mType, decl.mExpr, decl.mContext);
     }
   }
 }
 
 CSSVariableDeclarations::Iterator
 CSSVariableDeclarations::Iter() const
 {
   return mVariableIDs.ConstIter();
--- a/layout/style/CSSVariableResolver.cpp
+++ b/layout/style/CSSVariableResolver.cpp
@@ -2,269 +2,350 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* object that resolves CSS variables using specified and inherited variable
  * values
  */
 
-#include "CSSVariableResolver.h"
+#include "mozilla/CSSVariableResolver.h"
 
-#include "CSSVariableDeclarations.h"
-#include "CSSVariableValues.h"
+#include <algorithm>
+
+#include "mozilla/CSSVariableValues.h"
 #include "mozilla/PodOperations.h"
 #include "mozilla/UniquePtr.h"
-#include <algorithm>
 
 namespace mozilla {
 
-/**
- * Data used by the EnumerateVariableReferences callback.  Reset must be called
- * on it before it is used.
- */
-class EnumerateVariableReferencesData
+// Returns true if there was a cycle.
+bool
+CSSVariableResolver::HandleVisitNode(size_t aID, VisitNodeResult aResult)
 {
-public:
-  explicit EnumerateVariableReferencesData(CSSVariableResolver& aResolver)
-    : mResolver(aResolver)
-    , mReferences(MakeUnique<bool[]>(aResolver.mVariables.Length()))
-  {
-  }
-
-  /**
-   * Resets the data so that it can be passed to another call of
-   * EnumerateVariableReferences for a different variable.
-   */
-  void Reset()
-  {
-    PodZero(mReferences.get(), mResolver.mVariables.Length());
-    mReferencesNonExistentVariable = false;
-  }
-
-  void RecordVariableReference(const nsAString& aVariableName)
-  {
-    size_t id;
-    if (mResolver.mVariableIDs.Get(aVariableName, &id)) {
-      mReferences[id] = true;
+  Variable& var = mVariables[aID];
+  bool forceUseInitial = false;
+  if (aResult == VisitNodeResult::UseInherited) {
+    if (var.mInherited.isSome()) {
+      const Entry& entry = var.mInherited.value();
+      mOutput->Put(var.mName, entry.mExpr, entry.mValue, entry.mValueType,
+                   entry.mContext, entry.mFirstToken, entry.mLastToken);
     } else {
-      mReferencesNonExistentVariable = true;
+      forceUseInitial = true;
     }
   }
-
-  bool HasReferenceToVariable(size_t aID) const
-  {
-    return mReferences[aID];
-  }
-
-  bool ReferencesNonExistentVariable() const
-  {
-   return mReferencesNonExistentVariable;
-  }
-
-private:
-  CSSVariableResolver& mResolver;
-
-  // Array of booleans, where each index is a variable ID.  If an element is
-  // true, it indicates that the variable we have called
-  // EnumerateVariableReferences for has a reference to the variable with
-  // that ID.
-  const UniquePtr<bool[]> mReferences;
-
-  // Whether the variable we have called EnumerateVariableReferences for
-  // references a variable that does not exist in the resolver.
-  bool mReferencesNonExistentVariable;
-};
-
-static void
-RecordVariableReference(const nsAString& aVariableName,
-                        void* aData)
-{
-  static_cast<EnumerateVariableReferencesData*>(aData)->
-    RecordVariableReference(aVariableName);
-}
-
-void
-CSSVariableResolver::RemoveCycles(size_t v)
-{
-  mVariables[v].mIndex = mNextIndex;
-  mVariables[v].mLowLink = mNextIndex;
-  mVariables[v].mInStack = true;
-  mStack.AppendElement(v);
-  mNextIndex++;
-
-  for (size_t i = 0, n = mReferences[v].Length(); i < n; i++) {
-    size_t w = mReferences[v][i];
-    if (!mVariables[w].mIndex) {
-      RemoveCycles(w);
-      mVariables[v].mLowLink = std::min(mVariables[v].mLowLink,
-                                        mVariables[w].mLowLink);
-    } else if (mVariables[w].mInStack) {
-      mVariables[v].mLowLink = std::min(mVariables[v].mLowLink,
-                                        mVariables[w].mIndex);
+  if (aResult == VisitNodeResult::UseInitial || forceUseInitial ||
+      aResult == VisitNodeResult::Cycle) {
+    const CSSVariableRegistration* registration =
+      mRegistrations ? mRegistrations->mData.Get(var.mName)
+                     : nullptr;
+    // The initial value of a custom property is an empty value; that is,
+    // nothing at all. This initial value has a special interaction with the
+    // var() notation, which is explained in the section defining var().
+    // -- CSS Variables § 2.1
+    // If there is a cycle in the dependency graph, all the custom properties in
+    // the cycle must compute to their initial value (which is a
+    // guaranteed-invalid value).
+    // -- CSS Variables § 2.2
+    // nsCSSParser::ResolveValueWithVariableReferencesRec looks for the empty
+    // string.
+    // If this is a registered property, though, we can use its initial value
+    // (which might actually be the empty string).
+    if (registration && !registration->mInitialExpr.IsEmpty()) {
+      mOutput->Put(var.mName, registration->mInitialExpr,
+                   registration->mInitialValue, registration->mInitialType,
+                   registration->mInitialContext, registration->mInitialFirstToken,
+                   registration->mInitialLastToken);
     }
   }
-
-  if (mVariables[v].mLowLink == mVariables[v].mIndex) {
-    if (mStack.LastElement() == v) {
-      // A strongly connected component consisting of a single variable is not
-      // necessarily invalid.  We handle variables that reference themselves
-      // earlier, in CSSVariableResolver::Resolve.
-      mVariables[mStack.LastElement()].mInStack = false;
-      mStack.TruncateLength(mStack.Length() - 1);
-    } else {
-      size_t w;
-      do {
-        w = mStack.LastElement();
-        mVariables[w].mValue.Truncate(0);
-        mVariables[w].mInStack = false;
-        mStack.TruncateLength(mStack.Length() - 1);
-      } while (w != v);
-    }
+  if (aResult == VisitNodeResult::Cycle) {
+    return true;
   }
-}
-
-void
-CSSVariableResolver::ResolveVariable(size_t aID)
-{
-  if (mVariables[aID].mValue.IsEmpty() || mVariables[aID].mWasInherited) {
-    // The variable is invalid or was inherited.   We can just copy the value
-    // and its first/last token information across.
-    mOutput->Put(mVariables[aID].mVariableName, mVariables[aID].mValue,
-                 // Replaced by a later patch in the series.
-                 nsCSSValue(), CSSValueType(), CSSVariableExprContext(),
-                 mVariables[aID].mFirstToken,
-                 mVariables[aID].mLastToken);
-  } else {
-    // Otherwise we need to resolve the variable references, after resolving
-    // all of our dependencies first.  We do this even for variables that we
-    // know do not reference other variables so that we can find their
-    // first/last token.
-    //
-    // XXX We might want to do this first/last token finding during
-    // EnumerateVariableReferences, so that we can avoid calling
-    // ResolveVariableValue and parsing the value again.
-    for (size_t i = 0, n = mReferences[aID].Length(); i < n; i++) {
-      size_t j = mReferences[aID][i];
-      if (aID != j && !mVariables[j].mResolved) {
-        ResolveVariable(j);
-      }
-    }
-    nsString resolvedValue;
-    nsCSSTokenSerializationType firstToken, lastToken;
-    if (!mParser.ResolveVariableValue(mVariables[aID].mValue, mOutput,
-                                      resolvedValue, firstToken, lastToken)) {
-      resolvedValue.Truncate(0);
-    }
-    mOutput->Put(mVariables[aID].mVariableName, resolvedValue,
-                 // Replaced by a later patch in the series.
-                 nsCSSValue(), CSSValueType(), CSSVariableExprContext(),
-                 firstToken, lastToken);
-                 
-  }
-  mVariables[aID].mResolved = true;
+  var.mDone = true;
+  return false;
 }
 
 void
 CSSVariableResolver::Resolve(const CSSVariableValues* aInherited,
-                             const CSSVariableDeclarations* aSpecified)
+                             const CSSVariableDeclarations* aSpecified,
+                             bool& aHasUninherited)
 {
   MOZ_ASSERT(!mResolved);
-
-  // The only time we would be worried about having a null aInherited is
-  // for the root, but in that case nsRuleNode::ComputeVariablesData will
-  // happen to pass in whatever we're using as mOutput for aInherited,
-  // which will initially be empty.
   MOZ_ASSERT(aInherited);
   MOZ_ASSERT(aSpecified);
 
   aInherited->AddVariablesToResolver(this);
   aSpecified->AddVariablesToResolver(this);
 
-  // First we look at each variable's value and record which other variables
-  // it references.
-  size_t n = mVariables.Length();
-  mReferences.SetLength(n);
-  EnumerateVariableReferencesData data(*this);
-  for (size_t id = 0; id < n; id++) {
-    data.Reset();
-    if (!mVariables[id].mWasInherited &&
-        !mVariables[id].mValue.IsEmpty()) {
-      if (mParser.EnumerateVariableReferences(mVariables[id].mValue,
-                                              RecordVariableReference,
-                                              &data)) {
-        // Convert the boolean array of dependencies in |data| to a list
-        // of dependencies.
-        for (size_t i = 0; i < n; i++) {
-          if (data.HasReferenceToVariable(i)) {
-            mReferences[id].AppendElement(i);
-          }
-        }
-        // If a variable references itself, it is invalid.  (RemoveCycles
-        // does not check for cycles consisting of a single variable, so we
-        // check here.)
-        if (data.HasReferenceToVariable(id)) {
-          mVariables[id].mValue.Truncate();
-        }
-        // Also record whether it referenced any variables that don't exist
-        // in the resolver, so that we can ensure we still resolve its value
-        // in ResolveVariable, even though its mReferences list is empty.
-        mVariables[id].mReferencesNonExistentVariable =
-          data.ReferencesNonExistentVariable();
+  // Load initial values.
+  if (mRegistrations) {
+    for (auto iter = mRegistrations->mData.ConstIter(); !iter.Done(); iter.Next()) {
+      const nsAString& name = iter.Key();
+      if (mVariableIDs.Get(name)) {
+        // This variable was specified or inherited. It's initial value will be
+        // populated, if at all (we don't want to if there is a compute-time
+        // error) in the VisitNodeResult::UseInitial branch below.
+        continue;
+      }
+      if (iter.UserData()->mInitialExpr.IsEmpty()) {
+        // All custom properties initially have their initial value (even those
+        // never specified or declared anywhere!) If we are supposed to load an
+        // initial value, then, we don't actually want to set anything.
       } else {
-        MOZ_ASSERT(false, "EnumerateVariableReferences should not have failed "
-                          "if we previously parsed the specified value");
-        mVariables[id].mValue.Truncate(0);
+        const CSSVariableRegistration* registration = iter.UserData();
+        mOutput->Put(name, registration->mInitialExpr,
+                     registration->mInitialValue, registration->mInitialType,
+                     registration->mInitialContext, registration->mInitialFirstToken,
+                     registration->mInitialLastToken);
       }
     }
   }
 
-  // Next we remove any cycles in variable references using Tarjan's strongly
-  // connected components finding algorithm, setting variables in cycles to
-  // have an invalid value.
-  mNextIndex = 1;
-  for (size_t id = 0; id < n; id++) {
-    if (!mVariables[id].mIndex) {
-      RemoveCycles(id);
-      MOZ_ASSERT(mStack.IsEmpty());
-    }
+  mVisited.InsertElementsAt(0, mVariables.Length(), false);
+
+  for (size_t varID = 0; varID < mVariables.Length(); varID++) {
+    VisitNodeResult result = VisitNode(varID, aHasUninherited);
+    HandleVisitNode(varID, result);
+    // Don't care about whether the result is a cycle or not here -- only inside
+    // VisitNode, where it causes us to fail *without using fallbacks* (as in
+    // the spec).
   }
 
-  // Finally we construct the computed value for the variable by substituting
-  // any variable references.
-  for (size_t id = 0; id < n; id++) {
-    if (!mVariables[id].mResolved) {
-      ResolveVariable(id);
-    }
-  }
+  // After visiting all the nodes, the resolved variables should be stored in
+  // mOutput.
 
 #ifdef DEBUG
   mResolved = true;
 #endif
 }
 
-void
-CSSVariableResolver::Put(const nsAString& aVariableName,
-                         nsString aValue,
-                         nsCSSTokenSerializationType aFirstToken,
-                         nsCSSTokenSerializationType aLastToken,
-                         bool aWasInherited)
+// VisitNode visits a node (a list of possible expressions for a given
+// variable) and recursively attempts to expand it.
+// Resolved values will be placed in CSSVariableValues* mOutput from the bottom
+// up.
+
+// Return true if we should insert the inherited value, if any.
+CSSVariableResolver::VisitNodeResult
+CSSVariableResolver::VisitNode(size_t aID, bool& aHasUninherited)
 {
-  MOZ_ASSERT(!mResolved);
+  MOZ_ASSERT(mVisited.Length() == mVariables.Length());
+  if (mVisited[aID]) {
+    // Already visited this node; nothing to do.
+    if (mVariables[aID].mDone) {
+      return VisitNodeResult::Done;
+    }
+    return VisitNodeResult::Cycle;
+  }
+  mVisited[aID] = true;
+  MOZ_ASSERT(!mVariables[aID].mDone);
+
+  const Variable& var = mVariables[aID];
+
+  CSSVariableRegistration* registration = nullptr;
+  if (mRegistrations) {
+    mRegistrations->mData.Get(var.mName, &registration);
+  }
+
+  if (var.mSpecified.isNothing()) {
+    // Nothing specified. Inherit, unless this is a custom property with
+    // inherit: false.
+    if (registration) {
+      if (registration->mInherited) {
+        return VisitNodeResult::UseInherited;
+      }
+      return VisitNodeResult::UseInitial;
+    }
+    return VisitNodeResult::UseInherited;
+  }
+
+  // At this point we know we're going to do something with the specified
+  // declaration (even if it turns out to be erroneous).
+  // We have to set aHasUninherited if this is an uninherited custom property so
+  // that our children don't inherit our decision.
+  if (registration && !registration->mInherited) {
+    aHasUninherited = true;
+  }
+
+  const Entry& entry = var.mSpecified.value();
+
+  if (entry.mParsed) {
+    // Fast path for when a variable is
+    // 1) a registered custom property
+    // 2) specified with the correct type
+    // 3) doesn't contain any variables
+    // In that case, we have already done the work that we would have otherwise
+    // done here in CSSVariableDeclarations by necessity (to expose indexed
+    // getters, etc.)
+    mOutput->Put(var.mName, entry.mExpr, entry.mValue, entry.mValueType,
+                 entry.mContext, entry.mFirstToken, entry.mLastToken);
+    return VisitNodeResult::Done;
+  }
+
+  if (entry.mType != CSSVariableDeclarations::Type::TokenStream) {
+    switch (entry.mType) {
+    case CSSVariableDeclarations::Type::Initial:
+      return VisitNodeResult::UseInitial;
+    case CSSVariableDeclarations::Type::Inherit:
+      // Returning true will signal to Resolve that we should use the inherited
+      // value, if any.
+      return VisitNodeResult::UseInherited;
+    case CSSVariableDeclarations::Type::Unset:
+      if (!registration) {
+        // Custom properties are always uninherited, and unset => inherited if
+        // the property is inherited.
+        return VisitNodeResult::UseInherited;
+      }
+      if (registration->mInherited) {
+        return VisitNodeResult::UseInherited;
+      }
+      // registered & uninherited => put initial value
+      return VisitNodeResult::UseInitial;
+    case CSSVariableDeclarations::Type::TokenStream:
+      MOZ_ASSERT_UNREACHABLE("I'm in an if statement!");
+      break;
+    default:
+      MOZ_ASSERT_UNREACHABLE("Is there a new CSSVariableDeclarations::Type?");
+    }
+  }
+
 
-  size_t id;
-  if (mVariableIDs.Get(aVariableName, &id)) {
-    MOZ_ASSERT(mVariables[id].mWasInherited && !aWasInherited,
-               "should only overwrite inherited variables with specified "
-               "variables");
-    mVariables[id].mValue = aValue;
-    mVariables[id].mFirstToken = aFirstToken;
-    mVariables[id].mLastToken = aLastToken;
-    mVariables[id].mWasInherited = aWasInherited;
-  } else {
-    id = mVariables.Length();
-    mVariableIDs.Put(aVariableName, id);
-    mVariables.AppendElement(Variable(aVariableName, aValue,
-                                      aFirstToken, aLastToken, aWasInherited));
+  for (size_t refID = 0; refID < entry.mRefs.Length(); refID++) {
+    const nsString& ref = entry.mRefs[refID];
+    size_t refVarID;
+    if (!mVariableIDs.Get(ref, &refVarID)) {
+      // This expression refers to a variable we haven't seen any declaration
+      // for and that hasn't been inherited.
+      // Furthermore, it doesn't have an initial value registered (see the start
+      // of Resolve). Can't resolve this.
+      // Don't error out, though, in case there is a fallback: wait for
+      // ResolveVariableValue.
+      continue;
+    }
+    VisitNodeResult result = VisitNode(refVarID, aHasUninherited);
+    bool cyclical = HandleVisitNode(refVarID, result);
+    if (cyclical) {
+      return VisitNodeResult::Cycle;
+    }
+  }
+
+  // Variables we referred will have been resolved by now, if they will ever
+  // be. They'll have been stored in mOutput.
+  // Try to expand entry.mExpr.
+  nsAutoString expr;
+  nsCSSTokenSerializationType firstToken, lastToken;
+  if (!mParser.ResolveVariableValue(entry.mExpr, mOutput, expr,
+                                    firstToken, lastToken)) {
+    // The specified variable has a syntax error (or a missing variable).
+    // Note: we *do not* fall back to the inherited value. This is a
+    // compute-time error.
+
+    // A declaration can be invalid at computed-value time if it contains a
+    // var() that references a custom property with its initial value, as
+    // explained above, or if it uses a valid custom property, but the property
+    // value, after substituting its var() functions, is invalid. When this
+    // happens, the computed value of the property is either the property’s
+    // inherited value or its initial value depending on whether the property is
+    // inherited or not, respectively, as if the property’s value had been
+    // specified as the unset keyword
+    // -- CSS Variables § 3.1
+    if (!registration) {
+      return VisitNodeResult::UseInherited;
+    }
+    if (registration->mInherited) {
+      return VisitNodeResult::UseInherited;
+    }
+    return VisitNodeResult::UseInitial;
+  }
+
+  if (!registration) {
+    // Variables that aren't custom properties are token stream values.
+    RefPtr<nsCSSValueTokenStream> tokenStream = new nsCSSValueTokenStream;
+    tokenStream->mPropertyID = eCSSPropertyExtra_variable;
+    tokenStream->mTokenStream = expr;
+    tokenStream->mBaseURI = entry.mContext.mBaseURI;
+    tokenStream->mSheetURI = entry.mContext.mSheetURI;
+    tokenStream->mSheetPrincipal = entry.mContext.mSheetPrincipal;
+    // XXX Should store sheet here (see bug 952338).
+    // tokenStream->mSheet = mSheet;
+    tokenStream->mLineNumber = 0;
+    tokenStream->mLineOffset = 0;
+    nsCSSValue value = nsCSSValue(tokenStream);
+    mOutput->Put(var.mName, expr, value, CSSValueType::TokenStream,
+                 entry.mContext, firstToken, lastToken);
+    return VisitNodeResult::Done;
+  }
+
+  // This is a registered property whose value isn't 'initial', 'inherit', or
+  // 'unset'.
+
+  nsCSSValue value;
+  CSSValueType type;
+  if (!mParser.ParseTypedValue(registration->mSyntax, expr,
+                               entry.mContext.mSheetURI,
+                               entry.mContext.mBaseURI,
+                               entry.mContext.mSheetPrincipal,
+                               value, type)) {
+    // Type of expanded value is incorrect. This expansion doesn't work!
+    // Try to fall back to the inherited value.
+    if (registration->mInherited) {
+      return VisitNodeResult::UseInherited;
+    }
+    return VisitNodeResult::UseInitial;
+  }
+
+  // Finally!
+
+  mOutput->Put(var.mName, expr, value, type, entry.mContext, firstToken,
+               lastToken);
+  return VisitNodeResult::Done;
+}
+
+void
+CSSVariableResolver::PutInherited(const nsAString& aVariableName,
+                                  const nsAString& aExpr,
+                                  nsCSSValue aValue, CSSValueType aValueType,
+                                  CSSVariableExprContext aContext,
+                                  nsCSSTokenSerializationType aFirstToken,
+                                  nsCSSTokenSerializationType aLastToken)
+{
+  size_t id = LookupOrAddVariable(aVariableName);
+  mVariables[id].mInherited =
+    Some(Entry(CSSVariableDeclarations::Type::TokenStream, nsString(aExpr),
+               aValue, aValueType, aContext, aFirstToken, aLastToken));
+}
+
+void
+CSSVariableResolver::PutSpecifiedUnparsed(const nsAString& aVariableName,
+                                          CSSVariableDeclarations::Type aType,
+                                          const nsAString& aExpr,
+                                          CSSVariableExprContext aContext)
+{
+  size_t id = LookupOrAddVariable(aVariableName);
+  mVariables[id].mSpecified = Some(Entry(aType, nsString(aExpr), aContext));
+
+  // Populate mRefs.
+  if (aType == CSSVariableDeclarations::Type::TokenStream &&
+      !mParser.EnumerateVariableReferences(
+           aExpr,
+           [](const nsAString& aName, void* aData) {
+             auto maybe = static_cast<Maybe<Entry>*>(aData);
+             maybe->ptr()->mRefs.AppendElement(aName);
+           }, &(mVariables[id].mSpecified))) {
+    MOZ_ASSERT_UNREACHABLE("EnumerateVariableReferences should not have failed "
+                           "if we previously parsed the specified value.");
   }
 }
 
+void
+CSSVariableResolver::PutSpecifiedParsed(const nsAString& aVariableName,
+                                        const nsAString& aExpr,
+                                        nsCSSValue aValue, CSSValueType aValueType,
+                                        CSSVariableExprContext aContext,
+                                        nsCSSTokenSerializationType aFirstToken,
+                                        nsCSSTokenSerializationType aLastToken)
+{
+  size_t id = LookupOrAddVariable(aVariableName);
+  mVariables[id].mSpecified =
+    Some(Entry(CSSVariableDeclarations::Type::TokenStream, nsString(aExpr),
+               aValue, aValueType, aContext, aFirstToken, aLastToken));
+}
+
 } // namespace mozilla
--- a/layout/style/CSSVariableResolver.h
+++ b/layout/style/CSSVariableResolver.h
@@ -5,144 +5,175 @@
 
 /* object that resolves CSS variables using specified and inherited variable
  * values
  */
 
 #ifndef mozilla_CSSVariableResolver_h
 #define mozilla_CSSVariableResolver_h
 
+#include "mozilla/CSSValueType.h"
+#include "mozilla/CSSVariableDeclarations.h"
+#include "mozilla/CSSVariableRegistrations.h"
 #include "mozilla/DebugOnly.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
 #include "nsCSSParser.h"
 #include "nsCSSScanner.h"
 #include "nsDataHashtable.h"
 #include "nsTArray.h"
 
 namespace mozilla {
 
 class CSSVariableDeclarations;
 class CSSVariableValues;
-class EnumerateVariableReferencesData;
 
 class CSSVariableResolver
 {
   friend class CSSVariableDeclarations;
   friend class CSSVariableValues;
-  friend class EnumerateVariableReferencesData;
 public:
   /**
    * Creates a new CSSVariableResolver that will output a set of resolved,
    * computed variables into aOutput.
    */
-  explicit CSSVariableResolver(CSSVariableValues* aOutput)
+  CSSVariableResolver(CSSVariableValues* aOutput,
+                      const CSSVariableRegistrations* aRegistrations)
     : mOutput(aOutput)
+    , mRegistrations(aRegistrations)
 #ifdef DEBUG
     , mResolved(false)
 #endif
   {
     MOZ_ASSERT(aOutput);
   }
 
   /**
    * Resolves the set of inherited variables from aInherited and the
    * set of specified variables from aSpecified.  The resolved variables
    * are written into mOutput.
    */
   void Resolve(const CSSVariableValues* aInherited,
-               const CSSVariableDeclarations* aSpecified);
-
+               const CSSVariableDeclarations* aSpecified,
+               bool& aHasUninherited);
 private:
-  struct Variable
+  struct Entry
   {
-    Variable(const nsAString& aVariableName,
-             nsString aValue,
-             nsCSSTokenSerializationType aFirstToken,
-             nsCSSTokenSerializationType aLastToken,
-             bool aWasInherited)
-      : mVariableName(aVariableName)
+    // Specified
+    Entry(CSSVariableDeclarations::Type aType, nsString aExpr,
+          CSSVariableExprContext aContext)
+      : mType(aType)
+      , mExpr(aExpr)
+      , mContext(aContext)
+      , mParsed(false)
+    { }
+
+    // Inherited
+    Entry(CSSVariableDeclarations::Type aType, nsString aExpr,
+          nsCSSValue aValue, CSSValueType aValueType,
+          CSSVariableExprContext aContext,
+          nsCSSTokenSerializationType aFirstToken,
+          nsCSSTokenSerializationType aLastToken)
+      : mType(aType)
+      , mExpr(aExpr)
       , mValue(aValue)
+      , mValueType(aValueType)
+      , mContext(aContext)
       , mFirstToken(aFirstToken)
       , mLastToken(aLastToken)
-      , mWasInherited(aWasInherited)
-      , mResolved(false)
-      , mReferencesNonExistentVariable(false)
-      , mInStack(false)
-      , mIndex(0)
-      , mLowLink(0) { }
+      , mParsed(true)
+    { }
 
-    nsString mVariableName;
-    nsString mValue;
+    CSSVariableDeclarations::Type mType;
+    nsString mExpr;
+    nsTArray<nsString> mRefs;
+    nsCSSValue mValue;
+    CSSValueType mValueType;
+    CSSVariableExprContext mContext;
     nsCSSTokenSerializationType mFirstToken;
     nsCSSTokenSerializationType mLastToken;
-
-    // Whether this variable came from the set of inherited variables.
-    bool mWasInherited;
-
-    // Whether this variable has been resolved yet.
-    bool mResolved;
+    bool mParsed;
+  };
 
-    // Whether this variables includes any references to non-existent variables.
-    bool mReferencesNonExistentVariable;
+  struct Variable
+  {
+    explicit Variable(const nsAString& aName)
+      : mName(aName)
+      , mDone(false)
+    { }
 
-    // Bookkeeping for the cycle remover algorithm.
-    bool mInStack;
-    size_t mIndex;
-    size_t mLowLink;
+    nsString mName;
+
+    Maybe<Entry> mInherited;
+    Maybe<Entry> mSpecified;
+
+    bool mDone;
   };
 
-  /**
-   * Adds or modifies an existing entry in the set of variables to be resolved.
-   * This is intended to be called by the AddVariablesToResolver functions on
-   * the CSSVariableDeclarations and CSSVariableValues objects passed in to
-   * Resolve.
-   *
-   * @param aName The variable name (not including any "--" prefix that would
-   *   be part of the custom property name) whose value is to be set.
-   * @param aValue The variable value.
-   * @param aFirstToken The type of token at the start of the variable value.
-   * @param aLastToken The type of token at the en of the variable value.
-   * @param aWasInherited Whether this variable came from the set of inherited
-   *   variables.
-   */
-  void Put(const nsAString& aVariableName,
-           nsString aValue,
-           nsCSSTokenSerializationType aFirstToken,
-           nsCSSTokenSerializationType aLastToken,
-           bool aWasInherited);
+  void PutInherited(const nsAString& aVariableName,
+                    const nsAString& aExpr,
+                    nsCSSValue aValue, CSSValueType aValueType,
+                    CSSVariableExprContext aContext,
+                    nsCSSTokenSerializationType aFirstToken,
+                    nsCSSTokenSerializationType aLastToken);
+
+  void PutSpecifiedUnparsed(const nsAString& aVariableName,
+                            CSSVariableDeclarations::Type aType,
+                            const nsAString& aExpr, CSSVariableExprContext aContext);
+
+  void PutSpecifiedParsed(const nsAString& aVariableName,
+                          const nsAString& aExpr, nsCSSValue aValue,
+                          CSSValueType aValueType, CSSVariableExprContext aContext,
+                          nsCSSTokenSerializationType aFirstToken,
+                          nsCSSTokenSerializationType aLastToken);
+
+  enum class VisitNodeResult
+  {
+    UseInherited,
+    UseInitial,
+    Done,
+    Cycle
+  };
+
+  bool HandleVisitNode(size_t aID, VisitNodeResult aResult);
 
   // Helper functions for Resolve.
-  void RemoveCycles(size_t aID);
-  void ResolveVariable(size_t aID);
+  VisitNodeResult VisitNode(size_t aID, bool& aHasUninherited);
 
   // A mapping of variable names to an ID that indexes into mVariables
   // and mReferences.
   nsDataHashtable<nsStringHashKey, size_t> mVariableIDs;
 
   // The set of variables.
   nsTArray<Variable> mVariables;
 
-  // The list of variables that each variable references.
-  nsTArray<nsTArray<size_t> > mReferences;
-
-  // The next index to assign to a variable found during the cycle removing
-  // algorithm's traversal of the variable reference graph.
-  size_t mNextIndex;
-
-  // Stack of variable IDs that we push to as we traverse the variable reference
-  // graph while looking for cycles.  Variable::mInStack reflects whether a
-  // given variable has its ID in mStack.
-  nsTArray<size_t> mStack;
-
   // CSS parser to use for parsing property values with variable references.
   nsCSSParser mParser;
 
   // The object to output the resolved variables into.
   CSSVariableValues* mOutput;
 
+  RefPtr<const CSSVariableRegistrations> mRegistrations;
+
+  nsTArray<bool> mVisited;
+
 #ifdef DEBUG
   // Whether Resolve has been called.
   bool mResolved;
 #endif
+
+private:
+  inline size_t
+  LookupOrAddVariable(const nsAString& aName)
+  {
+    size_t id;
+    if (!mVariableIDs.Get(aName, &id)) {
+      id = mVariables.Length();
+      mVariableIDs.Put(aName, id);
+      mVariables.AppendElement(Variable(aName));
+    }
+    return id;
+  }
 };
 
 } // namespace mozilla
 
 #endif
--- a/layout/style/CSSVariableValues.cpp
+++ b/layout/style/CSSVariableValues.cpp
@@ -149,15 +149,16 @@ CSSVariableValues::CopyVariablesFrom(con
         aOther.mVariables[i].mLastToken);
   }
 }
 
 void
 CSSVariableValues::AddVariablesToResolver(CSSVariableResolver* aResolver) const
 {
   for (size_t i = 0, n = mVariables.Length(); i < n; i++) {
-    // Replaced by a later patch in the series.
-    aResolver->Put(mVariables[i].mVariableName, mVariables[i].mExpr,
-                   mVariables[i].mFirstToken, mVariables[i].mLastToken, true);
+    aResolver->PutInherited(mVariables[i].mVariableName, mVariables[i].mExpr,
+                            mVariables[i].mValue, mVariables[i].mType,
+                            mVariables[i].mContext, mVariables[i].mFirstToken,
+                            mVariables[i].mLastToken);
   }
 }
 
 } // namespace mozilla
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -2111,18 +2111,18 @@ CSSParserImpl::ParseVariable(const nsASt
                                              aVariableName);
     REPORT_UNEXPECTED(PEDeclDropped);
     OUTPUT_ERROR();
   } else {
     CLEAR_ERROR();
     aDeclaration->AddVariableDeclaration(aVariableName, variableType,
                                          variableValue, aIsImportant, true,
                                          CSSVariableExprContext(mSheetURI,
-                                                             mBaseURI,
-                                                             mSheetPrincipal));
+                                                                mBaseURI,
+                                                                mSheetPrincipal));
     *aChanged = true;
   }
 
   mTempData.AssertInitialState();
 
   ReleaseScanner();
 }
 
@@ -7648,18 +7648,18 @@ CSSParserImpl::ParseDeclaration(css::Dec
   if (customProperty) {
     MOZ_ASSERT(Substring(propertyName, 0,
                          CSS_CUSTOM_NAME_PREFIX_LENGTH).EqualsLiteral("--"));
     // remove '--'
     nsDependentString varName(propertyName, CSS_CUSTOM_NAME_PREFIX_LENGTH);
     aDeclaration->AddVariableDeclaration(varName, variableType, variableValue,
                                          status == ePriority_Important, false,
                                          CSSVariableExprContext(mSheetURI,
-                                                             mBaseURI,
-                                                             mSheetPrincipal));
+                                                                mBaseURI,
+                                                                mSheetPrincipal));
   } else {
     *aChanged |= mData.TransferFromBlock(mTempData, propID, EnabledState(),
                                          status == ePriority_Important,
                                          false, aMustCallValueAppended,
                                          aDeclaration, GetDocument());
   }
 
   return true;
--- a/layout/style/nsRuleNode.cpp
+++ b/layout/style/nsRuleNode.cpp
@@ -2436,16 +2436,20 @@ nsRuleNode::WalkRuleTree(const nsStyleSt
   if (!highestNode)
     highestNode = rootNode;
 
   if (!ruleData.mConditions.CacheableWithoutDependencies())
     detail = eRulePartialMixed; // Treat as though some data is specified to avoid
                                 // the optimizations and force data computation.
 
   if (detail == eRuleNone && startStruct) {
+    MOZ_ASSERT(aSID != eStyleStruct_Variables,
+               "It shouldn't be possible to be here for the variables style, "
+               "unless we have changed it so that we can tell if all variables "
+               "have been specified.");
     // We specified absolutely no rule information, but a parent rule in the tree
     // specified all the rule information.  We set a bit along the branch from our
     // node in the tree to the node that specified the data that tells nodes on that
     // branch that they never need to examine their rules for this particular struct type
     // ever again.
     PropagateDependentBit(aSID, ruleNode, startStruct);
     // For inherited structs, mark the struct (which will be set on
     // the context by our caller) as not being owned by the context.
@@ -2460,16 +2464,18 @@ nsRuleNode::WalkRuleTree(const nsStyleSt
 
     return startStruct;
   }
   if ((!startStruct && !isReset &&
        (detail == eRuleNone || detail == eRulePartialInherited)) ||
       detail == eRuleFullInherited) {
     // We specified no non-inherited information and neither did any of
     // our parent rules.
+    // Note: for variables, this means that we didn't specify anything at all
+    // (see CheckVariablesCallback).
 
     // We set a bit along the branch from the highest node (ruleNode)
     // down to our node (this) indicating that no non-inherited data was
     // specified.  This bit is guaranteed to be set already on the path
     // from the highest node to the root node in the case where
     // (detail == eRuleNone), which is the most common case here.
     // We must check |!isReset| because the Compute*Data functions for
     // reset structs wouldn't handle none bits correctly.
@@ -2487,24 +2493,37 @@ nsRuleNode::WalkRuleTree(const nsStyleSt
              parentContext->GetPseudo() == nsCSSPseudoElements::firstLine) {
         parentContext = parentContext->GetParent();
       }
       if (parentContext && parentContext != aContext->GetParent()) {
         PropagateGrandancestorBit(aContext, parentContext);
       }
     }
     if (parentContext) {
-      // We have a parent, and so we should just inherit from the parent.
-      // Set the inherit bits on our context.  These bits tell the style context that
-      // it never has to go back to the rule tree for data.  Instead the style context tree
-      // should be walked to find the data.
       const void* parentStruct = parentContext->StyleData(aSID);
-      aContext->AddStyleBit(bit); // makes const_cast OK.
-      aContext->SetStyle(aSID, const_cast<void*>(parentStruct));
-      return parentStruct;
+      bool share = true;
+
+      if (aSID == eStyleStruct_Variables) {
+        const nsStyleVariables* parentVariables = static_cast<const
+          nsStyleVariables*>(parentStruct);
+        if (parentVariables->mHasUninherited) {
+          share = false;
+        }
+      }
+
+      if (share) {
+        // We have a parent, and so we should just inherit from the parent.
+        // Set the inherit bits on our context.  These bits tell the style context that
+        // it never has to go back to the rule tree for data.  Instead the style context tree
+        // should be walked to find the data.
+        aContext->AddStyleBit(bit); // makes const_cast OK.
+        aContext->SetStyle(aSID, const_cast<void*>(parentStruct));
+
+        return parentStruct;
+      }
     }
     else
       // We are the root.  In the case of fonts, the default values just
       // come from the pres context.
       return SetDefaultOnRoot(aSID, aContext);
   }
 
   typedef const void* (nsRuleNode::*ComputeFunc)(void*, const nsRuleData*,
@@ -10150,23 +10169,34 @@ nsRuleNode::ComputeVariablesData(void* a
                                  const nsRuleData* aRuleData,
                                  nsStyleContext* aContext,
                                  nsRuleNode* aHighestNode,
                                  const RuleDetail aRuleDetail,
                                  const RuleNodeCacheConditions aConditions)
 {
   COMPUTE_START_INHERITED(Variables, variables, parentVariables)
 
-  MOZ_ASSERT(aRuleData->mVariables,
-             "shouldn't be in ComputeVariablesData if there were no variable "
-             "declarations specified");
-
-  CSSVariableResolver resolver(&variables->mVariables);
-  resolver.Resolve(&parentVariables->mVariables,
-                   aRuleData->mVariables);
+  CSSVariableDeclarations* specified;
+  if (!aRuleData->mVariables) {
+    // We must have been called because our parent set some uninherited custom
+    // properties.
+    specified = new CSSVariableDeclarations(nullptr);
+  } else {
+    specified = aRuleData->mVariables;
+  }
+
+  const CSSVariableRegistrations* registrations =
+    CSSVariableRegistrationsOfStyleContext(aContext);
+  CSSVariableResolver resolver(&variables->mVariables, registrations);
+
+  resolver.Resolve(&parentVariables->mVariables, specified, variables->mHasUninherited);
+
+  if (!aRuleData->mVariables)
+    delete specified;
+
   conditions.SetUncacheable();
 
   COMPUTE_END_INHERITED(Variables, variables)
 }
 
 const void*
 nsRuleNode::ComputeEffectsData(void* aStartStruct,
                                const nsRuleData* aRuleData,
--- a/layout/style/nsStyleContext.cpp
+++ b/layout/style/nsStyleContext.cpp
@@ -950,17 +950,18 @@ nsStyleContext::CalcStyleDifferenceInter
 
   // If we had any change in variable values, then we'll need to examine
   // all of the other style structs too, even if the new style context has
   // the same source as the old one.
   const nsStyleVariables* thisVariables = PeekStyleVariables();
   if (thisVariables) {
     structsFound |= NS_STYLE_INHERIT_BIT(Variables);
     const nsStyleVariables* otherVariables = aNewContext->StyleVariables();
-    if (thisVariables->mVariables == otherVariables->mVariables) {
+    if (thisVariables->mVariables == otherVariables->mVariables &&
+        thisVariables->mHasUninherited == otherVariables->mHasUninherited) {
       *aEqualStructs |= NS_STYLE_INHERIT_BIT(Variables);
     } else {
       compare = true;
     }
   } else {
     *aEqualStructs |= NS_STYLE_INHERIT_BIT(Variables);
   }
 
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -3713,22 +3713,24 @@ nsStyleUIReset::CalcDifference(const nsS
   return nsChangeHint(0);
 }
 
 //-----------------------
 // nsStyleVariables
 //
 
 nsStyleVariables::nsStyleVariables(StyleStructContext aContext)
+  : mHasUninherited(false)
 {
   MOZ_COUNT_CTOR(nsStyleVariables);
 }
 
 nsStyleVariables::nsStyleVariables(const nsStyleVariables& aSource)
   : mVariables(aSource.mVariables)
+  , mHasUninherited(aSource.mHasUninherited)
 {
   MOZ_COUNT_CTOR(nsStyleVariables);
 }
 
 nsStyleVariables::~nsStyleVariables()
 {
   MOZ_COUNT_DTOR(nsStyleVariables);
 }
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -3672,16 +3672,17 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
   }
   static nsChangeHint DifferenceAlwaysHandledForDescendants() {
     // CalcDifference never returns nsChangeHint_NeedReflow or
     // nsChangeHint_ClearAncestorIntrinsics at all.
     return nsChangeHint(0);
   }
 
   mozilla::CSSVariableValues mVariables;
+  bool mHasUninherited;
 };
 
 struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleEffects
 {
   explicit nsStyleEffects(StyleStructContext aContext);
   nsStyleEffects(const nsStyleEffects& aSource);
   ~nsStyleEffects();