diff --git a/core/lib/Drupal/Core/Render/theme.api.php b/core/lib/Drupal/Core/Render/theme.api.php
index 44fcdd0..5d20ba9 100644
--- a/core/lib/Drupal/Core/Render/theme.api.php
+++ b/core/lib/Drupal/Core/Render/theme.api.php
@@ -1092,87 +1092,16 @@ function hook_page_bottom(array &$page_bottom) {
  *   The directory path of the theme or module, so that it doesn't need to be
  *   looked up.
  *
- * @return array
+ * @return \Drupal\Core\Theme\ThemeHook[]
  *   An associative array of information about theme implementations. The keys
  *   on the outer array are known as "theme hooks". For theme suggestions,
  *   instead of the array key being the base theme hook, the key is a theme
  *   suggestion name with the format 'base_hook_name__sub_hook_name'.
  *   For render elements, the key is the machine name of the render element.
- *   The array values are themselves arrays containing information about the
- *   theme hook and its implementation. Each information array must contain
+ *   The array values are value objects containing information about the
+ *   theme hook and its implementation. Each theme hook object must specify
  *   either a 'variables' element (for using a #theme element) or a
  *   'render element' element (for render elements), but not both.
- *   The following elements may be part of each information array:
- *   - variables: Only used for #theme in render array: an array of variables,
- *     where the array keys are the names of the variables, and the array
- *     values are the default values if they are not given in the render array.
- *     Template implementations receive each array key as a variable in the
- *     template file (so they must be legal PHP/Twig variable names). Function
- *     implementations are passed the variables in a single $variables function
- *     argument. If you are using these variables in a render array, prefix the
- *     variable names defined here with a #.
- *   - render element: Used for render element items only: the name of the
- *     renderable element or element tree to pass to the theme function. This
- *     name is used as the name of the variable that holds the renderable
- *     element or tree in preprocess and process functions.
- *   - file: The file the implementation resides in. This file will be included
- *     prior to the theme being rendered, to make sure that the function or
- *     preprocess function (as needed) is actually loaded.
- *   - path: Override the path of the file to be used. Ordinarily the module or
- *     theme path will be used, but if the file will not be in the default
- *     path, include it here. This path should be relative to the Drupal root
- *     directory.
- *   - template: If specified, the theme implementation is a template file, and
- *     this is the template name. Do not add 'html.twig' on the end of the
- *     template name. The extension will be added automatically by the default
- *     rendering engine (which is Twig.) If 'path' is specified, 'template'
- *     should also be specified. If neither 'template' nor 'function' are
- *     specified, a default template name will be assumed. For example, if a
- *     module registers the 'search_result' theme hook, 'search-result' will be
- *     assigned as its template name.
- *   - function: (deprecated in Drupal 8.0.x, will be removed in Drupal 9.0.x)
- *     If specified, this will be the function name to invoke for this
- *     implementation. If neither 'template' nor 'function' are specified, a
- *     default template name will be assumed. See above for more details.
- *   - base hook: Used for theme suggestions only: the base theme hook name.
- *     Instead of this suggestion's implementation being used directly, the base
- *     hook will be invoked with this implementation as its first suggestion.
- *     The base hook's files will be included and the base hook's preprocess
- *     functions will be called in addition to any suggestion's preprocess
- *     functions. If an implementation of hook_theme_suggestions_HOOK() (where
- *     HOOK is the base hook) changes the suggestion order, a different
- *     suggestion may be used in place of this suggestion. If after
- *     hook_theme_suggestions_HOOK() this suggestion remains the first
- *     suggestion, then this suggestion's function or template will be used to
- *     generate the rendered output.
- *   - pattern: A regular expression pattern to be used to allow this theme
- *     implementation to have a dynamic name. The convention is to use __ to
- *     differentiate the dynamic portion of the theme. For example, to allow
- *     forums to be themed individually, the pattern might be: 'forum__'. Then,
- *     when the forum is rendered, following render array can be used:
- *     @code
- *     $render_array = array(
- *       '#theme' => array('forum__' . $tid, 'forum'),
- *       '#forum' => $forum,
- *     );
- *     @endcode
- *   - preprocess functions: A list of functions used to preprocess this data.
- *     Ordinarily this won't be used; it's automatically filled in. By default,
- *     for a module this will be filled in as template_preprocess_HOOK. For
- *     a theme this will be filled in as twig_preprocess and
- *     twig_preprocess_HOOK as well as themename_preprocess and
- *     themename_preprocess_HOOK.
- *   - override preprocess functions: Set to TRUE when a theme does NOT want
- *     the standard preprocess functions to run. This can be used to give a
- *     theme FULL control over how variables are set. For example, if a theme
- *     wants total control over how certain variables in the page.html.twig are
- *     set, this can be set to true. Please keep in mind that when this is used
- *     by a theme, that theme becomes responsible for making sure necessary
- *     variables are set.
- *   - type: (automatically derived) Where the theme hook is defined:
- *     'module', 'theme_engine', or 'theme'.
- *   - theme path: (automatically derived) The directory path of the theme or
- *     module, so that it doesn't need to be looked up.
  *
  * @see themeable
  * @see hook_theme_registry_alter()
diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php
index a0af702..0022c76 100644
--- a/core/lib/Drupal/Core/Theme/Registry.php
+++ b/core/lib/Drupal/Core/Theme/Registry.php
@@ -25,13 +25,6 @@
 class Registry implements DestructableInterface {
 
   /**
-   * The theme object representing the active theme for this registry.
-   *
-   * @var \Drupal\Core\Theme\ActiveTheme
-   */
-  protected $theme;
-
-  /**
    * The lock backend that should be used.
    *
    * @var \Drupal\Core\Lock\LockBackendInterface
@@ -106,13 +99,6 @@ class Registry implements DestructableInterface {
   protected $runtimeRegistry = [];
 
   /**
-   * Stores whether the registry was already initialized.
-   *
-   * @var bool
-   */
-  protected $initialized = FALSE;
-
-  /**
    * The name of the theme for which to construct the registry, if given.
    *
    * @var string|null
@@ -141,6 +127,13 @@ class Registry implements DestructableInterface {
   protected $themeManager;
 
   /**
+   * The theme initialization.
+   *
+   * @var \Drupal\Core\Theme\ThemeInitializationInterface
+   */
+  protected $themeInitialization;
+
+  /**
    * The runtime cache.
    *
    * @var \Drupal\Core\Cache\CacheBackendInterface
@@ -196,37 +189,37 @@ public function setThemeManager(ThemeManagerInterface $theme_manager) {
    *
    * @param string $theme_name
    *   (optional) The name of the theme for which to construct the registry.
+   *
+   * @return \Drupal\Core\Theme\ActiveTheme
+   *   The active theme.
    */
-  protected function init($theme_name = NULL) {
-    if ($this->initialized) {
-      return;
-    }
+  protected function getTheme($theme_name = NULL) {
     // Unless instantiated for a specific theme, use globals.
     if (!isset($theme_name)) {
-      $this->theme = $this->themeManager->getActiveTheme();
+      $theme = $this->themeManager->getActiveTheme();
     }
     // Instead of the active theme, a specific theme was requested.
     else {
-      $this->theme = $this->themeInitialization->getActiveThemeByName($theme_name);
-      $this->themeInitialization->loadActiveTheme($this->theme);
+      $theme = $this->themeInitialization->initTheme($theme_name);
     }
+    return $theme;
   }
 
   /**
    * Returns the complete theme registry from cache or rebuilds it.
    *
-   * @return array
+   * @return \Drupal\Core\Theme\ThemeHook[]
    *   The complete theme registry data array.
    *
    * @see Registry::$registry
    */
   public function get() {
-    $this->init($this->themeName);
-    if (isset($this->registry[$this->theme->getName()])) {
-      return $this->registry[$this->theme->getName()];
+    $theme = $this->getTheme($this->themeName);
+    if (isset($this->registry[$theme->getName()])) {
+      return $this->registry[$theme->getName()];
     }
-    if ($cache = $this->cache->get('theme_registry:' . $this->theme->getName())) {
-      $this->registry[$this->theme->getName()] = $cache->data;
+    if ($cache = $this->cache->get('theme_registry:' . $theme->getName())) {
+      $this->registry[$theme->getName()] = $cache->data;
     }
     else {
       $this->build();
@@ -235,7 +228,7 @@ public function get() {
         $this->setCache();
       }
     }
-    return $this->registry[$this->theme->getName()];
+    return $this->registry[$theme->getName()];
   }
 
   /**
@@ -247,18 +240,19 @@ public function get() {
    *   lightweight than the full registry.
    */
   public function getRuntime() {
-    $this->init($this->themeName);
-    if (!isset($this->runtimeRegistry[$this->theme->getName()])) {
-      $this->runtimeRegistry[$this->theme->getName()] = new ThemeRegistry('theme_registry:runtime:' . $this->theme->getName(), $this->runtimeCache ?: $this->cache, $this->lock, ['theme_registry'], $this->moduleHandler->isLoaded());
+    $theme = $this->getTheme($this->themeName);
+    if (!isset($this->runtimeRegistry[$theme->getName()])) {
+      $this->runtimeRegistry[$theme->getName()] = new ThemeRegistry('theme_registry:runtime:' . $theme->getName(), $this->runtimeCache ?: $this->cache, $this->lock, ['theme_registry'], $this->moduleHandler->isLoaded());
     }
-    return $this->runtimeRegistry[$this->theme->getName()];
+    return $this->runtimeRegistry[$theme->getName()];
   }
 
   /**
    * Persists the theme registry in the cache backend.
    */
   protected function setCache() {
-    $this->cache->set('theme_registry:' . $this->theme->getName(), $this->registry[$this->theme->getName()], Cache::PERMANENT, ['theme_registry']);
+    $theme = $this->getTheme($this->themeName);
+    $this->cache->set('theme_registry:' . $theme->getName(), $this->registry[$theme->getName()], Cache::PERMANENT, ['theme_registry']);
   }
 
   /**
@@ -271,7 +265,6 @@ protected function setCache() {
    *   The name of the base hook or FALSE.
    */
   public function getBaseHook($hook) {
-    $this->init($this->themeName);
     $base_hook = $hook;
     // Iteratively strip everything after the last '__' delimiter, until a
     // base hook definition is found. Recursive base hooks of base hooks are
@@ -338,49 +331,165 @@ protected function build() {
       }
     }
 
+    $theme = $this->getTheme($this->themeName);
     // Process each base theme.
     // Ensure that we start with the root of the parents, so that both CSS files
     // and preprocess functions comes first.
-    foreach (array_reverse($this->theme->getBaseThemes()) as $base) {
+    foreach (array_reverse($theme->getBaseThemes()) as $base) {
       // If the base theme uses a theme engine, process its hooks.
+      /** @var \Drupal\Core\Theme\ActiveTheme $base */
       $base_path = $base->getPath();
-      if ($this->theme->getEngine()) {
-        $this->processExtension($cache, $this->theme->getEngine(), 'base_theme_engine', $base->getName(), $base_path);
+      if ($theme->getEngine()) {
+        $this->processExtension($cache, $theme->getEngine(), 'base_theme_engine', $base->getName(), $base_path);
       }
       $this->processExtension($cache, $base->getName(), 'base_theme', $base->getName(), $base_path);
     }
 
     // And then the same thing, but for the theme.
-    if ($this->theme->getEngine()) {
-      $this->processExtension($cache, $this->theme->getEngine(), 'theme_engine', $this->theme->getName(), $this->theme->getPath());
+    if ($theme->getEngine()) {
+      $this->processExtension($cache, $theme->getEngine(), 'theme_engine', $theme->getName(), $theme->getPath());
     }
 
     // Hooks provided by the theme itself.
-    $this->processExtension($cache, $this->theme->getName(), 'theme', $this->theme->getName(), $this->theme->getPath());
+    $this->processExtension($cache, $theme->getName(), 'theme', $theme->getName(), $theme->getPath());
 
     // Discover and add all preprocess functions for theme hook suggestions.
-    $this->postProcessExtension($cache, $this->theme);
+    $this->postProcessExtension($cache, $theme);
 
     // Let modules and themes alter the registry.
     $this->moduleHandler->alter('theme_registry', $cache);
-    $this->themeManager->alterForTheme($this->theme, 'theme_registry', $cache);
+    $this->themeManager->alterForTheme($theme, 'theme_registry', $cache);
 
-    // @todo Implement more reduction of the theme registry entry.
-    // Optimize the registry to not have empty arrays for functions.
-    foreach ($cache as $hook => $info) {
-      if (empty($info['preprocess functions'])) {
-        unset($cache[$hook]['preprocess functions']);
+    $this->registry[$theme->getName()] = $cache;
+
+    return $this->registry[$theme->getName()];
+  }
+
+  /**
+   * @todo.
+   */
+  protected function handlePreprocessFunctions(ThemeHook $info, array $module_list, $name, $theme, $hook) {
+    // Preprocess variables for all theming hooks, whether the hook is
+    // implemented as a template or as a function. Ensure they are arrays.
+    if (!$info->getPreprocessFunctions()) {
+      $prefixes = [];
+      if ($info->getType() == 'module') {
+        // Default variable preprocessor prefix.
+        $prefixes[] = 'template';
+        // Add all modules so they can intervene with their own variable
+        // preprocessors. This allows them to provide variable preprocessors
+        // even if they are not the owner of the current hook.
+        $prefixes = array_merge($prefixes, $module_list);
+      }
+      elseif ($info->getType() == 'theme_engine' || $info->getType() == 'base_theme_engine') {
+        // Theme engines get an extra set that come before the normally
+        // named variable preprocessors.
+        $prefixes[] = $name . '_engine';
+        // The theme engine registers on behalf of the theme using the
+        // theme's name.
+        $prefixes[] = $theme;
+      }
+      else {
+        // This applies when the theme manually registers their own variable
+        // preprocessors.
+        $prefixes[] = $name;
+      }
+      foreach ($prefixes as $prefix) {
+        // Only use non-hook-specific variable preprocessors for theming
+        // hooks implemented as templates. See the @defgroup themeable
+        // topic.
+        if ($info->getTemplate() && function_exists($prefix . '_preprocess')) {
+          $info->addPreprocessFunction($prefix . '_preprocess');
+        }
+        if (function_exists($prefix . '_preprocess_' . $hook)) {
+          $info->addPreprocessFunction($prefix . '_preprocess_' . $hook);
+        }
+      }
+    }
+  }
+
+  /**
+   * @todo.
+   */
+  protected function handleIncludes(ThemeHook $info, ThemeHook $cached_info = NULL) {
+    if ($cached_info && $cached_info->getIncludes()) {
+      $info->setIncludes($cached_info->getIncludes());
+    }
+
+    // If the theme implementation defines a file, then also use the path
+    // that it defined. Otherwise use the default path. This allows
+    // system.module to declare theme functions on behalf of core .include
+    // files.
+    if ($info->getFile()) {
+      $include_file = $info->getPath() ?: $info->getThemePath();
+      $include_file .= '/' . $info->getFile();
+      $info->addInclude($include_file);
+    }
+
+    // Load the includes, as they may contain preprocess functions.
+    if ($info->hasIncludes()) {
+      foreach ($info->getIncludes() as $include_file) {
+        include_once $this->root . '/' . $include_file;
       }
     }
-    $this->registry[$this->theme->getName()] = $cache;
+  }
 
-    return $this->registry[$this->theme->getName()];
+  /**
+   * @todo.
+   */
+  protected function handleResult($hook, ThemeHook $info, ThemeHook $cached_info = NULL) {
+    // When a theme or engine overrides a module's theme function $info will
+    // only contain key/value pairs for information being overridden. Pull the
+    // rest of the information from what was defined by an earlier hook.
+
+    // If a theme hook has a base hook, mark its preprocess functions always
+    // incomplete in order to inherit the base hook's preprocess functions.
+    if ($info->getBaseHook()) {
+      $info->markPreprocessFunctionsIncomplete();
+    }
+
+    $this->handleIncludes($info, $cached_info);
+
+    // A template file is the default implementation for a theme hook, but
+    // if the theme hook specifies a function callback instead, check to
+    // ensure the function actually exists.
+    if ($function = $info->getFunction()) {
+      if (!function_exists($function)) {
+        throw new \BadFunctionCallException(sprintf(
+          'Theme hook "%s" refers to a theme function callback that does not exist: "%s"',
+          $hook,
+          $function
+        ));
+      }
+    }
+    // Provide a default naming convention for 'template' based on the
+    // hook used. If the template does not exist, the theme engine used
+    // should throw an exception at runtime when attempting to include
+    // the template file.
+    elseif (!$info->getTemplate()) {
+      $info->setTemplate(strtr($hook, '_', '-'));
+    }
+
+    // Prepend the current theming path when none is set. This is required
+    // for the default theme engine to know where the template lives.
+    if ($info->getTemplate() && !$info->getPath()) {
+      $info->setPath($info->getThemePath() . '/templates');
+    }
+
+    // If the default keys are not set, use the default values registered
+    // by the module.
+    if ($cached_info) {
+      $info->mergeVariables($cached_info);
+      $info->mergeRenderElement($cached_info);
+      $info->mergePattern($cached_info);
+      $info->mergeBaseHook($cached_info);
+    }
   }
 
   /**
    * Process a single implementation of hook_theme().
    *
-   * @param array $cache
+   * @param \Drupal\Core\Theme\ThemeHook[] $cache
    *   The theme registry that will eventually be cached; It is an associative
    *   array keyed by theme hooks, whose values are associative arrays
    *   describing the hook:
@@ -430,13 +539,6 @@ protected function build() {
   protected function processExtension(array &$cache, $name, $type, $theme, $path) {
     $result = [];
 
-    $hook_defaults = [
-      'variables' => TRUE,
-      'render element' => TRUE,
-      'pattern' => TRUE,
-      'base hook' => TRUE,
-    ];
-
     $module_list = array_keys($this->moduleHandler->getModuleList());
 
     // Invoke the hook_theme() implementation, preprocess what is returned, and
@@ -444,131 +546,32 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path)
     $function = $name . '_theme';
     if (function_exists($function)) {
       $result = $function($cache, $type, $theme, $path);
+      $to_be_merged = [];
       foreach ($result as $hook => $info) {
-        // When a theme or engine overrides a module's theme function
-        // $result[$hook] will only contain key/value pairs for information being
-        // overridden.  Pull the rest of the information from what was defined by
-        // an earlier hook.
+        $cached_info = isset($cache[$hook]) ? $cache[$hook] : NULL;
 
+        // @todo.
+        if (is_array($info)) {
+          $info = ThemeHook::createFromLegacy($info);
+        }
         // Fill in the type and path of the module, theme, or engine that
         // implements this theme function.
-        $result[$hook]['type'] = $type;
-        $result[$hook]['theme path'] = $path;
+        $info->setType($type);
+        $info->setThemePath($path);
+        $this->handleResult($hook, $info, $cached_info);
+        $this->handlePreprocessFunctions($info, $module_list, $name, $theme, $hook);
 
-        // If a theme hook has a base hook, mark its preprocess functions always
-        // incomplete in order to inherit the base hook's preprocess functions.
-        if (!empty($result[$hook]['base hook'])) {
-          $result[$hook]['incomplete preprocess functions'] = TRUE;
-        }
-
-        if (isset($cache[$hook]['includes'])) {
-          $result[$hook]['includes'] = $cache[$hook]['includes'];
-        }
-
-        // Load the includes, as they may contain preprocess functions.
-        if (isset($info['includes'])) {
-          foreach ($info['includes'] as $include_file) {
-            include_once $this->root . '/' . $include_file;
-          }
-        }
-
-        // If the theme implementation defines a file, then also use the path
-        // that it defined. Otherwise use the default path. This allows
-        // system.module to declare theme functions on behalf of core .include
-        // files.
-        if (isset($info['file'])) {
-          $include_file = isset($info['path']) ? $info['path'] : $path;
-          $include_file .= '/' . $info['file'];
-          include_once $this->root . '/' . $include_file;
-          $result[$hook]['includes'][] = $include_file;
-        }
-
-        // A template file is the default implementation for a theme hook, but
-        // if the theme hook specifies a function callback instead, check to
-        // ensure the function actually exists.
-        if (isset($info['function'])) {
-          if (!function_exists($info['function'])) {
-            throw new \BadFunctionCallException(sprintf(
-              'Theme hook "%s" refers to a theme function callback that does not exist: "%s"',
-              $hook,
-              $info['function']
-            ));
-          }
-        }
-        // Provide a default naming convention for 'template' based on the
-        // hook used. If the template does not exist, the theme engine used
-        // should throw an exception at runtime when attempting to include
-        // the template file.
-        elseif (!isset($info['template'])) {
-          $info['template'] = strtr($hook, '_', '-');
-          $result[$hook]['template'] = $info['template'];
-        }
-
-        // Prepend the current theming path when none is set. This is required
-        // for the default theme engine to know where the template lives.
-        if (isset($result[$hook]['template']) && !isset($info['path'])) {
-          $result[$hook]['path'] = $path . '/templates';
-        }
-
-        // If the default keys are not set, use the default values registered
-        // by the module.
-        if (isset($cache[$hook])) {
-          $result[$hook] += array_intersect_key($cache[$hook], $hook_defaults);
-        }
-
-        // Preprocess variables for all theming hooks, whether the hook is
-        // implemented as a template or as a function. Ensure they are arrays.
-        if (!isset($info['preprocess functions']) || !is_array($info['preprocess functions'])) {
-          $info['preprocess functions'] = [];
-          $prefixes = [];
-          if ($type == 'module') {
-            // Default variable preprocessor prefix.
-            $prefixes[] = 'template';
-            // Add all modules so they can intervene with their own variable
-            // preprocessors. This allows them to provide variable preprocessors
-            // even if they are not the owner of the current hook.
-            $prefixes = array_merge($prefixes, $module_list);
-          }
-          elseif ($type == 'theme_engine' || $type == 'base_theme_engine') {
-            // Theme engines get an extra set that come before the normally
-            // named variable preprocessors.
-            $prefixes[] = $name . '_engine';
-            // The theme engine registers on behalf of the theme using the
-            // theme's name.
-            $prefixes[] = $theme;
-          }
-          else {
-            // This applies when the theme manually registers their own variable
-            // preprocessors.
-            $prefixes[] = $name;
-          }
-          foreach ($prefixes as $prefix) {
-            // Only use non-hook-specific variable preprocessors for theming
-            // hooks implemented as templates. See the @defgroup themeable
-            // topic.
-            if (isset($info['template']) && function_exists($prefix . '_preprocess')) {
-              $info['preprocess functions'][] = $prefix . '_preprocess';
-            }
-            if (function_exists($prefix . '_preprocess_' . $hook)) {
-              $info['preprocess functions'][] = $prefix . '_preprocess_' . $hook;
-            }
-          }
-        }
         // Check for the override flag and prevent the cached variable
         // preprocessors from being used. This allows themes or theme engines
         // to remove variable preprocessors set earlier in the registry build.
-        if (!empty($info['override preprocess functions'])) {
-          // Flag not needed inside the registry.
-          unset($result[$hook]['override preprocess functions']);
+        if (!$info->getOverriddenPreprocessFunctionStatus() && $cached_info) {
+          $info->mergePreprocessFunctions($cached_info);
         }
-        elseif (isset($cache[$hook]['preprocess functions']) && is_array($cache[$hook]['preprocess functions'])) {
-          $info['preprocess functions'] = array_merge($cache[$hook]['preprocess functions'], $info['preprocess functions']);
-        }
-        $result[$hook]['preprocess functions'] = $info['preprocess functions'];
+        $to_be_merged[$hook] = $info;
       }
 
       // Merge the newly created theme hooks into the existing cache.
-      $cache = $result + $cache;
+      $cache = $to_be_merged + $cache;
     }
 
     // Let themes have variable preprocessors even if they didn't register a
@@ -577,17 +580,14 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path)
       foreach ($cache as $hook => $info) {
         // Check only if not registered by the theme or engine.
         if (empty($result[$hook])) {
-          if (!isset($info['preprocess functions'])) {
-            $cache[$hook]['preprocess functions'] = [];
-          }
           // Only use non-hook-specific variable preprocessors for theme hooks
           // implemented as templates. See the @defgroup themeable topic.
-          if (isset($info['template']) && function_exists($name . '_preprocess')) {
-            $cache[$hook]['preprocess functions'][] = $name . '_preprocess';
+          if ($info->getTemplate() && function_exists($name . '_preprocess')) {
+            $info->addPreprocessFunction($name . '_preprocess');
           }
           if (function_exists($name . '_preprocess_' . $hook)) {
-            $cache[$hook]['preprocess functions'][] = $name . '_preprocess_' . $hook;
-            $cache[$hook]['theme path'] = $path;
+            $info->addPreprocessFunction($name . '_preprocess_' . $hook);
+            $info->setThemePath($path);
           }
         }
       }
@@ -599,34 +599,42 @@ protected function processExtension(array &$cache, $name, $type, $theme, $path)
    *
    * @param string $hook
    *   The name of the suggestion hook to complete.
-   * @param array $cache
+   * @param \Drupal\Core\Theme\ThemeHook[] $cache
    *   The theme registry, as documented in
    *   \Drupal\Core\Theme\Registry::processExtension().
    */
   protected function completeSuggestion($hook, array &$cache) {
     $previous_hook = $hook;
-    $incomplete_previous_hook = [];
+    $incomplete_previous_hook = NULL;
     // Continue looping if the candidate hook doesn't exist or if the candidate
     // hook has incomplete preprocess functions, and if the candidate hook is a
     // suggestion (has a double underscore).
-    while ((!isset($cache[$previous_hook]) || isset($cache[$previous_hook]['incomplete preprocess functions']))
+    while ((!isset($cache[$previous_hook]) || $cache[$previous_hook]->hasIncompletePreprocessFunctions())
       && $pos = strrpos($previous_hook, '__')) {
       // Find the first existing candidate hook that has incomplete preprocess
       // functions.
-      if (isset($cache[$previous_hook]) && !$incomplete_previous_hook && isset($cache[$previous_hook]['incomplete preprocess functions'])) {
-        $incomplete_previous_hook = $cache[$previous_hook];
-        unset($incomplete_previous_hook['incomplete preprocess functions']);
+      if (!$incomplete_previous_hook) {
+        if (isset($cache[$previous_hook]) && $cache[$previous_hook]->hasIncompletePreprocessFunctions()) {
+          $incomplete_previous_hook = $cache[$previous_hook];
+          $incomplete_previous_hook->markPreprocessFunctionsComplete();
+        }
+        else {
+          $incomplete_previous_hook = ThemeHook::create();
+        }
       }
       $previous_hook = substr($previous_hook, 0, $pos);
-      $this->mergePreprocessFunctions($hook, $previous_hook, $incomplete_previous_hook, $cache);
+      $this->mergePreprocessFunctions($hook, $previous_hook, $cache, $incomplete_previous_hook);
     }
 
     // In addition to processing suggestions, include base hooks.
-    if (isset($cache[$hook]['base hook'])) {
+    if ($cache[$hook]->getBaseHook()) {
       // In order to retain the additions from above, pass in the current hook
       // as the parent hook, otherwise it will be overwritten.
-      $this->mergePreprocessFunctions($hook, $cache[$hook]['base hook'], $cache[$hook], $cache);
+      $this->mergePreprocessFunctions($hook, $cache[$hook]->getBaseHook(), $cache, $cache[$hook]);
     }
+
+    // @todo.
+    $cache[$hook]->markPreprocessFunctionsComplete();
   }
 
   /**
@@ -636,26 +644,23 @@ protected function completeSuggestion($hook, array &$cache) {
    *   The name of the hook to merge preprocess functions to.
    * @param string $source_hook_name
    *   The name of the hook to merge preprocess functions from.
-   * @param array $parent_hook
+   * @param \Drupal\Core\Theme\ThemeHook[] $cache
+   *   The theme registry, as documented in
+   *   \Drupal\Core\Theme\Registry::processExtension().
+   * @param \Drupal\Core\Theme\ThemeHook $parent_hook
    *   The parent hook if it exists. Either an incomplete hook from suggestions
    *   or a base hook.
-   * @param array $cache
-   *   The theme registry, as documented in
-   *   \Drupal\Core\Theme\Registry::processExtension().
    */
-  protected function mergePreprocessFunctions($destination_hook_name, $source_hook_name, $parent_hook, array &$cache) {
+  protected function mergePreprocessFunctions($destination_hook_name, $source_hook_name, array &$cache, ThemeHook $parent_hook) {
     // If base hook exists clone of it for the preprocess function
     // without a template.
     // @see https://www.drupal.org/node/2457295
-    if (isset($cache[$source_hook_name]) && (!isset($cache[$source_hook_name]['incomplete preprocess functions']) || !isset($cache[$destination_hook_name]['incomplete preprocess functions']))) {
-      $cache[$destination_hook_name] = $parent_hook + $cache[$source_hook_name];
-      if (isset($parent_hook['preprocess functions'])) {
-        $diff = array_diff($parent_hook['preprocess functions'], $cache[$source_hook_name]['preprocess functions']);
-        $cache[$destination_hook_name]['preprocess functions'] = array_merge($cache[$source_hook_name]['preprocess functions'], $diff);
-      }
+    if (isset($cache[$source_hook_name]) && (!$cache[$source_hook_name]->hasIncompletePreprocessFunctions() || !isset($cache[$destination_hook_name]) || !$cache[$destination_hook_name]->hasIncompletePreprocessFunctions())) {
+      $cache[$destination_hook_name] = $parent_hook->merge($cache[$source_hook_name]);
+
       // If a base hook isn't set, this is the actual base hook.
-      if (!isset($cache[$source_hook_name]['base hook'])) {
-        $cache[$destination_hook_name]['base hook'] = $source_hook_name;
+      if (!$cache[$destination_hook_name]->getBaseHook()) {
+        $cache[$destination_hook_name]->setBaseHook($source_hook_name);
       }
     }
   }
@@ -663,7 +668,7 @@ protected function mergePreprocessFunctions($destination_hook_name, $source_hook
   /**
    * Completes the theme registry adding discovered functions and hooks.
    *
-   * @param array $cache
+   * @param \Drupal\Core\Theme\ThemeHook[] $cache
    *   The theme registry as documented in
    *   \Drupal\Core\Theme\Registry::processExtension().
    * @param \Drupal\Core\Theme\ActiveTheme $theme
@@ -676,6 +681,7 @@ protected function postProcessExtension(array &$cache, ActiveTheme $theme) {
     // expected naming conventions.
     $prefixes = array_keys((array) $this->moduleHandler->getModuleList());
     foreach (array_reverse($theme->getBaseThemes()) as $base) {
+      /** @var \Drupal\Core\Theme\ActiveTheme $base */
       $prefixes[] = $base->getName();
     }
     if ($theme->getEngine()) {
@@ -718,16 +724,20 @@ protected function postProcessExtension(array &$cache, ActiveTheme $theme) {
     ksort($suggestion_level);
     foreach ($suggestion_level as $level => $item) {
       foreach ($item as $preprocessor => $hook) {
-        if (isset($cache[$hook]['preprocess functions']) && !in_array($hook, $cache[$hook]['preprocess functions'])) {
+        if (isset($cache[$hook]) && !$cache[$hook]->hasPreprocessFunction($hook)) {
           // Add missing preprocessor to existing hook.
-          $cache[$hook]['preprocess functions'][] = $preprocessor;
+          $cache[$hook]->addPreprocessFunction($preprocessor);
         }
         elseif (!isset($cache[$hook]) && strpos($hook, '__')) {
           // Process non-existing hook and register it.
           // Look for a previously defined hook that is either a less specific
           // suggestion hook or the base hook.
           $this->completeSuggestion($hook, $cache);
-          $cache[$hook]['preprocess functions'][] = $preprocessor;
+          // @todo.
+          if (!isset($cache[$hook])) {
+            throw new \Exception($hook);
+          }
+          $cache[$hook]->addPreprocessFunction($preprocessor);
         }
       }
     }
@@ -738,18 +748,9 @@ protected function postProcessExtension(array &$cache, ActiveTheme $theme) {
       // The 'base hook' is only applied to derivative hooks already registered
       // from a pattern. This is typically set from
       // drupal_find_theme_functions() and drupal_find_theme_templates().
-      if (isset($info['incomplete preprocess functions'])) {
+      if ($info->hasIncompletePreprocessFunctions()) {
         $this->completeSuggestion($hook, $cache);
-        unset($cache[$hook]['incomplete preprocess functions']);
-      }
-
-      // Optimize the registry.
-      if (isset($cache[$hook]['preprocess functions']) && empty($cache[$hook]['preprocess functions'])) {
-        unset($cache[$hook]['preprocess functions']);
-      }
-      // Ensure uniqueness.
-      if (isset($cache[$hook]['preprocess functions'])) {
-        $cache[$hook]['preprocess functions'] = array_unique($cache[$hook]['preprocess functions']);
+        $info->markPreprocessFunctionsComplete();
       }
     }
   }
@@ -783,12 +784,13 @@ public function destruct() {
   /**
    * Gets all user functions grouped by the word before the first underscore.
    *
-   * @param $prefixes
+   * @param array $prefixes
    *   An array of function prefixes by which the list can be limited.
+   *
    * @return array
    *   Functions grouped by the first prefix.
    */
-  public function getPrefixGroupedUserFunctions($prefixes = []) {
+  public function getPrefixGroupedUserFunctions(array $prefixes = []) {
     $functions = get_defined_functions();
 
     // If a list of prefixes is supplied, trim down the list to those items
@@ -817,6 +819,7 @@ public function getPrefixGroupedUserFunctions($prefixes = []) {
    *   The name of the item for which the path is requested.
    *
    * @return string
+   *   The path to the requested module.
    */
   protected function getPath($module) {
     return drupal_get_path('module', $module);
diff --git a/core/lib/Drupal/Core/Theme/ThemeHook.php b/core/lib/Drupal/Core/Theme/ThemeHook.php
new file mode 100644
index 0000000..96d06e2
--- /dev/null
+++ b/core/lib/Drupal/Core/Theme/ThemeHook.php
@@ -0,0 +1,668 @@
+<?php
+
+namespace Drupal\Core\Theme;
+
+/**
+ * @todo.
+ */
+class ThemeHook implements \ArrayAccess {
+
+  /**
+   * Where the theme hook is defined: 'module', 'theme_engine', or 'theme'.
+   *
+   * This is automatically derived and does not need to be specified.
+   *
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * The directory path of the theme or module, so that it doesn't need to be looked up.
+   *
+   * This is automatically derived and does not need to be specified.
+   *
+   * @var string
+   */
+  protected $theme_path;
+
+  /**
+   * Only used for #theme in render array: an array of variables, where the
+   * array keys are the names of the variables, and the array values are the
+   * default values if they are not given in the render array. Template
+   * implementations receive each array key as a variable in the template file
+   * (so they must be legal PHP/Twig variable names). Function implementations
+   * are passed the variables in a single $variables function argument. If you
+   * are using these variables in a render array, prefix the variable names
+   * defined here with a #.
+   *
+   * @var mixed[]|null
+   */
+  protected $variables;
+
+  /**
+   * Used for render element items only: the name of the renderable element or
+   * element tree to pass to the theme function. This name is used as the name
+   * of the variable that holds the renderable element or tree in preprocess and
+   * process functions.
+   *
+   * @var string
+   */
+  protected $render_element;
+
+  /**
+   * A regular expression pattern to be used to allow this theme
+   * implementation to have a dynamic name. The convention is to use __ to
+   * differentiate the dynamic portion of the theme. For example, to allow
+   * forums to be themed individually, the pattern might be: 'forum__'. Then,
+   * when the forum is rendered, following render array can be used:
+   *   @code
+   *   $render_array = array(
+   *     '#theme' => array('forum__' . $tid, 'forum'),
+   *     '#forum' => $forum,
+   *   );
+   *   @endcode
+   *
+   * @var string
+   */
+  protected $pattern;
+
+  /**
+   * Used for theme suggestions only: the base theme hook name.
+   *
+   * Instead of this suggestion's implementation being used directly, the base
+   * hook will be invoked with this implementation as its first suggestion. The
+   * base hook's files will be included and the base hook's preprocess functions
+   * will be called in addition to any suggestion's preprocess functions. If an
+   * implementation of hook_theme_suggestions_HOOK() (where HOOK is the base
+   * hook) changes the suggestion order, a different suggestion may be used in
+   * place of this suggestion. If after hook_theme_suggestions_HOOK() this
+   * suggestion remains the first suggestion, then this suggestion's function or
+   * template will be used to generate the rendered output.
+   *
+   * @var string
+   */
+  protected $base_hook;
+
+  /**
+   * @var string[]
+   */
+  protected $includes = [];
+
+  /**
+   * The file the implementation resides in. This file will be included prior to
+   * the theme being rendered, to make sure that the function or preprocess
+   * function (as needed) is actually loaded.
+   *
+   * @var string
+   */
+  protected $file;
+
+  /**
+   * If specified, this will be the function name to invoke for this
+   * implementation. If neither 'template' nor 'function' are specified, a
+   * default template name will be assumed. See above for more details.
+   *
+   * @deprecated in Drupal 8.0.x, will be removed in Drupal 9.0.x.
+   *
+   * @var string
+   */
+  protected $function;
+
+  /**
+   * If specified, the theme implementation is a template file, and this is the
+   * template name. Do not add 'html.twig' on the end of the template name. The
+   * extension will be added automatically by the default rendering engine
+   * (which is Twig.) If 'path' is specified, 'template' should also be
+   * specified. If neither 'template' nor 'function' are specified, a default
+   * template name will be assumed. For example, if a module registers the
+   * 'search_result' theme hook, 'search-result' will be assigned as its
+   * template name.
+   *
+   * @var string
+   */
+  protected $template;
+
+  /**
+   * Override the path of the file to be used. Ordinarily the module or theme
+   * path will be used, but if the file will not be in the default path, include
+   * it here. This path should be relative to the Drupal root directory.
+   *
+   * @var string
+   */
+  protected $path;
+
+  /**
+   * A list of functions used to preprocess this data.
+   *
+   * Ordinarily this won't be used; it's automatically filled in. By default,
+   * for a module this will be filled in as template_preprocess_HOOK. For a
+   * theme this will be filled in as twig_preprocess and twig_preprocess_HOOK as
+   * well as themename_preprocess and themename_preprocess_HOOK.
+   *
+   * @var string[]
+   */
+  protected $preprocess_functions = [];
+
+  /**
+   * @var bool
+   */
+  protected $incomplete_preprocess_functions = FALSE;
+
+  /**
+   * Set to TRUE when a theme does NOT want the standard preprocess functions to run.
+   *
+   * This can be used to give a theme FULL control over how variables are set.
+   * For example, if a theme wants total control over how certain variables in
+   * the page.html.twig are set, this can be set to true. Please keep in mind
+   * that when this is used by a theme, that theme becomes responsible for
+   * making sure necessary variables are set.
+   *
+   * @var bool
+   */
+  protected $override_preprocess_functions = FALSE;
+
+  /**
+   * Stores the attribute data.
+   *
+   * @var \Drupal\Core\Template\AttributeValueBase[]
+   */
+  protected $storage = [];
+
+  /**
+   * Constructs a \Drupal\Core\Template\Attribute object.
+   *
+   * @param array $attributes
+   *   An associative array of key-value pairs to be converted to attributes.
+   */
+  protected function __construct($attributes = []) {
+    foreach ($attributes as $name => $value) {
+      $this->offsetSet($name, $value);
+    }
+  }
+
+  /**
+   * @todo.
+   */
+  public static function create() {
+    return new static();
+  }
+
+  /**
+   * @todo.
+   *
+   * @deprecated
+   */
+  public static function createFromLegacy(array $info) {
+    return new static($info);
+  }
+
+  /**
+   * @return $this
+   */
+  public function merge(ThemeHook $other) {
+    $result = clone $this;
+    // If this object doesn't have a value, use the value from the other object.
+    if (!$result->getType()) {
+      $result->setType($other->getType());
+    }
+    if (!$result->getFile()) {
+      $result->setFile($other->getFile());
+    }
+    if (!$result->getThemePath()) {
+      $result->setThemePath($other->getThemePath());
+    }
+    if (!$result->getPath()) {
+      $result->setPath($other->getPath());
+    }
+    if (!$result->getOverriddenPreprocessFunctionStatus()) {
+      $result->setOverriddenPreprocessFunctionStatus($other->getOverriddenPreprocessFunctionStatus());
+    }
+
+    // @todo.
+    if (!$result->getFunction() && !$result->getTemplate()) {
+      $result->setTemplate($other->getTemplate());
+      $result->setFunction($other->getFunction());
+    }
+
+    // @todo.
+    if ($other->hasIncompletePreprocessFunctions()) {
+      $result->markPreprocessFunctionsIncomplete();
+    }
+
+    // @todo.
+    $result->setIncludes(array_merge($result->getIncludes(), $other->getIncludes()));
+
+    // @todo.
+    $result->mergeVariables($other);
+    $result->mergeRenderElement($other);
+    $result->mergePattern($other);
+    $result->mergeBaseHook($other);
+
+    $result->mergePreprocessFunctions($other);
+
+    return $result;
+  }
+  public function mergeVariables(ThemeHook $other) {
+    if ($other->getVariables() && !$this->getVariables()) {
+      $this->setVariables($other->getVariables());
+    }
+  }
+  public function mergePattern(ThemeHook $other) {
+    if (!$this->getPattern()) {
+      $this->setPattern($other->getPattern());
+    }
+  }
+  public function mergeBaseHook(ThemeHook $other) {
+    if (!$this->getBaseHook()) {
+      $this->setBaseHook($other->getBaseHook());
+    }
+  }
+  public function mergeRenderElement(ThemeHook $other) {
+    if ($other->getRenderElement() && !$this->getRenderElement()) {
+      $this->setRenderElement($other->getRenderElement());
+    }
+  }
+  public function mergePreprocessFunctions(ThemeHook $other) {
+    $this->setPreprocessFunctions(array_merge($this->getPreprocessFunctions(), $other->getPreprocessFunctions()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &offsetGet($name) {
+    $name = str_replace(' ', '_', $name);
+    if (property_exists($this, $name)) {
+      $value = &$this->{$name};
+    }
+    else {
+      // @todo.
+      throw new \Exception($name);
+    }
+    return $value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetSet($name, $value) {
+    $name = str_replace(' ', '_', $name);
+    if (property_exists($this, $name)) {
+      $this->{$name} = $value;
+    }
+    else {
+      // @todo.
+      throw new \Exception($name);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetUnset($name) {
+    $name = str_replace(' ', '_', $name);
+    if (property_exists($this, $name)) {
+      $reflection = new \ReflectionClass($this);
+      $this->{$name} = $reflection->getDefaultProperties()[$name];
+    }
+    else {
+      // @todo.
+      throw new \Exception($name);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetExists($name) {
+    $name = str_replace(' ', '_', $name);
+    if (property_exists($this, $name)) {
+      return isset($this->{$name});
+    }
+    else {
+      // @todo.
+      throw new \Exception($name);
+    }
+  }
+
+  /**
+   * @return mixed
+   */
+  public function getThemePath() {
+    return $this->theme_path;
+  }
+
+  /**
+   * @todo.
+   *
+   * @param string $theme_path
+   *
+   * @return $this
+   */
+  public function setThemePath($theme_path) {
+    $this->theme_path = $theme_path;
+
+    return $this;
+  }
+
+  /**
+   * @todo.
+   *
+   * @return string
+   */
+  public function getType() {
+    return $this->type;
+  }
+
+  /**
+   * @todo.
+   *
+   * @param string $type
+   */
+  public function setType($type) {
+    $this->type = $type;
+
+    return $this;
+  }
+
+  /**
+   * @todo.
+   *
+   * @return mixed[]|null
+   */
+  public function getVariables() {
+    return $this->variables;
+  }
+
+  /**
+   * @todo.
+   *
+   * @param mixed[] $variables
+   *
+   * @return $this
+   */
+  public function setVariables(array $variables) {
+    $this->variables = $variables;
+
+    return $this;
+  }
+
+  /**
+   * @todo.
+   *
+   * @return string
+   */
+  public function getRenderElement() {
+    return $this->render_element;
+  }
+
+  /**
+   * @todo.
+   *
+   * @param string $render_element
+   *
+   * @return $this
+   */
+  public function setRenderElement($render_element) {
+    $this->render_element = $render_element;
+
+    return $this;
+  }
+
+  /**
+   * @todo.
+   *
+   * @return string
+   */
+  public function getPattern() {
+    return $this->pattern;
+  }
+
+  /**
+   * @todo.
+   *
+   * @param string $pattern
+   *
+   * @return $this
+   */
+  public function setPattern($pattern) {
+    $this->pattern = $pattern;
+
+    return $this;
+  }
+
+  /**
+   * @todo.
+   *
+   * @return string
+   */
+  public function getBaseHook() {
+    return $this->base_hook;
+  }
+
+  /**
+   * @todo.
+   *
+   * @param string $base_hook
+   *
+   * @return $this
+   */
+  public function setBaseHook($base_hook) {
+    $this->base_hook = $base_hook;
+
+    return $this;
+  }
+
+  /**
+   * @return string[]
+   */
+  public function getIncludes() {
+    return $this->includes;
+  }
+
+  /**
+   * @param string[] $includes
+   *
+   * @return $this
+   */
+  public function setIncludes(array $includes) {
+    $this->includes = $includes;
+
+    return $this;
+  }
+
+  /**
+   * @param string $include
+   *
+   * @return $this
+   */
+  public function addInclude($include) {
+    $includes = $this->getIncludes();
+    $includes[] = $include;
+    $this->setIncludes($includes);
+
+    return $this;
+  }
+
+  /**
+   * @return bool
+   */
+  public function hasIncludes() {
+    return !empty($this->includes);
+  }
+
+  /**
+   * @return string
+   */
+  public function getFile() {
+    return $this->file;
+  }
+
+  /**
+   * @param string $file
+   *
+   * @return $this
+   */
+  public function setFile($file) {
+    $this->file = $file;
+
+    return $this;
+  }
+
+  /**
+   * @return string
+   */
+  public function getFunction() {
+    return $this->function;
+  }
+
+  /**
+   * @param string $function
+   *
+   * @return $this
+   */
+  public function setFunction($function) {
+    $this->function = $function;
+
+    return $this;
+  }
+
+  /**
+   * @return string
+   */
+  public function getTemplate() {
+    return $this->template;
+  }
+
+  /**
+   * @param string $template
+   *
+   * @return $this
+   */
+  public function setTemplate($template) {
+    $this->template = $template;
+
+    return $this;
+  }
+
+  /**
+   * @return string
+   */
+  public function getPath() {
+    return $this->path;
+  }
+
+  /**
+   * @param string $path
+   *
+   * @return $this
+   */
+  public function setPath($path) {
+    $this->path = $path;
+
+    return $this;
+  }
+
+  /**
+   * @return string[]
+   */
+  public function getPreprocessFunctions() {
+    return $this->preprocess_functions;
+  }
+
+  /**
+   * @param string[] $preprocess_functions
+   *
+   * @return $this
+   */
+  public function setPreprocessFunctions(array $preprocess_functions) {
+    $this->preprocess_functions = array_values(array_unique($preprocess_functions));
+
+    return $this;
+  }
+
+  /**
+   * @param string $preprocess_function
+   *
+   * @return bool
+   */
+  public function hasPreprocessFunction($preprocess_function) {
+    return in_array($preprocess_function, $this->getPreprocessFunctions());
+  }
+
+  /**
+   * @param string $preprocess_function
+   *
+   * @return $this
+   */
+  public function addPreprocessFunction($preprocess_function) {
+    $preprocess_functions = $this->getPreprocessFunctions();
+    $preprocess_functions[] = $preprocess_function;
+    $this->setPreprocessFunctions($preprocess_functions);
+
+    return $this;
+  }
+
+  /**
+   * @return bool
+   */
+  public function getOverriddenPreprocessFunctionStatus() {
+    return $this->override_preprocess_functions;
+  }
+
+  /**
+   * @return $this
+   */
+  public function setOverriddenPreprocessFunctionStatus($status) {
+    $this->override_preprocess_functions = (bool) $status;
+
+    return $this;
+  }
+
+  /**
+   * @return bool
+   */
+  public function hasIncompletePreprocessFunctions() {
+    return $this->incomplete_preprocess_functions;
+  }
+
+  /**
+   * @return $this
+   */
+  public function markPreprocessFunctionsIncomplete() {
+    $this->incomplete_preprocess_functions = TRUE;
+
+    return $this;
+  }
+
+  /**
+   * @return $this
+   */
+  public function markPreprocessFunctionsComplete() {
+    $this->incomplete_preprocess_functions = FALSE;
+
+    return $this;
+  }
+
+  /**
+   * @param string $name
+   *
+   * @return $this
+   */
+  public function setFlag($name) {
+    $this->flag[$name] = TRUE;
+
+    return $this;
+  }
+
+  /**
+   * @param string $name
+   *
+   * @return $this
+   */
+  public function unsetFlag($name) {
+    unset($this->flag[$name]);
+
+    return $this;
+  }
+
+}
diff --git a/core/modules/locale/tests/modules/locale_test/locale_test.module b/core/modules/locale/tests/modules/locale_test/locale_test.module
index 89ba2ba..9059e9c 100644
--- a/core/modules/locale/tests/modules/locale_test/locale_test.module
+++ b/core/modules/locale/tests/modules/locale_test/locale_test.module
@@ -154,7 +154,7 @@ function locale_test_theme() {
   $return = [];
 
   $return['locale_test_tokenized'] = [
-    'variable' => ['content' => ''],
+    'variables' => ['content' => ''],
   ];
 
   return $return;
diff --git a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php
index f2cd22e..0453502 100644
--- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php
@@ -7,9 +7,17 @@
 
 namespace Drupal\Tests\Core\Theme;
 
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Drupal\Core\Lock\LockBackendInterface;
 use Drupal\Core\Theme\ActiveTheme;
 use Drupal\Core\Theme\Registry;
+use Drupal\Core\Theme\ThemeHook;
+use Drupal\Core\Theme\ThemeInitializationInterface;
+use Drupal\Core\Theme\ThemeManagerInterface;
 use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
 
 /**
  * @coversDefaultClass \Drupal\Core\Theme\Registry
@@ -25,20 +33,6 @@ class RegistryTest extends UnitTestCase {
   protected $registry;
 
   /**
-   * The mocked cache backend.
-   *
-   * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
-   */
-  protected $cache;
-
-  /**
-   * The mocked lock backend.
-   *
-   * @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit_Framework_MockObject_MockObject
-   */
-  protected $lock;
-
-  /**
    * The mocked module handler.
    *
    * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
@@ -46,23 +40,9 @@ class RegistryTest extends UnitTestCase {
   protected $moduleHandler;
 
   /**
-   * The mocked theme handler.
-   *
-   * @var \Drupal\Core\Extension\ThemeHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
-   */
-  protected $themeHandler;
-
-  /**
-   * The mocked theme initialization.
-   *
-   * @var \Drupal\Core\Theme\ThemeInitializationInterface|\PHPUnit_Framework_MockObject_MockObject
-   */
-  protected $themeInitialization;
-
-  /**
    * The theme manager.
    *
-   * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   * @var \Drupal\Core\Theme\ThemeManagerInterface|\Prophecy\Prophecy\ProphecyInterface
    */
   protected $themeManager;
 
@@ -79,14 +59,16 @@ class RegistryTest extends UnitTestCase {
   protected function setUp() {
     parent::setUp();
 
-    $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
-    $this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface');
-    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
-    $this->themeHandler = $this->getMock('Drupal\Core\Extension\ThemeHandlerInterface');
-    $this->themeInitialization = $this->getMock('Drupal\Core\Theme\ThemeInitializationInterface');
-    $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
+    $cache = $this->prophesize(CacheBackendInterface::class);
+    $lock = $this->prophesize(LockBackendInterface::class);
+    $theme_handler = $this->prophesize(ThemeHandlerInterface::class);
+    $theme_initialization = $this->prophesize(ThemeInitializationInterface::class);
 
-    $this->setupTheme();
+    $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
+    $this->themeManager = $this->prophesize(ThemeManagerInterface::class);
+
+    $this->registry = new TestRegistry($this->root, $cache->reveal(), $lock->reveal(), $this->moduleHandler->reveal(), $theme_handler->reveal(), $theme_initialization->reveal());
+    $this->registry->setThemeManager($this->themeManager->reveal());
   }
 
   /**
@@ -127,20 +109,16 @@ public function testGetRegistryForModule() {
       'base_themes' => [],
     ]);
 
-    $this->themeManager->expects($this->exactly(2))
-      ->method('getActiveTheme')
-      ->willReturnOnConsecutiveCalls($test_theme, $test_stable);
+    $this->themeManager->getActiveTheme()->willReturn($test_theme);
+    $this->themeManager->alterForTheme($test_theme, 'theme_registry', Argument::any())->shouldBeCalled();
 
     // Include the module and theme files so that hook_theme can be called.
     include_once $this->root . '/core/modules/system/tests/modules/theme_test/theme_test.module';
     include_once $this->root . '/core/modules/system/tests/themes/test_stable/test_stable.theme';
-    $this->moduleHandler->expects($this->exactly(2))
-      ->method('getImplementations')
-      ->with('theme')
-      ->will($this->returnValue(['theme_test']));
-    $this->moduleHandler->expects($this->atLeastOnce())
-      ->method('getModuleList')
-      ->willReturn([]);
+    $this->moduleHandler->getImplementations('theme')->willReturn(['theme_test']);
+    $this->moduleHandler->getModuleList()->willReturn([]);
+    $this->moduleHandler->isLoaded()->willReturn(TRUE);
+    $this->moduleHandler->alter('theme_registry', Argument::any())->shouldBeCalled();
 
     $registry = $this->registry->get();
 
@@ -169,6 +147,9 @@ public function testGetRegistryForModule() {
     // The second call will initialize with the second theme. Ensure that this
     // returns a different object and the discovery for the second theme's
     // preprocess function worked.
+    $this->themeManager->getActiveTheme()->willReturn($test_stable);
+    $this->themeManager->alterForTheme($test_stable, 'theme_registry', Argument::any())->shouldBeCalled();
+
     $other_registry = $this->registry->get();
     $this->assertNotSame($registry, $other_registry);
     $this->assertTrue(in_array('test_stable_preprocess_theme_test_render_element', $other_registry['theme_test_render_element']['preprocess functions']));
@@ -191,14 +172,23 @@ public function testGetRegistryForModule() {
   public function testPostProcessExtension($defined_functions, $hooks, $expected) {
     static::$functions['user'] = $defined_functions;
 
+    foreach ($expected as $name => $hook) {
+      if (is_array($hook)) {
+        $expected[$name] = ThemeHook::createFromLegacy($hook);
+      }
+    }
+    foreach ($hooks as $name => $hook) {
+      if (is_array($hook)) {
+        $hooks[$name] = ThemeHook::createFromLegacy($hook);
+      }
+    }
+
     $theme = $this->prophesize(ActiveTheme::class);
     $theme->getBaseThemes()->willReturn([]);
     $theme->getName()->willReturn('test');
     $theme->getEngine()->willReturn('twig');
 
-    $this->moduleHandler->expects($this->atLeastOnce())
-      ->method('getModuleList')
-      ->willReturn([]);
+    $this->moduleHandler->getModuleList()->willReturn([]);
 
     $class = new \ReflectionClass(TestRegistry::class);
     $reflection_method = $class->getMethod('postProcessExtension');
@@ -485,11 +475,6 @@ public function providerTestPostProcessExtension() {
     return $data;
   }
 
-  protected function setupTheme() {
-    $this->registry = new TestRegistry($this->root, $this->cache, $this->lock, $this->moduleHandler, $this->themeHandler, $this->themeInitialization);
-    $this->registry->setThemeManager($this->themeManager);
-  }
-
 }
 
 class TestRegistry extends Registry {
