content: // Copyright 2016 Google Inc. Use of this source code is governed by an
  // MIT-style license that can be found in the LICENSE file or at
  // https://opensource.org/licenses/MIT.
  
  import 'dart:async';
  import 'dart:collection';
  import 'dart:math' as math;
  
  import 'package:cli_pkg/js.dart';
  import 'package:charcode/charcode.dart';
  import 'package:collection/collection.dart';
  import 'package:path/path.dart' as p;
  import 'package:source_span/source_span.dart';
  import 'package:stack_trace/stack_trace.dart';
  
  import '../ast/css.dart';
  import '../ast/css/modifiable.dart';
  import '../ast/node.dart';
  import '../ast/sass.dart';
  import '../ast/selector.dart';
  import '../async_environment.dart';
  import '../async_import_cache.dart';
  import '../callable.dart';
  import '../color_names.dart';
  import '../configuration.dart';
  import '../configured_value.dart';
  import '../deprecation.dart';
  import '../evaluation_context.dart';
  import '../exception.dart';
  import '../extend/extension_store.dart';
  import '../extend/extension.dart';
  import '../functions.dart';
  import '../functions/meta.dart' as meta;
  import '../importer.dart';
  import '../importer/legacy_node.dart';
  import '../interpolation_map.dart';
  import '../logger.dart';
  import '../module.dart';
  import '../module/built_in.dart';
  import '../parse/keyframe_selector.dart';
  import '../syntax.dart';
  import '../utils.dart';
  import '../util/character.dart';
  import '../util/map.dart';
  import '../util/multi_span.dart';
  import '../util/nullable.dart';
  import '../util/span.dart';
  import '../value.dart';
  import 'expression_to_calc.dart';
  import 'interface/css.dart';
  import 'interface/expression.dart';
  import 'interface/modifiable_css.dart';
  import 'interface/statement.dart';
  import 'serialize.dart';
  
  /// A function that takes a callback with no arguments.
  typedef _ScopeCallback = Future<void> Function(
      Future<void> Function() callback);
  
  /// Converts [stylesheet] to a plain CSS tree.
  ///
  /// If [importCache] (or, on Node.js, [nodeImporter]) is passed, it's used to
  /// resolve imports in the Sass files.
  ///
  /// If [importer] is passed, it's used to resolve relative imports in
  /// [stylesheet] relative to `stylesheet.span.sourceUrl`.
  ///
  /// The [functions] are available as global functions when evaluating
  /// [stylesheet].
  ///
  /// The [variables] are available as global variables when evaluating
  /// [stylesheet].
  ///
  /// Warnings are emitted using [logger], or printed to standard error by
  /// default.
  ///
  /// If [sourceMap] is `true`, this will track the source locations of variable
  /// declarations.
  ///
  /// Throws a [SassRuntimeException] if evaluation fails.
  Future<EvaluateResult> evaluateAsync(
    Stylesheet stylesheet, {
    AsyncImportCache? importCache,
    NodeImporter? nodeImporter,
    AsyncImporter? importer,
    Iterable<AsyncCallable>? functions,
    Logger? logger,
    bool quietDeps = false,
    bool sourceMap = false,
  }) =>
      _EvaluateVisitor(
        importCache: importCache,
        nodeImporter: nodeImporter,
        functions: functions,
        logger: logger,
        quietDeps: quietDeps,
        sourceMap: sourceMap,
      ).run(importer, stylesheet);
  
  /// A class that can evaluate multiple independent statements and expressions
  /// in the context of a single module.
  final class AsyncEvaluator {
    /// The visitor that evaluates each expression and statement.
    final _EvaluateVisitor _visitor;
  
    /// The importer to use to resolve `@use` rules in [_visitor].
    final AsyncImporter? _importer;
  
    /// Creates an evaluator.
    ///
    /// Arguments are the same as for [evaluateAsync].
    AsyncEvaluator({
      AsyncImportCache? importCache,
      AsyncImporter? importer,
      Iterable<AsyncCallable>? functions,
      Logger? logger,
    })  : _visitor = _EvaluateVisitor(
            importCache: importCache,
            functions: functions,
            logger: logger,
          ),
          _importer = importer;
  
    Future<void> use(UseRule use) => _visitor.runStatement(_importer, use);
  
    Future<Value> evaluate(Expression expression) =>
        _visitor.runExpression(_importer, expression);
  
    Future<void> setVariable(VariableDeclaration declaration) =>
        _visitor.runStatement(_importer, declaration);
  }
  
  /// A visitor that executes Sass code to produce a CSS tree.
  final class _EvaluateVisitor
      implements
          StatementVisitor<Future<Value?>>,
          ExpressionVisitor<Future<Value>>,
          CssVisitor<Future<void>> {
    /// The import cache used to import other stylesheets.
    final AsyncImportCache? _importCache;
  
    /// The Node Sass-compatible importer to use when loading new Sass files when
    /// compiled to Node.js.
    final NodeImporter? _nodeImporter;
  
    /// Built-in functions that are globally-accessible, even under the new module
    /// system.
    final _builtInFunctions = <String, AsyncCallable>{};
  
    /// Built in modules, indexed by their URLs.
    final _builtInModules = <Uri, Module>{};
  
    /// All modules that have been loaded and evaluated so far.
    final _modules = <Uri, Module>{};
  
    /// The first [Configuration] used to load a module [Uri].
    final _moduleConfigurations = <Uri, Configuration>{};
  
    /// A map from canonical module URLs to the nodes whose spans indicate where
    /// those modules were originally loaded.
    ///
    /// This is not guaranteed to have a node for every module in [_modules]. For
    /// example, the entrypoint module was not loaded by a node.
    final _moduleNodes = <Uri, AstNode>{};
  
    /// The logger to use to print warnings.
    final Logger _logger;
  
    /// A set of message/location pairs for warnings that have been emitted via
    /// [_warn].
    ///
    /// We only want to emit one warning per location, to avoid blowing up users'
    /// consoles with redundant warnings.
    final _warningsEmitted = <(String, SourceSpan)>{};
  
    /// Whether to avoid emitting warnings for files loaded from dependencies.
    final bool _quietDeps;
  
    /// Whether to track source map information.
    final bool _sourceMap;
  
    /// The unique compile context for tracking if [SassFunction]s and
    /// [SassMixin]s belongs to the current compilation or not.
    final Object _compileContext = Object();
  
    /// The current lexical environment.
    AsyncEnvironment _environment;
  
    /// The style rule that defines the current parent selector, if any.
    ///
    /// This doesn't take into consideration any intermediate `@at-root` rules. In
    /// the common case where those rules are relevant, use [_styleRule] instead.
    ModifiableCssStyleRule? _styleRuleIgnoringAtRoot;
  
    /// The current media queries, if any.
    List<CssMediaQuery>? _mediaQueries;
  
    /// The set of media queries that were merged together to create
    /// [_mediaQueries].
    ///
    /// This will be non-null if and only if [_mediaQueries] is non-null, but it
    /// will be empty if [_mediaQueries] isn't the result of a merge.
    Set<CssMediaQuery>? _mediaQuerySources;
  
    /// The current parent node in the output CSS tree.
    ModifiableCssParentNode get _parent => _assertInModule(__parent, "__parent");
    set _parent(ModifiableCssParentNode value) => __parent = value;
  
    ModifiableCssParentNode? __parent;
  
    /// The name of the current declaration parent.
    String? _declarationName;
  
    /// The human-readable name of the current stack frame.
    var _member = "root stylesheet";
  
    /// The node for the innermost callable that's being invoked.
    ///
    /// This is used to produce warnings for function calls. It's stored as an
    /// [AstNode] rather than a [FileSpan] so we can avoid calling [AstNode.span]
    /// if the span isn't required, since some nodes need to do real work to
    /// manufacture a source span.
    AstNode? _callableNode;
  
    /// The span for the current import that's being resolved.
    ///
    /// This is used to produce warnings for importers.
    FileSpan? _importSpan;
  
    /// Whether we're currently executing a function.
    var _inFunction = false;
  
    /// Whether we're currently building the output of an unknown at rule.
    var _inUnknownAtRule = false;
  
    ModifiableCssStyleRule? get _styleRule =>
        _atRootExcludingStyleRule ? null : _styleRuleIgnoringAtRoot;
  
    /// Whether we're directly within an `@at-root` rule that excludes style
    /// rules.
    var _atRootExcludingStyleRule = false;
  
    /// Whether we're currently building the output of a `@keyframes` rule.
    var _inKeyframes = false;
  
    /// Whether we're currently evaluating a [SupportsDeclaration].
    ///
    /// When this is true, calculations will not be simplified.
    var _inSupportsDeclaration = false;
  
    /// The canonical URLs of all stylesheets loaded during compilation.
    final _loadedUrls = <Uri>{};
  
    /// A map from canonical URLs for modules (or imported files) that are
    /// currently being evaluated to AST nodes whose spans indicate the original
    /// loads for those modules.
    ///
    /// Map values may be `null`, which indicates an active module that doesn't
    /// have a source span associated with its original load (such as the
    /// entrypoint module).
    ///
    /// This is used to ensure that we don't get into an infinite load loop.
    final _activeModules = <Uri, AstNode?>{};
  
    /// The dynamic call stack representing function invocations, mixin
    /// invocations, and imports surrounding the current context.
    ///
    /// Each member is a pair of the span where the stack trace starts and the
    /// name of the member being invoked.
    ///
    /// This stores [AstNode]s rather than [FileSpan]s so it can avoid calling
    /// [AstNode.span] if the span isn't required, since some nodes need to do
    /// real work to manufacture a source span.
    final _stack = <(String, AstNode)>[];
  
    /// Whether we're running in Node Sass-compatibility mode.
    bool get _asNodeSass => _nodeImporter != null;
  
    // ## Module-Specific Fields
  
    /// The importer that's currently being used to resolve relative imports.
    ///
    /// If this is `null`, relative imports aren't supported in the current
    /// stylesheet.
    AsyncImporter? _importer;
  
    /// Whether we're in a dependency.
    ///
    /// A dependency is defined as a stylesheet imported by an importer other than
    /// the original. In Node importers, nothing is considered a dependency.
    var _inDependency = false;
  
    /// The stylesheet that's currently being evaluated.
    Stylesheet get _stylesheet => _assertInModule(__stylesheet, "_stylesheet");
    set _stylesheet(Stylesheet value) => __stylesheet = value;
    Stylesheet? __stylesheet;
  
    /// The root stylesheet node.
    ModifiableCssStylesheet get _root => _assertInModule(__root, "_root");
    set _root(ModifiableCssStylesheet value) => __root = value;
    ModifiableCssStylesheet? __root;
  
    /// The first index in [_root.children] after the initial block of CSS
    /// imports.
    int get _endOfImports => _assertInModule(__endOfImports, "_endOfImports");
    set _endOfImports(int value) => __endOfImports = value;
    int? __endOfImports;
  
    /// Plain-CSS imports that didn't appear in the initial block of CSS imports.
    ///
    /// These are added to the initial CSS import block by [visitStylesheet] after
    /// the stylesheet has been fully performed.
    ///
    /// This is `null` unless there are any out-of-order imports in the current
    /// stylesheet.
    List<ModifiableCssImport>? _outOfOrderImports;
  
    /// A map from modules loaded by the current module to loud comments written
    /// in this module that should appear before the loaded module.
    ///
    /// This is `null` unless there are any pre-module comments in the current
    /// stylesheet.
    Map<Module, List<CssComment>>? _preModuleComments;
  
    /// The extension store that tracks extensions and style rules for the current
    /// module.
    ExtensionStore get _extensionStore =>
        _assertInModule(__extensionStore, "_extensionStore");
    set _extensionStore(ExtensionStore value) => __extensionStore = value;
    ExtensionStore? __extensionStore;
  
    /// The configuration for the current module.
    ///
    /// If this is empty, that indicates that the current module is not configured.
    var _configuration = const Configuration.empty();
  
    /// Creates a new visitor.
    ///
    /// Most arguments are the same as those to [evaluateAsync].
    _EvaluateVisitor({
      AsyncImportCache? importCache,
      NodeImporter? nodeImporter,
      Iterable<AsyncCallable>? functions,
      Logger? logger,
      bool quietDeps = false,
      bool sourceMap = false,
    })  : _importCache = importCache ??
              (nodeImporter == null ? AsyncImportCache.none() : null),
          _nodeImporter = nodeImporter,
          _logger = logger ?? const Logger.stderr(),
          _quietDeps = quietDeps,
          _sourceMap = sourceMap,
          // The default environment is overridden in [_execute] for full
          // stylesheets, but for [AsyncEvaluator] this environment is used.
          _environment = AsyncEnvironment() {
      var metaFunctions = [
        // These functions are defined in the context of the evaluator because
        // they need access to the [_environment] or other local state.
        BuiltInCallable.function(
          "global-variable-exists",
          r"$name, $module: null",
          (arguments) {
            var variable = arguments[0].assertString("name");
            var module = arguments[1].realNull?.assertString("module");
            return SassBoolean(
              _environment.globalVariableExists(
                variable.text.replaceAll("_", "-"),
                namespace: module?.text,
              ),
            );
          },
          url: "sass:meta",
        ),
  
        BuiltInCallable.function("variable-exists", r"$name", (arguments) {
          var variable = arguments[0].assertString("name");
          return SassBoolean(
            _environment.variableExists(variable.text.replaceAll("_", "-")),
          );
        }, url: "sass:meta"),
  
        BuiltInCallable.function("function-exists", r"$name, $module: null", (
          arguments,
        ) {
          var variable = arguments[0].assertString("name");
          var module = arguments[1].realNull?.assertString("module");
          return SassBoolean(
            _environment.functionExists(
                  variable.text.replaceAll("_", "-"),
                  namespace: module?.text,
                ) ||
                _builtInFunctions.containsKey(variable.text),
          );
        }, url: "sass:meta"),
  
        BuiltInCallable.function("mixin-exists", r"$name, $module: null", (
          arguments,
        ) {
          var variable = arguments[0].assertString("name");
          var module = arguments[1].realNull?.assertString("module");
          return SassBoolean(
            _environment.mixinExists(
              variable.text.replaceAll("_", "-"),
              namespace: module?.text,
            ),
          );
        }, url: "sass:meta"),
  
        BuiltInCallable.function("content-exists", "", (arguments) {
          if (!_environment.inMixin) {
            throw SassScriptException(
              "content-exists() may only be called within a mixin.",
            );
          }
          return SassBoolean(_environment.content != null);
        }, url: "sass:meta"),
  
        BuiltInCallable.function("module-variables", r"$module", (arguments) {
          var namespace = arguments[0].assertString("module");
          var module = _environment.modules[namespace.text];
          if (module == null) {
            throw 'There is no module with namespace "${namespace.text}".';
          }
  
          return SassMap({
            for (var (name, value) in module.variables.pairs)
              SassString(name): value,
          });
        }, url: "sass:meta"),
  
        BuiltInCallable.function("module-functions", r"$module", (arguments) {
          var namespace = arguments[0].assertString("module");
          var module = _environment.modules[namespace.text];
          if (module == null) {
            throw 'There is no module with namespace "${namespace.text}".';
          }
  
          return SassMap({
            for (var (name, value) in module.functions.pairs)
              SassString(name):
                  SassFunction.withCompileContext(value, _compileContext),
          });
        }, url: "sass:meta"),
  
        BuiltInCallable.function("module-mixins", r"$module", (arguments) {
          var namespace = arguments[0].assertString("module");
          var module = _environment.modules[namespace.text];
          if (module == null) {
            throw 'There is no module with namespace "${namespace.text}".';
          }
  
          return SassMap({
            for (var (name, value) in module.mixins.pairs)
              SassString(name):
                  SassMixin.withCompileContext(value, _compileContext),
          });
        }, url: "sass:meta"),
  
        BuiltInCallable.function(
          "get-function",
          r"$name, $css: false, $module: null",
          (arguments) {
            var name = arguments[0].assertString("name");
            var css = arguments[1].isTruthy;
            var module = arguments[2].realNull?.assertString("module");
  
            if (css) {
              if (module != null) {
                throw r"$css and $module may not both be passed at once.";
              }
              return SassFunction.withCompileContext(
                  PlainCssCallable(name.text), _compileContext);
            }
  
            var callable = _addExceptionSpan(_callableNode!, () {
              var normalizedName = name.text.replaceAll("_", "-");
              var namespace = module?.text;
              var local = _environment.getFunction(
                normalizedName,
                namespace: namespace,
              );
              if (local != null || namespace != null) return local;
              return _builtInFunctions[normalizedName];
            });
            if (callable == null) throw "Function not found: $name";
  
            return SassFunction.withCompileContext(callable, _compileContext);
          },
          url: "sass:meta",
        ),
  
        BuiltInCallable.function("get-mixin", r"$name, $module: null", (
          arguments,
        ) {
          var name = arguments[0].assertString("name");
          var module = arguments[1].realNull?.assertString("module");
  
          var callable = _addExceptionSpan(
            _callableNode!,
            () => _environment.getMixin(
              name.text.replaceAll("_", "-"),
              namespace: module?.text,
            ),
          );
          if (callable == null) throw "Mixin not found: $name";
  
          return SassMixin.withCompileContext(callable, _compileContext);
        }, url: "sass:meta"),
  
        AsyncBuiltInCallable.function("call", r"$function, $args...", (
          arguments,
        ) async {
          var function = arguments[0];
          var args = arguments[1] as SassArgumentList;
  
          var callableNode = _callableNode!;
          var invocation = ArgumentList(
            [],
            {},
            callableNode.span,
            rest: ValueExpression(args, callableNode.span),
            keywordRest: args.keywords.isEmpty
                ? null
                : ValueExpression(
                    SassMap({
                      for (var (name, value) in args.keywords.pairs)
                        SassString(name, quotes: false): value,
                    }),
                    callableNode.span,
                  ),
          );
  
          if (function is SassString) {
            warnForDeprecation(
              "Passing a string to call() is deprecated and will be illegal in "
              "Dart Sass 2.0.0.\n"
              "\n"
              "Recommendation: call(get-function($function))",
              Deprecation.callString,
            );
  
            var callableNode = _callableNode!;
            var expression = FunctionExpression(
              function.text,
              invocation,
              callableNode.span,
            );
            return await expression.accept(this);
          }
  
          var callable = function
              .assertFunction("function")
              .assertCompileContext(_compileContext)
              .callable;
          // ignore: unnecessary_type_check
          if (callable is AsyncCallable) {
            return await _runFunctionCallable(
              invocation,
              callable,
              _callableNode!,
            );
          } else {
            throw SassScriptException(
              "The function ${callable.name} is asynchronous.\n"
              "This is probably caused by a bug in a Sass plugin.",
            );
          }
        }, url: "sass:meta"),
      ];
  
      var metaMixins = [
        AsyncBuiltInCallable.mixin("load-css", r"$url, $with: null", (
          arguments,
        ) async {
          var url = Uri.parse(arguments[0].assertString("url").text);
          var withMap = arguments[1].realNull?.assertMap("with").contents;
  
          var callableNode = _callableNode!;
          var configuration = const Configuration.empty();
          if (withMap != null) {
            var values = <String, ConfiguredValue>{};
            var span = callableNode.span;
            var privateDeprecation = false;
            withMap.forEach((variable, value) {
              var name =
                  variable.assertString("with key").text.replaceAll("_", "-");
              if (values.containsKey(name)) {
                throw "The variable \$$name was configured twice.";
              } else if (name.startsWith("-") && !privateDeprecation) {
                privateDeprecation = true;
                warnForDeprecation(
                  "Configuring private variables (such as \$$name) is "
                  "deprecated.\n"
                  "This will be an error in Dart Sass 2.0.0.",
                  Deprecation.withPrivate,
                );
              }
  
              values[name] = ConfiguredValue.explicit(value, span, callableNode);
            });
            configuration = ExplicitConfiguration(values, callableNode);
          }
  
          await _loadModule(
            url,
            "load-css()",
            callableNode,
            (module, _) => _combineCss(module, clone: true).accept(this),
            baseUrl: callableNode.span.sourceUrl,
            configuration: configuration,
            namesInErrors: true,
          );
          _assertConfigurationIsEmpty(configuration, nameInError: true);
        }, url: "sass:meta"),
        AsyncBuiltInCallable.mixin(
          "apply",
          r"$mixin, $args...",
          (arguments) async {
            var mixin = arguments[0];
            var args = arguments[1] as SassArgumentList;
  
            var callableNode = _callableNode!;
            var invocation = ArgumentList(
              const [],
              const {},
              callableNode.span,
              rest: ValueExpression(args, callableNode.span),
            );
  
            var callable = mixin
                .assertMixin("mixin")
                .assertCompileContext(_compileContext)
                .callable;
            var content = _environment.content;
  
            // ignore: unnecessary_type_check
            if (callable is AsyncCallable) {
              await _applyMixin(
                callable,
                content,
                invocation,
                callableNode,
                callableNode,
              );
            } else {
              throw SassScriptException(
                "The mixin ${callable.name} is asynchronous.\n"
                "This is probably caused by a bug in a Sass plugin.",
              );
            }
          },
          url: "sass:meta",
          acceptsContent: true,
        ),
      ];
  
      var metaModule = BuiltInModule(
        "meta",
        functions: [...meta.moduleFunctions, ...metaFunctions],
        mixins: metaMixins,
      );
  
      for (var module in [...coreModules, metaModule]) {
        _builtInModules[module.url] = module;
      }
  
      functions = [
        ...?functions,
        ...globalFunctions,
        ...[
          for (var function in metaFunctions)
            function.withDeprecationWarning('meta'),
        ],
      ];
      for (var function in functions) {
        _builtInFunctions[function.name.replaceAll("_", "-")] = function;
      }
    }
  
    Future<EvaluateResult> run(AsyncImporter? importer, Stylesheet node) async {
      try {
        return await withEvaluationContext(
          _EvaluationContext(this, node),
          () async {
            if (node.span.sourceUrl case var url?) {
              _activeModules[url] = null;
              if (!(_asNodeSass && url.toString() == 'stdin')) {
                _loadedUrls.add(url);
              }
            }
  
            var module = await _addExceptionTrace(() => _execute(importer, node));
            return (stylesheet: _combineCss(module), loadedUrls: _loadedUrls);
          },
        );
      } on SassException catch (error, stackTrace) {
        throwWithTrace(error.withLoadedUrls(_loadedUrls), error, stackTrace);
      }
    }
  
    Future<Value> runExpression(AsyncImporter? importer, Expression expression) =>
        withEvaluationContext(
          _EvaluationContext(this, expression),
          () => _withFakeStylesheet(
            importer,
            expression,
            () => _addExceptionTrace(() => expression.accept(this)),
          ),
        );
  
    Future<void> runStatement(AsyncImporter? importer, Statement statement) =>
        withEvaluationContext(
          _EvaluationContext(this, statement),
          () => _withFakeStylesheet(
            importer,
            statement,
            () => _addExceptionTrace(() => statement.accept(this)),
          ),
        );
  
    /// Asserts that [value] is not `null` and returns it.
    ///
    /// This is used for fields that are set whenever the evaluator is evaluating
    /// a module, which is to say essentially all the time (unless running via
    /// [runExpression] or [runStatement]).
    T _assertInModule<T>(T? value, String name) {
      if (value != null) return value;
      throw StateError("Can't access $name outside of a module.");
    }
  
    /// Runs [callback] with [importer] as [_importer] and a fake [_stylesheet]
    /// with [nodeWithSpan]'s source span.
    Future<T> _withFakeStylesheet<T>(
      AsyncImporter? importer,
      AstNode nodeWithSpan,
      FutureOr<T> callback(),
    ) async {
      var oldImporter = _importer;
      _importer = importer;
  
      assert(__stylesheet == null);
      _stylesheet = Stylesheet(const [], nodeWithSpan.span);
  
      try {
        return await callback();
      } finally {
        _importer = oldImporter;
        __stylesheet = null;
      }
    }
  
    /// Loads the module at [url] and passes it to [callback], along with a
    /// boolean indicating whether this is the first time the module has been
    /// loaded.
    ///
    /// This first tries loading [url] relative to [baseUrl], which defaults to
    /// `_stylesheet.span.sourceUrl`.
    ///
    /// The [configuration] overrides values for `!default` variables defined in
    /// the module or modules it forwards and/or imports. If it's not passed, the
    /// current configuration is used instead. Throws a [SassRuntimeException] if
    /// a configured variable is not declared with `!default`.
    ///
    /// If [namesInErrors] is `true`, this includes the names of modules or
    /// configured variables in errors relating to them. This should only be
    /// `true` if the names won't be obvious from the source span.
    ///
    /// The [stackFrame] and [nodeWithSpan] are used for the name and location of
    /// the stack frame for the duration of the [callback].
    Future<void> _loadModule(
      Uri url,
      String stackFrame,
      AstNode nodeWithSpan,
      FutureOr<void> callback(Module module, bool firstLoad), {
      Uri? baseUrl,
      Configuration? configuration,
      bool namesInErrors = false,
    }) async {
      if (_builtInModules[url] case var builtInModule?) {
        if (configuration is ExplicitConfiguration) {
          throw _exception(
            namesInErrors
                ? "Built-in module $url can't be configured."
                : "Built-in modules can't be configured.",
            configuration.nodeWithSpan.span,
          );
        }
  
        // Always consider built-in stylesheets to be "already loaded", since they
        // never require additional execution to load and never produce CSS.
        await _addExceptionSpanAsync(
          nodeWithSpan,
          () => callback(builtInModule, false),
        );
        return;
      }
  
      await _withStackFrame(stackFrame, nodeWithSpan, () async {
        var (stylesheet, :importer, :isDependency) = await _loadStylesheet(
          url.toString(),
          nodeWithSpan.span,
          baseUrl: baseUrl,
        );
  
        var canonicalUrl = stylesheet.span.sourceUrl;
        if (canonicalUrl != null) {
          if (_activeModules.containsKey(canonicalUrl)) {
            var message = namesInErrors
                ? "Module loop: ${p.prettyUri(canonicalUrl)} is already being "
                    "loaded."
                : "Module loop: this module is already being loaded.";
  
            throw _activeModules[canonicalUrl].andThen(
                  (previousLoad) => _multiSpanException(message, "new load", {
                    previousLoad.span: "original load",
                  }),
                ) ??
                _exception(message);
          } else {
            _activeModules[canonicalUrl] = nodeWithSpan;
          }
        }
  
        var firstLoad = !_modules.containsKey(canonicalUrl);
        var oldInDependency = _inDependency;
        _inDependency = isDependency;
        Module module;
        try {
          module = await _execute(
            importer,
            stylesheet,
            configuration: configuration,
            nodeWithSpan: nodeWithSpan,
            namesInErrors: namesInErrors,
          );
        } finally {
          _activeModules.remove(canonicalUrl);
          _inDependency = oldInDependency;
        }
  
        await _addExceptionSpanAsync(
          nodeWithSpan,
          () => callback(module, firstLoad),
          addStackFrame: false,
        );
      });
    }
  
    /// Executes [stylesheet], loaded by [importer], to produce a module.
    ///
    /// If [configuration] is not passed, the current configuration is used
    /// instead.
    ///
    /// If [namesInErrors] is `true`, this includes the names of modules in errors
    /// relating to them. This should only be `true` if the names won't be obvious
    /// from the source span.
    Future<Module> _execute(
      AsyncImporter? importer,
      Stylesheet stylesheet, {
      Configuration? configuration,
      AstNode? nodeWithSpan,
      bool namesInErrors = false,
    }) async {
      var url = stylesheet.span.sourceUrl;
  
      var currentConfiguration = configuration ?? _configuration;
      if (_modules[url] case var alreadyLoaded?) {
        if (!_moduleConfigurations[url]!.sameOriginal(currentConfiguration) &&
            currentConfiguration is ExplicitConfiguration &&
            // Don't throw an error if the module being loaded doesn't expose any
            // configurable variables that could have been affected by the
            // configuration in the first place. If the configuration defines a
            // value that's not in the module, it'll still throw an error, but
            // this avoids throwing confusing errors for `@forward`ed modules
            // without configuration.
            alreadyLoaded.couldHaveBeenConfigured(
                MapKeySet(currentConfiguration.values))) {
          var message = namesInErrors
              ? "${p.prettyUri(url)} was already loaded, so it can't be "
                  "configured using \"with\"."
              : "This module was already loaded, so it can't be configured using "
                  "\"with\".";
  
          var existingSpan = _moduleNodes[url]?.span;
          var configurationSpan = configuration == null
              ? currentConfiguration.nodeWithSpan.span
              : null;
          var secondarySpans = {
            if (existingSpan != null) existingSpan: "original load",
            if (configurationSpan != null) configurationSpan: "configuration",
          };
  
          throw secondarySpans.isEmpty
              ? _exception(message)
              : _multiSpanException(message, "new load", secondarySpans);
        }
  
        return alreadyLoaded;
      }
  
      var environment = AsyncEnvironment();
      late CssStylesheet css;
      late Map<Module, List<CssComment>>? preModuleComments;
      var extensionStore = ExtensionStore();
      await _withEnvironment(environment, () async {
        var oldImporter = _importer;
        var oldStylesheet = __stylesheet;
        var oldRoot = __root;
        var oldPreModuleComments = _preModuleComments;
        var oldParent = __parent;
        var oldEndOfImports = __endOfImports;
        var oldOutOfOrderImports = _outOfOrderImports;
        var oldExtensionStore = __extensionStore;
        var oldStyleRule = _styleRule;
        var oldMediaQueries = _mediaQueries;
        var oldDeclarationName = _declarationName;
        var oldInUnknownAtRule = _inUnknownAtRule;
        var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule;
        var oldInKeyframes = _inKeyframes;
        var oldConfiguration = _configuration;
        _importer = importer;
        _stylesheet = stylesheet;
        var root = __root = ModifiableCssStylesheet(stylesheet.span);
        _parent = root;
        _endOfImports = 0;
        _outOfOrderImports = null;
        _extensionStore = extensionStore;
        _styleRuleIgnoringAtRoot = null;
        _mediaQueries = null;
        _declarationName = null;
        _inUnknownAtRule = false;
        _atRootExcludingStyleRule = false;
        _inKeyframes = false;
        if (configuration != null) _configuration = configuration;
  
        await visitStylesheet(stylesheet);
        css = _outOfOrderImports == null
            ? root
            : CssStylesheet(_addOutOfOrderImports(), stylesheet.span);
        preModuleComments = _preModuleComments;
  
        _importer = oldImporter;
        __stylesheet = oldStylesheet;
        __root = oldRoot;
        _preModuleComments = oldPreModuleComments;
        __parent = oldParent;
        __endOfImports = oldEndOfImports;
        _outOfOrderImports = oldOutOfOrderImports;
        __extensionStore = oldExtensionStore;
        _styleRuleIgnoringAtRoot = oldStyleRule;
        _mediaQueries = oldMediaQueries;
        _declarationName = oldDeclarationName;
        _inUnknownAtRule = oldInUnknownAtRule;
        _atRootExcludingStyleRule = oldAtRootExcludingStyleRule;
        _inKeyframes = oldInKeyframes;
        _configuration = oldConfiguration;
      });
  
      var module = environment.toModule(
        css,
        preModuleComments ?? const {},
        extensionStore,
      );
      if (url != null) {
        _modules[url] = module;
        _moduleConfigurations[url] = currentConfiguration;
        if (nodeWithSpan != null) _moduleNodes[url] = nodeWithSpan;
      }
  
      return module;
    }
  
    /// Returns a copy of [_root.children] with [_outOfOrderImports] inserted
    /// after [_endOfImports], if necessary.
    List<ModifiableCssNode> _addOutOfOrderImports() =>
        switch (_outOfOrderImports) {
          null => _root.children,
          var outOfOrderImports => [
              ..._root.children.take(_endOfImports),
              ...outOfOrderImports,
              ..._root.children.skip(_endOfImports),
            ],
        };
  
    /// Returns a new stylesheet containing [root]'s CSS as well as the CSS of all
    /// modules transitively used by [root].
    ///
    /// This also applies each module's extensions to its upstream modules.
    ///
    /// If [clone] is `true`, this will copy the modules before extending them so
    /// that they don't modify [root] or its dependencies.
    CssStylesheet _combineCss(Module root, {bool clone = false}) {
      if (!root.upstream.any((module) => module.transitivelyContainsCss)) {
        var selectors = root.extensionStore.simpleSelectors;
        if (root.extensionStore
                .extensionsWhereTarget((target) => !selectors.contains(target))
                .firstOrNull
            case var unsatisfiedExtension?) {
          _throwForUnsatisfiedExtension(unsatisfiedExtension);
        }
  
        return root.css;
      }
  
      // The imports (and comments between them) that should be included at the
      // beginning of the final document.
      var imports = <CssNode>[];
  
      // The CSS statements in the final document.
      var css = <CssNode>[];
  
      /// The modules in reverse topological order.
      var sorted = Queue<Module>();
  
      /// The modules that have been visited so far. Note that if [cloneCss] is
      /// true, this contains the original modules, not the copies.
      var seen = <Module>{};
  
      void visitModule(Module module) {
        if (!seen.add(module)) return;
        if (clone) module = module.cloneCss();
  
        for (var upstream in module.upstream) {
          if (upstream.transitivelyContainsCss) {
            if (module.preModuleComments[upstream] case var comments?) {
              // Intermix the top-level comments with plain CSS `@import`s until we
              // start to have actual CSS defined, at which point start treating it as
              // normal CSS.
              (css.isEmpty ? imports : css).addAll(comments);
            }
            visitModule(upstream);
          }
        }
  
        sorted.addFirst(module);
        var statements = module.css.children;
        var index = _indexAfterImports(statements);
        imports.addAll(statements.getRange(0, index));
        css.addAll(statements.getRange(index, statements.length));
      }
  
      visitModule(root);
  
      if (root.transitivelyContainsExtensions) _extendModules(sorted);
  
      return CssStylesheet(imports + css, root.css.span);
    }
  
    /// Destructively updates the selectors in each module with the extensions
    /// defined in downstream modules.
    void _extendModules(Iterable<Module> sortedModules) {
      // All the [ExtensionStore]s directly downstream of a given module (indexed
      // by its canonical URL). It's important that we create this in topological
      // order, so that by the time we're processing a module we've already filled
      // in all its downstream [ExtensionStore]s and we can use them to extend
      // that module.
      var downstreamExtensionStores = <Uri, List<ExtensionStore>>{};
  
      /// Extensions that haven't yet been satisfied by some upstream module. This
      /// adds extensions when they're defined but not satisfied, and removes them
      /// when they're satisfied by any module.
      var unsatisfiedExtensions = Set<Extension>.identity();
  
      for (var module in sortedModules) {
        // Create a snapshot of the simple selectors currently in the
        // [ExtensionStore] so that we don't consider an extension "satisfied"
        // below because of a simple selector added by another (sibling)
        // extension.
        var originalSelectors = module.extensionStore.simpleSelectors.toSet();
  
        // Add all as-yet-unsatisfied extensions before adding downstream
        // [ExtensionStore]s, because those are all in [unsatisfiedExtensions]
        // already.
        unsatisfiedExtensions.addAll(
          module.extensionStore.extensionsWhereTarget(
            (target) => !originalSelectors.contains(target),
          ),
        );
  
        downstreamExtensionStores[module.url].andThen(
          module.extensionStore.addExtensions,
        );
        if (module.extensionStore.isEmpty) continue;
  
        for (var upstream in module.upstream) {
          if (upstream.url case var url?) {
            downstreamExtensionStores
                .putIfAbsent(url, () => [])
                .add(module.extensionStore);
          }
        }
  
        // Remove all extensions that are now satisfied after adding downstream
        // [ExtensionStore]s so it counts any downstream extensions that have been
        // newly satisfied.
        unsatisfiedExtensions.removeAll(
          module.extensionStore.extensionsWhereTarget(originalSelectors.contains),
        );
      }
  
      if (unsatisfiedExtensions.isNotEmpty) {
        _throwForUnsatisfiedExtension(unsatisfiedExtensions.first);
      }
    }
  
    /// Throws an exception indicating that [extension] is unsatisfied.
    Never _throwForUnsatisfiedExtension(Extension extension) {
      throw SassException(
        'The target selector was not found.\n'
        'Use "@extend ${extension.target} !optional" to avoid this error.',
        extension.span,
      );
    }
  
    /// Returns the index of the first node in [statements] that comes after all
    /// static imports.
    int _indexAfterImports(List<CssNode> statements) {
      var lastImport = -1;
      loop:
      for (var i = 0; i < statements.length; i++) {
        switch (statements[i]) {
          case CssImport():
            lastImport = i;
          case CssComment():
            continue loop;
          case _:
            break loop;
        }
      }
      return lastImport + 1;
    }
  
    // ## Statements
  
    Future<Value?> visitStylesheet(Stylesheet node) async {
      for (var warning in node.parseTimeWarnings) {
        _warn(warning.message, warning.span, warning.deprecation);
      }
      for (var child in node.children) {
        await child.accept(this);
      }
  
      // Make sure all global variables declared in a module always appear in the
      // module's definition, even if their assignments aren't reached.
      for (var (name, span) in node.globalVariables.pairs) {
        visitVariableDeclaration(
          VariableDeclaration(name, NullExpression(span), span, guarded: true),
        );
      }
  
      return null;
    }
  
    Future<Value?> visitAtRootRule(AtRootRule node) async {
      var query = AtRootQuery.defaultQuery;
      if (node.query case var unparsedQuery?) {
        var (resolved, map) = await _performInterpolationWithMap(
          unparsedQuery,
          warnForColor: true,
        );
        query = AtRootQuery.parse(resolved, interpolationMap: map);
      }
  
      var parent = _parent;
      var included = <ModifiableCssParentNode>[];
      while (parent is! CssStylesheet) {
        if (!query.excludes(parent)) included.add(parent);
  
        if (parent.parent case var grandparent?) {
          parent = grandparent;
        } else {
          throw StateError(
            "CssNodes must have a CssStylesheet transitive parent node.",
          );
        }
      }
      var root = _trimIncluded(included);
  
      // If we didn't exclude any rules, we don't need to use the copies we might
      // have created.
      if (root == _parent) {
        await _environment.scope(() async {
          for (var child in node.children) {
            await child.accept(this);
          }
        }, when: node.hasDeclarations);
        return null;
      }
  
      var innerCopy = root;
      if (included case [var first, ...var rest]) {
        innerCopy = first.copyWithoutChildren();
        var outerCopy = innerCopy;
        for (var node in rest) {
          var copy = node.copyWithoutChildren();
          copy.addChild(outerCopy);
          outerCopy = copy;
        }
  
        root.addChild(outerCopy);
      }
  
      await _scopeForAtRoot(node, innerCopy, query, included)(() async {
        for (var child in node.children) {
          await child.accept(this);
        }
      });
  
      return null;
    }
  
    /// Destructively trims a trailing sublist from [nodes] that matches the
    /// current list of parents.
    ///
    /// [nodes] should be a list of parents included by an `@at-root` rule, from
    /// innermost to outermost. If it contains a trailing sublist that's
    /// contiguous—meaning that each node is a direct parent of the node before
    /// it—and whose final node is a direct child of [_root], this removes that
    /// sublist and returns the innermost removed parent.
    ///
    /// Otherwise, this leaves [nodes] as-is and returns [_root].
    ModifiableCssParentNode _trimIncluded(List<ModifiableCssParentNode> nodes) {
      if (nodes.isEmpty) return _root;
  
      var parent = _parent;
      int? innermostContiguous;
      for (var i = 0; i < nodes.length; i++) {
        while (parent != nodes[i]) {
          innermostContiguous = null;
  
          if (parent.parent case var grandparent?) {
            parent = grandparent;
          } else {
            throw ArgumentError(
              "Expected ${nodes[i]} to be an ancestor of $this.",
            );
          }
        }
        innermostContiguous ??= i;
  
        if (parent.parent case var grandparent?) {
          parent = grandparent;
        } else {
          throw ArgumentError("Expected ${nodes[i]} to be an ancestor of $this.");
        }
      }
  
      if (parent != _root) return _root;
      var root = nodes[innermostContiguous!];
      nodes.removeRange(innermostContiguous, nodes.length);
      return root;
    }
  
    /// Returns a [_ScopeCallback] for [query].
    ///
    /// This returns a callback that adjusts various instance variables for its
    /// duration, based on which rules are excluded by [query]. It always assigns
    /// [_parent] to [newParent].
    _ScopeCallback _scopeForAtRoot(
      AtRootRule node,
      ModifiableCssParentNode newParent,
      AtRootQuery query,
      List<ModifiableCssParentNode> included,
    ) {
      var scope = (Future<void> callback()) async {
        // We can't use [_withParent] here because it'll add the node to the tree
        // in the wrong place.
        var oldParent = _parent;
        _parent = newParent;
        await _environment.scope(callback, when: node.hasDeclarations);
        _parent = oldParent;
      };
  
      if (query.excludesStyleRules) {
        var innerScope = scope;
        scope = (callback) async {
          var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule;
          _atRootExcludingStyleRule = true;
          await innerScope(callback);
          _atRootExcludingStyleRule = oldAtRootExcludingStyleRule;
        };
      }
  
      if (_mediaQueries != null && query.excludesName('media')) {
        var innerScope = scope;
        scope = (callback) =>
            _withMediaQueries(null, null, () => innerScope(callback));
      }
  
      if (_inKeyframes && query.excludesName('keyframes')) {
        var innerScope = scope;
        scope = (callback) async {
          var wasInKeyframes = _inKeyframes;
          _inKeyframes = false;
          await innerScope(callback);
          _inKeyframes = wasInKeyframes;
        };
      }
  
      if (_inUnknownAtRule && !included.any((parent) => parent is CssAtRule)) {
        var innerScope = scope;
        scope = (callback) async {
          var wasInUnknownAtRule = _inUnknownAtRule;
          _inUnknownAtRule = false;
          await innerScope(callback);
          _inUnknownAtRule = wasInUnknownAtRule;
        };
      }
  
      return scope;
    }
  
    Future<Value> visitContentBlock(ContentBlock node) => throw UnsupportedError(
          "Evaluation handles @include and its content block together.",
        );
  
    Future<Value?> visitContentRule(ContentRule node) async {
      var content = _environment.content;
      if (content == null) return null;
  
      await _runUserDefinedCallable(node.arguments, content, node, () async {
        for (var statement in content.declaration.children) {
          await statement.accept(this);
        }
        return null;
      });
  
      return null;
    }
  
    Future<Value?> visitDebugRule(DebugRule node) async {
      var value = await node.expression.accept(this);
      _logger.debug(
        value is SassString ? value.text : serializeValue(value, inspect: true),
        node.span,
      );
      return null;
    }
  
    Future<Value?> visitDeclaration(Declaration node) async {
      if (_styleRule == null && !_inUnknownAtRule && !_inKeyframes) {
        throw _exception(
          "Declarations may only be used within style rules.",
          node.span,
        );
      }
      if (_declarationName != null && node.isCustomProperty) {
        throw _exception(
          'Declarations whose names begin with "--" may not be nested.',
          node.span,
        );
      }
  
      var name = await _interpolationToValue(node.name, warnForColor: true);
      if (_declarationName case var declarationName?) {
        name = CssValue("$declarationName-${name.value}", name.span);
      }
  
      if (node.value case var expression?) {
        var value = await expression.accept(this);
        if (!value.isBlank ||
            // If the value is an empty list, preserve it, because converting it
            // to CSS will throw an error that we want the user to see.
            _isEmptyList(value) ||
            // Custom properties are allowed to have empty values, per spec.
            name.value.startsWith('--')) {
          _copyParentAfterSibling();
          _parent.addChild(
            ModifiableCssDeclaration(
              name,
              CssValue(value, expression.span),
              node.span,
              parsedAsCustomProperty: node.isCustomProperty,
              trace: _stackTrace(node.span),
              valueSpanForMap:
                  _sourceMap ? node.value.andThen(_expressionNode)?.span : null,
            ),
          );
        }
      }
  
      if (node.children case var children?) {
        var oldDeclarationName = _declarationName;
        _declarationName = name.value;
        await _environment.scope(() async {
          for (var child in children) {
            await child.accept(this);
          }
        }, when: node.hasDeclarations);
        _declarationName = oldDeclarationName;
      }
  
      return null;
    }
  
    /// Returns whether [value] is an empty list.
    bool _isEmptyList(Value value) => value.asList.isEmpty;
  
    Future<Value?> visitEachRule(EachRule node) async {
      var list = await node.list.accept(this);
      var nodeWithSpan = _expressionNode(node.list);
      var setVariables = switch (node.variables) {
        [var variable] => (Value value) => _environment.setLocalVariable(
              variable,
              _withoutSlash(value, nodeWithSpan),
              nodeWithSpan,
            ),
        var variables => (Value value) =>
            _setMultipleVariables(variables, value, nodeWithSpan),
      };
      return _environment.scope(() {
        return _handleReturn<Value>(list.asList, (element) {
          setVariables(element);
          return _handleReturn<Statement>(
            node.children,
            (child) => child.accept(this),
          );
        });
      }, semiGlobal: true);
    }
  
    /// Destructures [value] and assigns it to [variables], as in an `@each`
    /// statement.
    void _setMultipleVariables(
      List<String> variables,
      Value value,
      AstNode nodeWithSpan,
    ) {
      var list = value.asList;
      var minLength = math.min(variables.length, list.length);
      for (var i = 0; i < minLength; i++) {
        _environment.setLocalVariable(
          variables[i],
          _withoutSlash(list[i], nodeWithSpan),
          nodeWithSpan,
        );
      }
      for (var i = minLength; i < variables.length; i++) {
        _environment.setLocalVariable(variables[i], sassNull, nodeWithSpan);
      }
    }
  
    Future<Value> visitErrorRule(ErrorRule node) async {
      throw _exception(
        (await node.expression.accept(this)).toString(),
        node.span,
      );
    }
  
    Future<Value?> visitExtendRule(ExtendRule node) async {
      var styleRule = _styleRule;
      if (styleRule == null || _declarationName != null) {
        throw _exception(
          "@extend may only be used within style rules.",
          node.span,
        );
      }
  
      for (var complex in styleRule.originalSelector.components) {
        if (!complex.isBogus) continue;
        _warn(
          'The selector "${complex.toString().trim()}" is invalid CSS and ' +
              (complex.isUseless ? "can't" : "shouldn't") +
              ' be an extender.\n'
                  'This will be an error in Dart Sass 2.0.0.\n'
                  '\n'
                  'More info: https://sass-lang.com/d/bogus-combinators',
          MultiSpan(complex.span.trimRight(), 'invalid selector', {
            node.span: '@extend rule',
          }),
          Deprecation.bogusCombinators,
        );
      }
  
      var (targetText, targetMap) = await _performInterpolationWithMap(
        node.selector,
        warnForColor: true,
      );
  
      var list = SelectorList.parse(
        trimAscii(targetText, excludeEscape: true),
        interpolationMap: targetMap,
        allowParent: false,
      );
  
      for (var complex in list.components) {
        var compound = complex.singleCompound;
        if (compound == null) {
          // If the selector was a compound selector but not a simple
          // selector, emit a more explicit error.
          throw SassFormatException(
            "complex selectors may not be extended.",
            complex.span,
          );
        }
  
        var simple = compound.singleSimple;
        if (simple == null) {
          throw SassFormatException(
            "compound selectors may no longer be extended.\n"
            "Consider `@extend ${compound.components.join(', ')}` instead.\n"
            "See https://sass-lang.com/d/extend-compound for details.\n",
            compound.span,
          );
        }
  
        _extensionStore.addExtension(
          styleRule.selector,
          simple,
          node,
          _mediaQueries,
        );
      }
  
      return null;
    }
  
    Future<Value?> visitAtRule(AtRule node) async {
      // NOTE: this logic is largely duplicated in [visitCssAtRule]. Most changes
      // here should be mirrored there.
  
      if (_declarationName != null) {
        throw _exception(
          "At-rules may not be used within nested declarations.",
          node.span,
        );
      }
  
      var name = await _interpolationToValue(node.name);
  
      var value = await node.value.andThen(
        (value) => _interpolationToValue(value, trim: true, warnForColor: true),
      );
  
      var children = node.children;
      if (children == null) {
        _copyParentAfterSibling();
        _parent.addChild(
          ModifiableCssAtRule(name, node.span, childless: true, value: value),
        );
        return null;
      }
  
      var wasInKeyframes = _inKeyframes;
      var wasInUnknownAtRule = _inUnknownAtRule;
      if (unvendor(name.value) == 'keyframes') {
        _inKeyframes = true;
      } else {
        _inUnknownAtRule = true;
      }
  
      await _withParent(
        ModifiableCssAtRule(name, node.span, value: value),
        () async {
          var styleRule = _styleRule;
          if (styleRule == null || _inKeyframes || name.value == 'font-face') {
            // Special-cased at-rules within style blocks are pulled out to the
            // root. Equivalent to prepending "@at-root" on them.
            for (var child in children) {
              await child.accept(this);
            }
          } else {
            // If we're in a style rule, copy it into the at-rule so that
            // declarations immediately inside it have somewhere to go.
            //
            // For example, "a {@foo {b: c}}" should produce "@foo {a {b: c}}".
            await _withParent(styleRule.copyWithoutChildren(), () async {
              for (var child in children) {
                await child.accept(this);
              }
            }, scopeWhen: false);
          }
        },
        through: (node) => node is CssStyleRule,
        scopeWhen: node.hasDeclarations,
      );
  
      _inUnknownAtRule = wasInUnknownAtRule;
      _inKeyframes = wasInKeyframes;
      return null;
    }
  
    Future<Value?> visitForRule(ForRule node) async {
      var fromNumber = await _addExceptionSpanAsync(
        node.from,
        () async => (await node.from.accept(this)).assertNumber(),
      );
      var toNumber = await _addExceptionSpanAsync(
        node.to,
        () async => (await node.to.accept(this)).assertNumber(),
      );
  
      var from = _addExceptionSpan(node.from, () => fromNumber.assertInt());
      var to = _addExceptionSpan(
        node.to,
        () => toNumber
            .coerce(fromNumber.numeratorUnits, fromNumber.denominatorUnits)
            .assertInt(),
      );
  
      var direction = from > to ? -1 : 1;
      if (!node.isExclusive) to += direction;
      if (from == to) return null;
  
      return _environment.scope(() async {
        var nodeWithSpan = _expressionNode(node.from);
        for (var i = from; i != to; i += direction) {
          _environment.setLocalVariable(
            node.variable,
            SassNumber.withUnits(
              i,
              numeratorUnits: fromNumber.numeratorUnits,
              denominatorUnits: fromNumber.denominatorUnits,
            ),
            nodeWithSpan,
          );
          if (await _handleReturn<Statement>(
            node.children,
            (child) => child.accept(this),
          )
              case var result?) {
            return result;
          }
        }
        return null;
      }, semiGlobal: true);
    }
  
    Future<Value?> visitForwardRule(ForwardRule node) async {
      var oldConfiguration = _configuration;
      var adjustedConfiguration = oldConfiguration.throughForward(node);
  
      if (node.configuration.isNotEmpty) {
        var newConfiguration = await _addForwardConfiguration(
          adjustedConfiguration,
          node,
        );
  
        await _loadModule(node.url, "@forward", node, (module, firstLoad) {
          if (firstLoad) _registerCommentsForModule(module);
          _environment.forwardModule(module, node);
        }, configuration: newConfiguration);
  
        _removeUsedConfiguration(
          adjustedConfiguration,
          newConfiguration,
          except: {
            for (var variable in node.configuration)
              if (!variable.isGuarded) variable.name,
          },
        );
  
        // Remove all the variables that weren't configured by this particular
        // `@forward` before checking that the configuration is empty. Errors for
        // outer `with` clauses will be thrown once those clauses finish
        // executing.
        var configuredVariables = {
          for (var variable in node.configuration) variable.name,
        };
        for (var name in newConfiguration.values.keys.toList()) {
          if (!configuredVariables.contains(name)) newConfiguration.remove(name);
        }
  
        _assertConfigurationIsEmpty(newConfiguration);
      } else {
        _configuration = adjustedConfiguration;
        await _loadModule(node.url, "@forward", node, (module, firstLoad) {
          if (firstLoad) _registerCommentsForModule(module);
          _environment.forwardModule(module, node);
        });
        _configuration = oldConfiguration;
      }
  
      return null;
    }
  
    /// Updates [configuration] to include [node]'s configuration and returns the
    /// result.
    Future<Configuration> _addForwardConfiguration(
      Configuration configuration,
      ForwardRule node,
    ) async {
      var newValues = Map.of(configuration.values);
      for (var variable in node.configuration) {
        if (variable.isGuarded) {
          if (configuration.remove(variable.name) case var oldValue?
              when oldValue.value != sassNull) {
            newValues[variable.name] = oldValue;
            continue;
          }
        }
  
        var variableNodeWithSpan = _expressionNode(variable.expression);
        newValues[variable.name] = ConfiguredValue.explicit(
          _withoutSlash(
            await variable.expression.accept(this),
            variableNodeWithSpan,
          ),
          variable.span,
          variableNodeWithSpan,
        );
      }
  
      if (configuration is ExplicitConfiguration || configuration.isEmpty) {
        return ExplicitConfiguration(newValues, node);
      } else {
        return Configuration.implicit(newValues);
      }
    }
  
    /// Adds any comments in [_root.children] to [_preModuleComments] for
    /// [module].
    void _registerCommentsForModule(Module module) {
      // If we're not in a module (for example, we're evaluating a line of code
      // for the repl), there's nothing to register comments for.
      if (__root == null) return;
      if (_root.children.isEmpty || !module.transitivelyContainsCss) return;
      (_preModuleComments ??= {})
          .putIfAbsent(module, () => [])
          .addAll(_root.children.cast<CssComment>());
      _root.clearChildren();
      _endOfImports = 0;
    }
  
    /// Remove configured values from [upstream] that have been removed from
    /// [downstream], unless they match a name in [except].
    void _removeUsedConfiguration(
      Configuration upstream,
      Configuration downstream, {
      required Set<String> except,
    }) {
      for (var name in upstream.values.keys.toList()) {
        if (except.contains(name)) continue;
        if (!downstream.values.containsKey(name)) upstream.remove(name);
      }
    }
  
    /// Throws an error if [configuration] contains any values.
    ///
    /// If [only] is passed, this will only throw an error for configured values
    /// with the given names.
    ///
    /// If [nameInError] is `true`, this includes the name of the configured
    /// variable in the error message. This should only be `true` if the name
    /// won't be obvious from the source span.
    void _assertConfigurationIsEmpty(
      Configuration configuration, {
      bool nameInError = false,
    }) {
      // By definition, implicit configurations are allowed to only use a subset
      // of their values.
      if (configuration is! ExplicitConfiguration) return;
      if (configuration.isEmpty) return;
  
      var (name, value) = configuration.values.pairs.first;
      throw _exception(
        nameInError
            ? "\$$name was not declared with !default in the @used "
                "module."
            : "This variable was not declared with !default in the @used "
                "module.",
        value.configurationSpan,
      );
    }
  
    Future<Value?> visitFunctionRule(FunctionRule node) async {
      _environment.setFunction(
        UserDefinedCallable(
          node,
          _environment.closure(),
          inDependency: _inDependency,
        ),
      );
      return null;
    }
  
    Future<Value?> visitIfRule(IfRule node) async {
      IfRuleClause? clause = node.lastClause;
      for (var clauseToCheck in node.clauses) {
        if ((await clauseToCheck.expression.accept(this)).isTruthy) {
          clause = clauseToCheck;
          break;
        }
      }
  
      return await clause.andThen<Future<Value?>>(
        (clause) => _environment.scope(
          () => _handleReturn<Statement>(
            clause.children,
            (child) => child.accept(this),
          ),
          semiGlobal: true,
          when: clause.hasDeclarations,
        ),
      );
    }
  
    Future<Value?> visitImportRule(ImportRule node) async {
      for (var import in node.imports) {
        if (import is DynamicImport) {
          await _visitDynamicImport(import);
        } else {
          await _visitStaticImport(import as StaticImport);
        }
      }
      return null;
    }
  
    /// Adds the stylesheet imported by [import] to the current document.
    Future<void> _visitDynamicImport(DynamicImport import) {
      return _withStackFrame("@import", import, () async {
        var (stylesheet, :importer, :isDependency) = await _loadStylesheet(
          import.urlString,
          import.span,
          forImport: true,
        );
  
        var url = stylesheet.span.sourceUrl;
        if (url != null) {
          if (_activeModules.containsKey(url)) {
            throw _activeModules[url].andThen(
                  (previousLoad) => _multiSpanException(
                    "This file is already being loaded.",
                    "new load",
                    {previousLoad.span: "original load"},
                  ),
                ) ??
                _exception("This file is already being loaded.");
          }
          _activeModules[url] = import;
        }
  
        // If the imported stylesheet doesn't use any modules, we can inject its
        // CSS directly into the current stylesheet. If it does use modules, we
        // need to put its CSS into an intermediate [ModifiableCssStylesheet] so
        // that we can hermetically resolve `@extend`s before injecting it.
        if (stylesheet.uses.isEmpty && stylesheet.forwards.isEmpty) {
          var oldImporter = _importer;
          var oldStylesheet = _stylesheet;
          var oldInDependency = _inDependency;
          _importer = importer;
          _stylesheet = stylesheet;
          _inDependency = isDependency;
          await visitStylesheet(stylesheet);
          _importer = oldImporter;
          _stylesheet = oldStylesheet;
          _inDependency = oldInDependency;
          _activeModules.remove(url);
          return;
        }
  
        // If only built-in modules are loaded, we still need a separate
        // environment to ensure their namespaces aren't exposed in the outer
        // environment, but we don't need to worry about `@extend`s, so we can
        // add styles directly to the existing stylesheet instead of creating a
        // new one.
        var loadsUserDefinedModules =
            stylesheet.uses.any((rule) => rule.url.scheme != 'sass') ||
                stylesheet.forwards.any((rule) => rule.url.scheme != 'sass');
  
        late List<ModifiableCssNode> children;
        var environment = _environment.forImport();
        await _withEnvironment(environment, () async {
          var oldImporter = _importer;
          var oldStylesheet = _stylesheet;
          var oldRoot = _root;
          var oldParent = _parent;
          var oldEndOfImports = _endOfImports;
          var oldOutOfOrderImports = _outOfOrderImports;
          var oldConfiguration = _configuration;
          var oldInDependency = _inDependency;
          _importer = importer;
          _stylesheet = stylesheet;
          if (loadsUserDefinedModules) {
            _root = ModifiableCssStylesheet(stylesheet.span);
            _parent = _root;
            _endOfImports = 0;
            _outOfOrderImports = null;
          }
          _inDependency = isDependency;
  
          // This configuration is only used if it passes through a `@forward`
          // rule, so we avoid creating unnecessary ones for performance reasons.
          if (stylesheet.forwards.isNotEmpty) {
            _configuration = environment.toImplicitConfiguration();
          }
  
          await visitStylesheet(stylesheet);
          children = loadsUserDefinedModules ? _addOutOfOrderImports() : [];
  
          _importer = oldImporter;
          _stylesheet = oldStylesheet;
          if (loadsUserDefinedModules) {
            _root = oldRoot;
            _parent = oldParent;
            _endOfImports = oldEndOfImports;
            _outOfOrderImports = oldOutOfOrderImports;
          }
          _configuration = oldConfiguration;
          _inDependency = oldInDependency;
        });
  
        // Create a dummy module with empty CSS and no extensions to make forwarded
        // members available in the current import context and to combine all the
        // CSS from modules used by [stylesheet].
        var module = environment.toDummyModule();
        _environment.importForwards(module);
        if (loadsUserDefinedModules) {
          if (module.transitivelyContainsCss) {
            // If any transitively used module contains extensions, we need to
            // clone all modules' CSS. Otherwise, it's possible that they'll be
            // used or imported from another location that shouldn't have the same
            // extensions applied.
            await _combineCss(
              module,
              clone: module.transitivelyContainsExtensions,
            ).accept(this);
          }
  
          var visitor = _ImportedCssVisitor(this);
          for (var child in children) {
            child.accept(visitor);
          }
        }
  
        _activeModules.remove(url);
      });
    }
  
    /// Loads the [Stylesheet] identified by [url], or throws a
    /// [SassRuntimeException] if loading fails.
    ///
    /// This first tries loading [url] relative to [baseUrl], which defaults to
    /// `_stylesheet.span.sourceUrl`.
    Future<_LoadedStylesheet> _loadStylesheet(
      String url,
      FileSpan span, {
      Uri? baseUrl,
      bool forImport = false,
    }) async {
      try {
        assert(_importSpan == null);
        _importSpan = span;
  
        if (_importCache case var importCache?) {
          baseUrl ??= _stylesheet.span.sourceUrl;
          if (await importCache.canonicalize(
            Uri.parse(url),
            baseImporter: _importer,
            baseUrl: baseUrl,
            forImport: forImport,
          )
              case (var importer, var canonicalUrl, :var originalUrl)) {
            if (canonicalUrl.scheme == '') {
              _logger.warnForDeprecation(
                Deprecation.relativeCanonical,
                "Importer $importer canonicalized $url to $canonicalUrl.\n"
                "Relative canonical URLs are deprecated and will eventually be "
                "disallowed.",
              );
            }
            // Make sure we record the canonical URL as "loaded" even if the
            // actual load fails, because watchers should watch it to see if it
            // changes in a way that allows the load to succeed.
            _loadedUrls.add(canonicalUrl);
  
            var isDependency = _inDependency || importer != _importer;
            if (await importCache.importCanonical(
              importer,
              canonicalUrl,
              originalUrl: originalUrl,
            )
                case var stylesheet?) {
              return (stylesheet, importer: importer, isDependency: isDependency);
            }
          }
        }
  
        if (_nodeImporter != null) {
          if (await _importLikeNode(
            url,
            baseUrl ?? _stylesheet.span.sourceUrl,
            forImport,
          )
              case var result?) {
            result.$1.span.sourceUrl.andThen(_loadedUrls.add);
            return result;
          }
        }
  
        if (url.startsWith('package:') && isJS) {
          // Special-case this error message, since it's tripped people up in the
          // past.
          throw "\"package:\" URLs aren't supported on this platform.";
        } else {
          throw "Can't find stylesheet to import.";
        }
      } on SassException {
        rethrow;
      } on ArgumentError catch (error, stackTrace) {
        throwWithTrace(_exception(error.toString()), error, stackTrace);
      } catch (error, stackTrace) {
        throwWithTrace(_exception(_getErrorMessage(error)), error, stackTrace);
      } finally {
        _importSpan = null;
      }
    }
  
    /// Imports a stylesheet using [_nodeImporter].
    ///
    /// Returns the [Stylesheet], or `null` if the import failed.
    Future<_LoadedStylesheet?> _importLikeNode(
      String originalUrl,
      Uri? previous,
      bool forImport,
    ) async {
      var result = _nodeImporter!.loadRelative(originalUrl, previous, forImport);
  
      bool isDependency;
      if (result != null) {
        isDependency = _inDependency;
      } else {
        result = await _nodeImporter.loadAsync(originalUrl, previous, forImport);
        if (result == null) return null;
        isDependency = true;
      }
  
      var (contents, url) = result;
      return (
        Stylesheet.parse(
          contents,
          url.startsWith('file') ? Syntax.forPath(url) : Syntax.scss,
          url: url,
        ),
        importer: null,
        isDependency: isDependency,
      );
    }
  
    /// Adds a CSS import for [import].
    Future<void> _visitStaticImport(StaticImport import) async {
      // NOTE: this logic is largely duplicated in [visitCssImport]. Most changes
      // here should be mirrored there.
  
      var node = ModifiableCssImport(
        await _interpolationToValue(import.url),
        import.span,
        modifiers: await import.modifiers.andThen<Future<CssValue<String>>?>(
          _interpolationToValue,
        ),
      );
  
      if (_parent != _root) {
        _copyParentAfterSibling();
        _parent.addChild(node);
      } else if (_endOfImports == _root.children.length) {
        _root.addChild(node);
        _endOfImports++;
      } else {
        (_outOfOrderImports ??= []).add(node);
      }
    }
  
    /// Evaluate a given [mixin] with [arguments] and [contentCallable]
    Future<void> _applyMixin(
      AsyncCallable? mixin,
      UserDefinedCallable<AsyncEnvironment>? contentCallable,
      ArgumentList arguments,
      AstNode nodeWithSpan,
      AstNode nodeWithSpanWithoutContent,
    ) async {
      switch (mixin) {
        case null:
          throw _exception("Undefined mixin.", nodeWithSpan.span);
  
        case AsyncBuiltInCallable(acceptsContent: false)
            when contentCallable != null:
          {
            var evaluated = await _evaluateArguments(arguments);
            var (overload, _) = mixin.callbackFor(
              evaluated.positional.length,
              MapKeySet(evaluated.named),
            );
            throw MultiSpanSassRuntimeException(
              "Mixin doesn't accept a content block.",
              nodeWithSpanWithoutContent.span,
              "invocation",
              {overload.spanWithName: "declaration"},
              _stackTrace(nodeWithSpanWithoutContent.span),
            );
          }
        case AsyncBuiltInCallable():
          await _environment.withContent(contentCallable, () async {
            await _environment.asMixin(() async {
              await _runBuiltInCallable(
                arguments,
                mixin,
                nodeWithSpanWithoutContent,
              );
            });
          });
  
        case UserDefinedCallable<AsyncEnvironment>(
              declaration: MixinRule(hasContent: false),
            )
            when contentCallable != null:
          throw MultiSpanSassRuntimeException(
            "Mixin doesn't accept a content block.",
            nodeWithSpanWithoutContent.span,
            "invocation",
            {mixin.declaration.parameters.spanWithName: "declaration"},
            _stackTrace(nodeWithSpanWithoutContent.span),
          );
  
        case UserDefinedCallable<AsyncEnvironment>():
          await _runUserDefinedCallable(
            arguments,
            mixin,
            nodeWithSpanWithoutContent,
            () async {
              await _environment.withContent(contentCallable, () async {
                await _environment.asMixin(() async {
                  for (var statement in mixin.declaration.children) {
                    await _addErrorSpan(
                      nodeWithSpanWithoutContent,
                      () => statement.accept(this),
                    );
                  }
                });
              });
            },
          );
  
        case _:
          throw UnsupportedError("Unknown callable type $mixin.");
      }
    }
  
    Future<Value?> visitIncludeRule(IncludeRule node) async {
      var mixin = _addExceptionSpan(
        node,
        () => _environment.getMixin(node.name, namespace: node.namespace),
      );
      if (node.originalName.startsWith('--') &&
          mixin is UserDefinedCallable &&
          !mixin.declaration.originalName.startsWith('--')) {
        _warn(
          'Sass @mixin names beginning with -- are deprecated for forward-'
          'compatibility with plain CSS mixins.\n'
          '\n'
          'For details, see https://sass-lang.com/d/css-function-mixin',
          node.nameSpan,
          Deprecation.cssFunctionMixin,
        );
      }
  
      var contentCallable = node.content.andThen(
        (content) => UserDefinedCallable(
          content,
          _environment.closure(),
          inDependency: _inDependency,
        ),
      );
  
      var nodeWithSpanWithoutContent = AstNode.fake(
        () => node.spanWithoutContent,
      );
  
      await _applyMixin(
        mixin,
        contentCallable,
        node.arguments,
        node,
        nodeWithSpanWithoutContent,
      );
  
      return null;
    }
  
    Future<Value?> visitMixinRule(MixinRule node) async {
      _environment.setMixin(
        UserDefinedCallable(
          node,
          _environment.closure(),
          inDependency: _inDependency,
        ),
      );
      return null;
    }
  
    Future<Value?> visitLoudComment(LoudComment node) async {
      // NOTE: this logic is largely duplicated in [visitCssComment]. Most changes
      // here should be mirrored there.
  
      if (_inFunction) return null;
  
      // Comments are allowed to appear between CSS imports.
      if (_parent == _root && _endOfImports == _root.children.length) {
        _endOfImports++;
      }
  
      var text = await _performInterpolation(node.text);
      // Indented syntax doesn't require */
      if (!text.endsWith("*/")) text += " */";
  
      _copyParentAfterSibling();
      _parent.addChild(ModifiableCssComment(text, node.span));
      return null;
    }
  
    Future<Value?> visitMediaRule(MediaRule node) async {
      // NOTE: this logic is largely duplicated in [visitCssMediaRule]. Most
      // changes here should be mirrored there.
  
      if (_declarationName != null) {
        throw _exception(
          "Media rules may not be used within nested declarations.",
          node.span,
        );
      }
  
      var queries = await _visitMediaQueries(node.query);
      var mergedQueries = _mediaQueries.andThen(
        (mediaQueries) => _mergeMediaQueries(mediaQueries, queries),
      );
      if (mergedQueries != null && mergedQueries.isEmpty) return null;
  
      var mergedSources = mergedQueries == null
          ? const <CssMediaQuery>{}
          : {..._mediaQuerySources!, ..._mediaQueries!, ...queries};
  
      await _withParent(
        ModifiableCssMediaRule(mergedQueries ?? queries, node.span),
        () async {
          await _withMediaQueries(
            mergedQueries ?? queries,
            mergedSources,
            () async {
              if (_styleRule case var styleRule?) {
                // If we're in a style rule, copy it into the media query so that
                // declarations immediately inside @media have somewhere to go.
                //
                // For example, "a {@media screen {b: c}}" should produce
                // "@media screen {a {b: c}}".
                await _withParent(styleRule.copyWithoutChildren(), () async {
                  for (var child in node.children) {
                    await child.accept(this);
                  }
                }, scopeWhen: false);
              } else {
                for (var child in node.children) {
                  await child.accept(this);
                }
              }
            },
          );
        },
        through: (node) =>
            node is CssStyleRule ||
            (mergedSources.isNotEmpty &&
                node is CssMediaRule &&
                node.queries.every(mergedSources.contains)),
        scopeWhen: node.hasDeclarations,
      );
  
      return null;
    }
  
    /// Evaluates [interpolation] and parses the result as a list of media
    /// queries.
    Future<List<CssMediaQuery>> _visitMediaQueries(
      Interpolation interpolation,
    ) async {
      var (resolved, map) = await _performInterpolationWithMap(
        interpolation,
        warnForColor: true,
      );
      return CssMediaQuery.parseList(resolved, interpolationMap: map);
    }
  
    /// Returns a list of queries that selects for contexts that match both
    /// [queries1] and [queries2].
    ///
    /// Returns the empty list if there are no contexts that match both [queries1]
    /// and [queries2], or `null` if there are contexts that can't be represented
    /// by media queries.
    List<CssMediaQuery>? _mergeMediaQueries(
      Iterable<CssMediaQuery> queries1,
      Iterable<CssMediaQuery> queries2,
    ) {
      var queries = <CssMediaQuery>[];
      for (var query1 in queries1) {
        inner:
        for (var query2 in queries2) {
          switch (query1.merge(query2)) {
            case MediaQueryMergeResult.empty:
              continue inner;
            case MediaQueryMergeResult.unrepresentable:
              return null;
            case MediaQuerySuccessfulMergeResult result:
              queries.add(result.query);
          }
        }
      }
      return queries;
    }
  
    Future<Value> visitReturnRule(ReturnRule node) async =>
        _withoutSlash(await node.expression.accept(this), node.expression);
  
    Future<Value?> visitSilentComment(SilentComment node) async => null;
  
    Future<Value?> visitStyleRule(StyleRule node) async {
      // NOTE: this logic is largely duplicated in [visitCssStyleRule]. Most
      // changes here should be mirrored there.
  
      if (_declarationName != null) {
        throw _exception(
          "Style rules may not be used within nested declarations.",
          node.span,
        );
      } else if (_inKeyframes && _parent is CssKeyframeBlock) {
        throw _exception(
          "Style rules may not be used within keyframe blocks.",
          node.span,
        );
      }
  
      var (selectorText, selectorMap) = await _performInterpolationWithMap(
        node.selector,
        warnForColor: true,
      );
  
      if (_inKeyframes) {
        // NOTE: this logic is largely duplicated in [visitCssKeyframeBlock]. Most
        // changes here should be mirrored there.
  
        var parsedSelector = KeyframeSelectorParser(
          selectorText,
          interpolationMap: selectorMap,
        ).parse();
        var rule = ModifiableCssKeyframeBlock(
          CssValue(List.unmodifiable(parsedSelector), node.selector.span),
          node.span,
        );
        await _withParent(
          rule,
          () async {
            for (var child in node.children) {
              await child.accept(this);
            }
          },
          through: (node) => node is CssStyleRule,
          scopeWhen: node.hasDeclarations,
        );
        return null;
      }
  
      var parsedSelector = SelectorList.parse(
        selectorText,
        interpolationMap: selectorMap,
        plainCss: _stylesheet.plainCss,
      );
  
      var nest = switch (_styleRule) {
        null => true,
        CssStyleRule(fromPlainCss: true) => false,
        _ => !(_stylesheet.plainCss && parsedSelector.containsParentSelector)
      };
      if (nest) {
        if (_stylesheet.plainCss) {
          for (var complex in parsedSelector.components) {
            if (complex.leadingCombinators
                case [
                  var first,
                  ...,
                ] when _stylesheet.plainCss) {
              throw _exception(
                "Top-level leading combinators aren't allowed in plain CSS.",
                first.span,
              );
            }
          }
        }
  
        parsedSelector = parsedSelector.nestWithin(
          _styleRuleIgnoringAtRoot?.originalSelector,
          implicitParent: !_atRootExcludingStyleRule,
          preserveParentSelectors: _stylesheet.plainCss,
        );
      }
  
      var selector = _extensionStore.addSelector(parsedSelector, _mediaQueries);
      var rule = ModifiableCssStyleRule(
        selector,
        node.span,
        originalSelector: parsedSelector,
        fromPlainCss: _stylesheet.plainCss,
      );
      var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule;
      _atRootExcludingStyleRule = false;
      await _withParent(
        rule,
        () async {
          await _withStyleRule(rule, () async {
            for (var child in node.children) {
              await child.accept(this);
            }
          });
        },
        through: nest ? (node) => node is CssStyleRule : null,
        scopeWhen: node.hasDeclarations,
      );
      _atRootExcludingStyleRule = oldAtRootExcludingStyleRule;
  
      _warnForBogusCombinators(rule);
  
      if (_styleRule == null && _parent.children.isNotEmpty) {
        var lastChild = _parent.children.last;
        lastChild.isGroupEnd = true;
      }
  
      return null;
    }
  
    /// Emits deprecation warnings for any bogus combinators in [rule].
    void _warnForBogusCombinators(CssStyleRule rule) {
      if (!rule.isInvisibleOtherThanBogusCombinators) {
        for (var complex in rule.selector.components) {
          if (!complex.isBogus) continue;
  
          if (complex.isUseless) {
            _warn(
              'The selector "${complex.toString().trim()}" is invalid CSS. It '
              'will be omitted from the generated CSS.\n'
              'This will be an error in Dart Sass 2.0.0.\n'
              '\n'
              'More info: https://sass-lang.com/d/bogus-combinators',
              complex.span.trimRight(),
              Deprecation.bogusCombinators,
            );
          } else if (complex.leadingCombinators.isNotEmpty) {
            if (!_stylesheet.plainCss) {
              _warn(
                'The selector "${complex.toString().trim()}" is invalid CSS.\n'
                'This will be an error in Dart Sass 2.0.0.\n'
                '\n'
                'More info: https://sass-lang.com/d/bogus-combinators',
                complex.span.trimRight(),
                Deprecation.bogusCombinators,
              );
            }
          } else {
            _warn(
              'The selector "${complex.toString().trim()}" is only valid for '
                      "nesting and shouldn't\n"
                      'have children other than style rules.' +
                  (complex.isBogusOtherThanLeadingCombinator
                      ? ' It will be omitted from the generated CSS.'
                      : '') +
                  '\n'
                      'This will be an error in Dart Sass 2.0.0.\n'
                      '\n'
                      'More info: https://sass-lang.com/d/bogus-combinators',
              MultiSpan(complex.span.trimRight(), 'invalid selector', {
                rule.children.first.span: "this is not a style rule" +
                    (rule.children.every((child) => child is CssComment)
                        ? '\n(try converting to a //-style comment)'
                        : ''),
              }),
              Deprecation.bogusCombinators,
            );
          }
        }
      }
    }
  
    Future<Value?> visitSupportsRule(SupportsRule node) async {
      // NOTE: this logic is largely duplicated in [visitCssSupportsRule]. Most
      // changes here should be mirrored there.
  
      if (_declarationName != null) {
        throw _exception(
          "Supports rules may not be used within nested declarations.",
          node.span,
        );
      }
  
      var condition = CssValue(
        await _visitSupportsCondition(node.condition),
        node.condition.span,
      );
      await _withParent(
        ModifiableCssSupportsRule(condition, node.span),
        () async {
          if (_styleRule case var styleRule?) {
            // If we're in a style rule, copy it into the supports rule so that
            // declarations immediately inside @supports have somewhere to go.
            //
            // For example, "a {@supports (a: b) {b: c}}" should produce "@supports
            // (a: b) {a {b: c}}".
            await _withParent(styleRule.copyWithoutChildren(), () async {
              for (var child in node.children) {
                await child.accept(this);
              }
            });
          } else {
            for (var child in node.children) {
              await child.accept(this);
            }
          }
        },
        through: (node) => node is CssStyleRule,
        scopeWhen: node.hasDeclarations,
      );
  
      return null;
    }
  
    /// Evaluates [condition] and converts it to a plain CSS string.
    Future<String> _visitSupportsCondition(SupportsCondition condition) async =>
        switch (condition) {
          SupportsOperation operation =>
            "${await _parenthesize(operation.left, operation.operator)} "
                "${operation.operator} "
                "${await _parenthesize(operation.right, operation.operator)}",
          SupportsNegation negation =>
            "not ${await _parenthesize(negation.condition)}",
          SupportsInterpolation interpolation => await _evaluateToCss(
              interpolation.expression,
              quote: false,
            ),
          SupportsDeclaration declaration => await _withSupportsDeclaration(
              () async => "(${await _evaluateToCss(declaration.name)}:"
                  "${declaration.isCustomProperty ? '' : ' '}"
                  "${await _evaluateToCss(declaration.value)})",
            ),
          SupportsFunction function =>
            "${await _performInterpolation(function.name)}("
                "${await _performInterpolation(function.arguments)})",
          SupportsAnything anything =>
            "(${await _performInterpolation(anything.contents)})",
          var condition => throw ArgumentError(
              "Unknown supports condition type ${condition.runtimeType}.",
            ),
        };
  
    /// Runs [callback] in a context where [_inSupportsDeclaration] is true.
    Future<T> _withSupportsDeclaration<T>(FutureOr<T> callback()) async {
      var oldInSupportsDeclaration = _inSupportsDeclaration;
      _inSupportsDeclaration = true;
      try {
        return await callback();
      } finally {
        _inSupportsDeclaration = oldInSupportsDeclaration;
      }
    }
  
    /// Evaluates [condition] and converts it to a plain CSS string, with
    /// parentheses if necessary.
    ///
    /// If [operator] is passed, it's the operator for the surrounding
    /// [SupportsOperation], and is used to determine whether parentheses are
    /// necessary if [condition] is also a [SupportsOperation].
    Future<String> _parenthesize(
      SupportsCondition condition, [
      String? operator,
    ]) async {
      switch (condition) {
        case SupportsNegation():
        case SupportsOperation()
            when operator == null || operator != condition.operator:
          return "(${await _visitSupportsCondition(condition)})";
  
        case _:
          return await _visitSupportsCondition(condition);
      }
    }
  
    Future<Value?> visitVariableDeclaration(VariableDeclaration node) async {
      if (node.isGuarded) {
        if (node.namespace == null && _environment.atRoot) {
          _environment.markVariableConfigurable(node.name);
          if (_configuration.remove(node.name) case var override?
              when override.value != sassNull) {
            _addExceptionSpan(node, () {
              _environment.setVariable(
                node.name,
                override.value,
                override.assignmentNode,
                global: true,
              );
            });
            return null;
          }
        }
  
        var value = _addExceptionSpan(
          node,
          () => _environment.getVariable(node.name, namespace: node.namespace),
        );
        if (value != null && value != sassNull) return null;
      }
  
      if (node.isGlobal && !_environment.globalVariableExists(node.name)) {
        _warn(
          _environment.atRoot
              ? "As of Dart Sass 2.0.0, !global assignments won't be able to "
                  "declare new variables.\n"
                  "\n"
                  "Since this assignment is at the root of the stylesheet, the "
                  "!global flag is\n"
                  "unnecessary and can safely be removed."
              : "As of Dart Sass 2.0.0, !global assignments won't be able to "
                  "declare new variables.\n"
                  "\n"
                  "Recommendation: add `${node.originalName}: null` at the "
                  "stylesheet root.",
          node.span,
          Deprecation.newGlobal,
        );
      }
  
      var value = _withoutSlash(
        await node.expression.accept(this),
        node.expression,
      );
      _addExceptionSpan(node, () {
        _environment.setVariable(
          node.name,
          value,
          _expressionNode(node.expression),
          namespace: node.namespace,
          global: node.isGlobal,
        );
      });
      return null;
    }
  
    Future<Value?> visitUseRule(UseRule node) async {
      var configuration = const Configuration.empty();
      if (node.configuration.isNotEmpty) {
        var values = <String, ConfiguredValue>{};
        for (var variable in node.configuration) {
          var variableNodeWithSpan = _expressionNode(variable.expression);
          values[variable.name] = ConfiguredValue.explicit(
            _withoutSlash(
              await variable.expression.accept(this),
              variableNodeWithSpan,
            ),
            variable.span,
            variableNodeWithSpan,
          );
        }
        configuration = ExplicitConfiguration(values, node);
      }
  
      await _loadModule(node.url, "@use", node, (module, firstLoad) {
        if (firstLoad) _registerCommentsForModule(module);
        _environment.addModule(module, node, namespace: node.namespace);
      }, configuration: configuration);
      _assertConfigurationIsEmpty(configuration);
  
      return null;
    }
  
    Future<Value?> visitWarnRule(WarnRule node) async {
      var value = await _addExceptionSpanAsync(
        node,
        () => node.expression.accept(this),
      );
      _logger.warn(
        value is SassString ? value.text : _serialize(value, node.expression),
        trace: _stackTrace(node.span),
      );
      return null;
    }
  
    Future<Value?> visitWhileRule(WhileRule node) {
      return _environment.scope(
        () async {
          while ((await node.condition.accept(this)).isTruthy) {
            if (await _handleReturn<Statement>(
              node.children,
              (child) => child.accept(this),
            )
                case var result?) {
              return result;
            }
          }
          return null;
        },
        semiGlobal: true,
        when: node.hasDeclarations,
      );
    }
  
    // ## Expressions
  
    Future<Value> visitBinaryOperationExpression(BinaryOperationExpression node) {
      if (_stylesheet.plainCss &&
          node.operator != BinaryOperator.singleEquals &&
          node.operator != BinaryOperator.dividedBy) {
        throw _exception(
          "Operators aren't allowed in plain CSS.",
          node.operatorSpan,
        );
      }
  
      return _addExceptionSpanAsync(node, () async {
        var left = await node.left.accept(this);
        return switch (node.operator) {
          BinaryOperator.singleEquals => left.singleEquals(
              await node.right.accept(this),
            ),
          BinaryOperator.or =>
            left.isTruthy ? left : await node.right.accept(this),
          BinaryOperator.and =>
            left.isTruthy ? await node.right.accept(this) : left,
          BinaryOperator.equals => SassBoolean(
              left == await node.right.accept(this),
            ),
          BinaryOperator.notEquals => SassBoolean(
              left != await node.right.accept(this),
            ),
          BinaryOperator.greaterThan => left.greaterThan(
              await node.right.accept(this),
            ),
          BinaryOperator.greaterThanOrEquals => left.greaterThanOrEquals(
              await node.right.accept(this),
            ),
          BinaryOperator.lessThan => left.lessThan(await node.right.accept(this)),
          BinaryOperator.lessThanOrEquals => left.lessThanOrEquals(
              await node.right.accept(this),
            ),
          BinaryOperator.plus => left.plus(await node.right.accept(this)),
          BinaryOperator.minus => left.minus(await node.right.accept(this)),
          BinaryOperator.times => left.times(await node.right.accept(this)),
          BinaryOperator.dividedBy => _slash(
              left,
              await node.right.accept(this),
              node,
            ),
          BinaryOperator.modulo => left.modulo(await node.right.accept(this)),
        };
      });
    }
  
    /// Returns the result of the SassScript `/` operation between [left] and
    /// [right] in [node].
    Value _slash(Value left, Value right, BinaryOperationExpression node) {
      var result = left.dividedBy(right);
      switch ((left, right)) {
        case (SassNumber left, SassNumber right)
            when node.allowsSlash &&
                _operandAllowsSlash(node.left) &&
                _operandAllowsSlash(node.right):
          return (result as SassNumber).withSlash(left, right);
  
        case (SassNumber(), SassNumber()):
          String recommendation(Expression expression) => switch (expression) {
                BinaryOperationExpression(
                  operator: BinaryOperator.dividedBy,
                  :var left,
                  :var right,
                ) =>
                  "math.div(${recommendation(left)}, ${recommendation(right)})",
                ParenthesizedExpression() => expression.expression.toString(),
                _ => expression.toString(),
              };
  
          _warn(
            "Using / for division outside of calc() is deprecated "
            "and will be removed in Dart Sass 2.0.0.\n"
            "\n"
            "Recommendation: ${recommendation(node)} or "
            "${expressionToCalc(node)}\n"
            "\n"
            "More info and automated migrator: "
            "https://sass-lang.com/d/slash-div",
            node.span,
            Deprecation.slashDiv,
          );
          return result;
  
        case _:
          return result;
      }
    }
  
    /// Returns whether [node] can be used as a component of a slash-separated
    /// number.
    ///
    /// Although this logic is mostly resolved at parse-time, we can't tell
    /// whether operands will be evaluated as calculations until evaluation-time.
    bool _operandAllowsSlash(Expression node) =>
        node is! FunctionExpression ||
        (node.namespace == null &&
            const {
              "calc", "clamp", "hypot", "sin", "cos", "tan", "asin", "acos", //
              "atan", "sqrt", "exp", "sign", "mod", "rem", "atan2", "pow", //
              "log", "calc-size",
            }.contains(node.name.toLowerCase()) &&
            _environment.getFunction(node.name) == null);
  
    Future<Value> visitValueExpression(ValueExpression node) async => node.value;
  
    Future<Value> visitVariableExpression(VariableExpression node) async {
      var result = _addExceptionSpan(
        node,
        () => _environment.getVariable(node.name, namespace: node.namespace),
      );
      if (result != null) return result;
      throw _exception("Undefined variable.", node.span);
    }
  
    Future<Value> visitUnaryOperationExpression(
      UnaryOperationExpression node,
    ) async {
      var operand = await node.operand.accept(this);
      return _addExceptionSpan(node, () {
        return switch (node.operator) {
          UnaryOperator.plus => operand.unaryPlus(),
          UnaryOperator.minus => operand.unaryMinus(),
          UnaryOperator.divide => operand.unaryDivide(),
          UnaryOperator.not => operand.unaryNot(),
        };
      });
    }
  
    Future<SassBoolean> visitBooleanExpression(BooleanExpression node) async =>
        SassBoolean(node.value);
  
    Future<Value> visitIfExpression(IfExpression node) async {
      var (positional, named) = await _evaluateMacroArguments(node);
      _verifyArguments(positional.length, named, IfExpression.declaration, node);
  
      // ignore: prefer_is_empty
      var condition = positional.elementAtOrNull(0) ?? named["condition"]!;
      var ifTrue = positional.elementAtOrNull(1) ?? named["if-true"]!;
      var ifFalse = positional.elementAtOrNull(2) ?? named["if-false"]!;
  
      var result = (await condition.accept(this)).isTruthy ? ifTrue : ifFalse;
      return _withoutSlash(await result.accept(this), _expressionNode(result));
    }
  
    Future<Value> visitNullExpression(NullExpression node) async => sassNull;
  
    Future<SassNumber> visitNumberExpression(NumberExpression node) async =>
        SassNumber(node.value, node.unit);
  
    Future<Value> visitParenthesizedExpression(ParenthesizedExpression node) =>
        _stylesheet.plainCss
            ? throw _exception(
                "Parentheses aren't allowed in plain CSS.",
                node.span,
              )
            : node.expression.accept(this);
  
    Future<SassColor> visitColorExpression(ColorExpression node) async =>
        node.value;
  
    Future<SassList> visitListExpression(ListExpression node) async => SassList(
          await mapAsync(
            node.contents,
            (Expression expression) => expression.accept(this),
          ),
          node.separator,
          brackets: node.hasBrackets,
        );
  
    Future<SassMap> visitMapExpression(MapExpression node) async {
      var map = <Value, Value>{};
      var keyNodes = <Value, AstNode>{};
      for (var (key, value) in node.pairs) {
        var keyValue = await key.accept(this);
        var valueValue = await value.accept(this);
  
        if (map.containsKey(keyValue)) {
          var oldValueSpan = keyNodes[keyValue]?.span;
          throw MultiSpanSassRuntimeException(
            'Duplicate key.',
            key.span,
            'second key',
            {if (oldValueSpan != null) oldValueSpan: 'first key'},
            _stackTrace(key.span),
          );
        }
        map[keyValue] = valueValue;
        keyNodes[keyValue] = key;
      }
      return SassMap(map);
    }
  
    Future<Value> visitFunctionExpression(FunctionExpression node) async {
      var function = _stylesheet.plainCss
          ? null
          : _addExceptionSpan(
              node,
              () => _environment.getFunction(
                node.name,
                namespace: node.namespace,
              ),
            );
      if (function == null) {
        if (node.namespace != null) {
          throw _exception("Undefined function.", node.span);
        }
  
        // Note that the list of calculation functions is also tracked in
        // lib/src/visitor/is_plain_css_safe.dart.
        switch (node.name.toLowerCase()) {
          case ("min" || "max" || "round" || "abs") && var name
              when node.arguments.named.isEmpty &&
                  node.arguments.rest == null &&
                  node.arguments.positional.every(
                    (argument) => argument.isCalculationSafe,
                  ):
            return await _visitCalculation(node, inLegacySassFunction: name);
  
          case "calc" ||
                "clamp" ||
                "hypot" ||
                "sin" ||
                "cos" ||
                "tan" ||
                "asin" ||
                "acos" ||
                "atan" ||
                "sqrt" ||
                "exp" ||
                "sign" ||
                "mod" ||
                "rem" ||
                "atan2" ||
                "pow" ||
                "log" ||
                "calc-size":
            return await _visitCalculation(node);
        }
  
        function = (_stylesheet.plainCss ? null : _builtInFunctions[node.name]) ??
            PlainCssCallable(node.originalName);
      }
  
      if (node.originalName.startsWith('--') &&
          function is UserDefinedCallable &&
          !function.declaration.originalName.startsWith('--')) {
        _warn(
          'Sass @function names beginning with -- are deprecated for forward-'
          'compatibility with plain CSS functions.\n'
          '\n'
          'For details, see https://sass-lang.com/d/css-function-mixin',
          node.nameSpan,
          Deprecation.cssFunctionMixin,
        );
      }
  
      var oldInFunction = _inFunction;
      _inFunction = true;
      var result = await _addErrorSpan(
        node,
        () => _runFunctionCallable(node.arguments, function, node),
      );
      _inFunction = oldInFunction;
      return result;
    }
  
    /// Evaluates [node] as a calculation.
    ///
    /// If [inLegacySassFunction] isn't null, this allows unitless numbers to be
    /// added and subtracted with numbers with units, for backwards-compatibility
    /// with the old global `min()`, `max()`, `round()`, and `abs()` functions.
    /// The parameter is the name of the function, which is used for reporting
    /// deprecation warnings.
    Future<Value> _visitCalculation(
      FunctionExpression node, {
      String? inLegacySassFunction,
    }) async {
      if (node.arguments.named.isNotEmpty) {
        throw _exception(
          "Keyword arguments can't be used with calculations.",
          node.span,
        );
      } else if (node.arguments.rest != null) {
        throw _exception(
          "Rest arguments can't be used with calculations.",
          node.span,
        );
      }
  
      _checkCalculationArguments(node);
      var arguments = [
        for (var argument in node.arguments.positional)
          await _visitCalculationExpression(
            argument,
            inLegacySassFunction: inLegacySassFunction,
          ),
      ];
      if (_inSupportsDeclaration) {
        return SassCalculation.unsimplified(node.name, arguments);
      }
  
      var oldCallableNode = _callableNode;
      _callableNode = node;
  
      try {
        return switch (node.name.toLowerCase()) {
          "calc" => SassCalculation.calc(arguments[0]),
          "sqrt" => SassCalculation.sqrt(arguments[0]),
          "sin" => SassCalculation.sin(arguments[0]),
          "cos" => SassCalculation.cos(arguments[0]),
          "tan" => SassCalculation.tan(arguments[0]),
          "asin" => SassCalculation.asin(arguments[0]),
          "acos" => SassCalculation.acos(arguments[0]),
          "atan" => SassCalculation.atan(arguments[0]),
          "abs" => SassCalculation.abs(arguments[0]),
          "exp" => SassCalculation.exp(arguments[0]),
          "sign" => SassCalculation.sign(arguments[0]),
          "min" => SassCalculation.min(arguments),
          "max" => SassCalculation.max(arguments),
          "hypot" => SassCalculation.hypot(arguments),
          "pow" => SassCalculation.pow(
              arguments[0],
              arguments.elementAtOrNull(1),
            ),
          "atan2" => SassCalculation.atan2(
              arguments[0],
              arguments.elementAtOrNull(1),
            ),
          "log" => SassCalculation.log(
              arguments[0],
              arguments.elementAtOrNull(1),
            ),
          "mod" => SassCalculation.mod(
              arguments[0],
              arguments.elementAtOrNull(1),
            ),
          "rem" => SassCalculation.rem(
              arguments[0],
              arguments.elementAtOrNull(1),
            ),
          "round" => SassCalculation.roundInternal(
              arguments[0],
              arguments.elementAtOrNull(1),
              arguments.elementAtOrNull(2),
              span: node.span,
              inLegacySassFunction: inLegacySassFunction,
              warn: (message, [deprecation]) =>
                  _warn(message, node.span, deprecation),
            ),
          "clamp" => SassCalculation.clamp(
              arguments[0],
              arguments.elementAtOrNull(1),
              arguments.elementAtOrNull(2),
            ),
          "calc-size" => SassCalculation.calcSize(
              arguments[0],
              arguments.elementAtOrNull(1),
            ),
          _ => throw UnsupportedError('Unknown calculation name "${node.name}".'),
        };
      } on SassScriptException catch (error, stackTrace) {
        // The simplification logic in the [SassCalculation] static methods will
        // throw an error if the arguments aren't compatible, but we have access
        // to the original spans so we can throw a more informative error.
        if (error.message.contains("compatible")) {
          _verifyCompatibleNumbers(arguments, node.arguments.positional);
        }
        throwWithTrace(_exception(error.message, node.span), error, stackTrace);
      } finally {
        _callableNode = oldCallableNode;
      }
    }
  
    /// Verifies that the calculation [node] has the correct number of arguments.
    void _checkCalculationArguments(FunctionExpression node) {
      void check([int? maxArgs]) {
        if (node.arguments.positional.isEmpty) {
          throw _exception("Missing argument.", node.span);
        } else if (maxArgs != null &&
            node.arguments.positional.length > maxArgs) {
          throw _exception(
            "Only $maxArgs ${pluralize('argument', maxArgs)} allowed, but "
                    "${node.arguments.positional.length} " +
                pluralize(
                  'was',
                  node.arguments.positional.length,
                  plural: 'were',
                ) +
                " passed.",
            node.span,
          );
        }
      }
  
      switch (node.name.toLowerCase()) {
        case "calc" ||
              "sqrt" ||
              "sin" ||
              "cos" ||
              "tan" ||
              "asin" ||
              "acos" ||
              "atan" ||
              "abs" ||
              "exp" ||
              "sign":
          check(1);
        case "min" || "max" || "hypot":
          check();
        case "pow" || "atan2" || "log" || "mod" || "rem" || "calc-size":
          check(2);
        case "round" || "clamp":
          check(3);
        case _:
          throw UnsupportedError('Unknown calculation name "${node.name}".');
      }
    }
  
    /// Verifies that [args] all have compatible units that can be used for CSS
    /// calculations, and throws a [SassException] if not.
    ///
    /// The [nodesWithSpans] should correspond to the spans for [args].
    void _verifyCompatibleNumbers(
      List<Object> args,
      List<AstNode> nodesWithSpans,
    ) {
      // Note: this logic is largely duplicated in
      // SassCalculation._verifyCompatibleNumbers and most changes here should
      // also be reflected there.
      for (var i = 0; i < args.length; i++) {
        if (args[i] case SassNumber arg when arg.hasComplexUnits) {
          throw _exception(
            "Number $arg isn't compatible with CSS calculations.",
            nodesWithSpans[i].span,
          );
        }
      }
  
      for (var i = 0; i < args.length - 1; i++) {
        var number1 = args[i];
        if (number1 is! SassNumber) continue;
  
        for (var j = i + 1; j < args.length; j++) {
          var number2 = args[j];
          if (number2 is! SassNumber) continue;
          if (number1.hasPossiblyCompatibleUnits(number2)) continue;
  
          throw MultiSpanSassRuntimeException(
            "$number1 and $number2 are incompatible.",
            nodesWithSpans[i].span,
            number1.toString(),
            {nodesWithSpans[j].span: number2.toString()},
            _stackTrace(nodesWithSpans[i].span),
          );
        }
      }
    }
  
    /// Evaluates [node] as a component of a calculation.
    ///
    /// If [inLegacySassFunction] isn't null, this allows unitless numbers to be
    /// added and subtracted with numbers with units, for backwards-compatibility
    /// with the old global `min()`, `max()`, `round()`, and `abs()` functions.
    /// The parameter is the name of the function, which is used for reporting
    /// deprecation warnings.
    Future<Object> _visitCalculationExpression(
      Expression node, {
      required String? inLegacySassFunction,
    }) async {
      switch (node) {
        case ParenthesizedExpression(expression: var inner):
          var result = await _visitCalculationExpression(
            inner,
            inLegacySassFunction: inLegacySassFunction,
          );
          return result is SassString
              ? SassString('(${result.text})', quotes: false)
              : result;
  
        case StringExpression() when node.isCalculationSafe:
          assert(!node.hasQuotes);
          return switch (node.text.asPlain?.toLowerCase()) {
            'pi' => SassNumber(math.pi),
            'e' => SassNumber(math.e),
            'infinity' => SassNumber(double.infinity),
            '-infinity' => SassNumber(double.negativeInfinity),
            'nan' => SassNumber(double.nan),
            _ => SassString(
                await _performInterpolation(node.text),
                quotes: false,
              ),
          };
  
        case BinaryOperationExpression(:var operator, :var left, :var right):
          _checkWhitespaceAroundCalculationOperator(node);
          return await _addExceptionSpanAsync(
            node,
            () async => SassCalculation.operateInternal(
              _binaryOperatorToCalculationOperator(operator, node),
              await _visitCalculationExpression(
                left,
                inLegacySassFunction: inLegacySassFunction,
              ),
              await _visitCalculationExpression(
                right,
                inLegacySassFunction: inLegacySassFunction,
              ),
              inLegacySassFunction: inLegacySassFunction,
              simplify: !_inSupportsDeclaration,
              warn: (message, [deprecation]) =>
                  _warn(message, node.span, deprecation),
            ),
          );
  
        case NumberExpression() ||
              VariableExpression() ||
              FunctionExpression() ||
              IfExpression():
          return switch (await node.accept(this)) {
            SassNumber result => result,
            SassCalculation result => result,
            SassString result when !result.hasQuotes => result,
            var result => throw _exception(
                "Value $result can't be used in a calculation.",
                node.span,
              ),
          };
  
        case ListExpression(
            hasBrackets: false,
            separator: ListSeparator.space,
            contents: [_, _, ...],
          ):
          var elements = [
            for (var element in node.contents)
              await _visitCalculationExpression(
                element,
                inLegacySassFunction: inLegacySassFunction,
              ),
          ];
  
          _checkAdjacentCalculationValues(elements, node);
  
          for (var i = 0; i < elements.length; i++) {
            if (elements[i] is CalculationOperation &&
                node.contents[i] is ParenthesizedExpression) {
              elements[i] = SassString("(${elements[i]})", quotes: false);
            }
          }
  
          return SassString(elements.join(' '), quotes: false);
  
        case _:
          assert(!node.isCalculationSafe);
          throw _exception(
            "This expression can't be used in a calculation.",
            node.span,
          );
      }
    }
  
    /// Throws an error if [node] requires whitespace around its operator in a
    /// calculation but doesn't have it.
    void _checkWhitespaceAroundCalculationOperator(
      BinaryOperationExpression node,
    ) {
      if (node.operator != BinaryOperator.plus &&
          node.operator != BinaryOperator.minus) {
        return;
      }
  
      // We _should_ never be able to violate these conditions since we always
      // parse binary operations from a single file, but it's better to be safe
      // than have this crash bizarrely.
      if (node.left.span.file != node.right.span.file) return;
      if (node.left.span.end.offset >= node.right.span.start.offset) return;
  
      var textBetweenOperands = node.left.span.file.getText(
        node.left.span.end.offset,
        node.right.span.start.offset,
      );
      var first = textBetweenOperands.codeUnitAt(0);
      var last = textBetweenOperands.codeUnitAt(textBetweenOperands.length - 1);
      if (!(first.isWhitespace || first == $slash) ||
          !(last.isWhitespace || last == $slash)) {
        throw _exception(
          '"+" and "-" must be surrounded by whitespace in calculations.',
          node.operatorSpan,
        );
      }
    }
  
    /// Returns the [CalculationOperator] that corresponds to [operator].
    CalculationOperator _binaryOperatorToCalculationOperator(
      BinaryOperator operator,
      BinaryOperationExpression node,
    ) =>
        switch (operator) {
          BinaryOperator.plus => CalculationOperator.plus,
          BinaryOperator.minus => CalculationOperator.minus,
          BinaryOperator.times => CalculationOperator.times,
          BinaryOperator.dividedBy => CalculationOperator.dividedBy,
          _ => throw _exception(
              "This operation can't be used in a calculation.",
              node.operatorSpan,
            ),
        };
  
    /// Throws an error if [elements] contains two adjacent non-string values.
    void _checkAdjacentCalculationValues(
      List<Object> elements,
      ListExpression node,
    ) {
      assert(elements.length > 1);
  
      for (var i = 1; i < elements.length; i++) {
        var previous = elements[i - 1];
        var current = elements[i];
        if (previous is SassString || current is SassString) continue;
  
        var previousNode = node.contents[i - 1];
        var currentNode = node.contents[i];
        if (currentNode
            case UnaryOperationExpression(
                  operator: UnaryOperator.minus || UnaryOperator.plus,
                ) ||
                NumberExpression(value: < 0)) {
          // `calc(1 -2)` parses as a space-separated list whose second value is a
          // unary operator or a negative number, but just saying it's an invalid
          // expression doesn't help the user understand what's going wrong. We
          // add special case error handling to help clarify the issue.
          throw _exception(
            '"+" and "-" must be surrounded by whitespace in calculations.',
            currentNode.span.subspan(0, 1),
          );
        } else {
          throw _exception(
            'Missing math operator.',
            previousNode.span.expand(currentNode.span),
          );
        }
      }
    }
  
    Future<Value> visitInterpolatedFunctionExpression(
      InterpolatedFunctionExpression node,
    ) async {
      var function = PlainCssCallable(await _performInterpolation(node.name));
      var oldInFunction = _inFunction;
      _inFunction = true;
      var result = await _addErrorSpan(
        node,
        () => _runFunctionCallable(node.arguments, function, node),
      );
      _inFunction = oldInFunction;
      return result;
    }
  
    /// Evaluates the arguments in [arguments] as applied to [callable], and
    /// invokes [run] in a scope with those arguments defined.
    Future<V> _runUserDefinedCallable<V extends Value?>(
      ArgumentList arguments,
      UserDefinedCallable<AsyncEnvironment> callable,
      AstNode nodeWithSpan,
      Future<V> run(),
    ) async {
      // TODO(nweiz): Set [trackSpans] to `null` once we're no longer emitting
      // deprecation warnings for /-as-division.
      var evaluated = await _evaluateArguments(arguments);
  
      var name = callable.name;
      if (name != "@content") name += "()";
  
      var oldInDependency = _inDependency;
      _inDependency = callable.inDependency;
      var result = await _withStackFrame(name, nodeWithSpan, () {
        // Add an extra closure() call so that modifications to the environment
        // don't affect the underlying environment closure.
        return _withEnvironment(callable.environment.closure(), () {
          return _environment.scope(() async {
            _verifyArguments(
              evaluated.positional.length,
              evaluated.named,
              callable.declaration.parameters,
              nodeWithSpan,
            );
  
            var parameters = callable.declaration.parameters.parameters;
            var minLength = math.min(
              evaluated.positional.length,
              parameters.length,
            );
            for (var i = 0; i < minLength; i++) {
              _environment.setLocalVariable(
                parameters[i].name,
                evaluated.positional[i],
                evaluated.positionalNodes[i],
              );
            }
  
            for (var i = evaluated.positional.length;
                i < parameters.length;
                i++) {
              var parameter = parameters[i];
              var value = evaluated.named.remove(parameter.name) ??
                  _withoutSlash(
                    await parameter.defaultValue!.accept<Future<Value>>(this),
                    _expressionNode(parameter.defaultValue!),
                  );
              _environment.setLocalVariable(
                parameter.name,
                value,
                evaluated.namedNodes[parameter.name] ??
                    _expressionNode(parameter.defaultValue!),
              );
            }
  
            SassArgumentList? argumentList;
            var restParameter = callable.declaration.parameters.restParameter;
            if (restParameter != null) {
              var rest = evaluated.positional.length > parameters.length
                  ? evaluated.positional.sublist(parameters.length)
                  : const <Value>[];
              argumentList = SassArgumentList(
                rest,
                evaluated.named,
                evaluated.separator == ListSeparator.undecided
                    ? ListSeparator.comma
                    : evaluated.separator,
              );
              _environment.setLocalVariable(
                restParameter,
                argumentList,
                nodeWithSpan,
              );
            }
  
            var result = await run();
  
            if (argumentList == null) return result;
            if (evaluated.named.isEmpty) return result;
            if (argumentList.wereKeywordsAccessed) return result;
  
            var parameterWord = pluralize(
              'parameter',
              evaluated.named.keys.length,
            );
            var parameterNames = toSentence(
              evaluated.named.keys.map((name) => "\$$name"),
              'or',
            );
            throw MultiSpanSassRuntimeException(
              "No $parameterWord named $parameterNames.",
              nodeWithSpan.span,
              "invocation",
              {callable.declaration.parameters.spanWithName: "declaration"},
              _stackTrace(nodeWithSpan.span),
            );
          });
        });
      });
      _inDependency = oldInDependency;
      return result;
    }
  
    /// Evaluates [arguments] as applied to [callable].
    Future<Value> _runFunctionCallable(
      ArgumentList arguments,
      AsyncCallable? callable,
      AstNode nodeWithSpan,
    ) async {
      if (callable is AsyncBuiltInCallable) {
        return _withoutSlash(
          await _runBuiltInCallable(arguments, callable, nodeWithSpan),
          nodeWithSpan,
        );
      } else if (callable is UserDefinedCallable<AsyncEnvironment>) {
        return await _runUserDefinedCallable(
          arguments,
          callable,
          nodeWithSpan,
          () async {
            for (var statement in callable.declaration.children) {
              var returnValue = await statement.accept(this);
              if (returnValue is Value) return returnValue;
            }
  
            throw _exception(
              "Function finished without @return.",
              callable.declaration.span,
            );
          },
        );
      } else if (callable is PlainCssCallable) {
        if (arguments.named.isNotEmpty || arguments.keywordRest != null) {
          throw _exception(
            "Plain CSS functions don't support keyword arguments.",
            nodeWithSpan.span,
          );
        }
  
        var buffer = StringBuffer("${callable.name}(");
        try {
          var first = true;
          for (var argument in arguments.positional) {
            if (first) {
              first = false;
            } else {
              buffer.write(", ");
            }
  
            buffer.write(await _evaluateToCss(argument));
          }
  
          var restArg = arguments.rest;
          if (restArg != null) {
            var rest = await restArg.accept(this);
            if (!first) buffer.write(", ");
            buffer.write(_serialize(rest, restArg));
          }
        } on SassRuntimeException catch (error) {
          if (!error.message.endsWith("isn't a valid CSS value.")) rethrow;
          throw MultiSpanSassRuntimeException(
            error.message,
            error.span,
            "value",
            {nodeWithSpan.span: "unknown function treated as plain CSS"},
            error.trace,
          );
        }
        buffer.writeCharCode($rparen);
  
        return SassString(buffer.toString(), quotes: false);
      } else {
        throw ArgumentError('Unknown callable type ${callable.runtimeType}.');
      }
    }
  
    /// Evaluates [invocation] as applied to [callable], and invokes [callable]'s
    /// body.
    Future<Value> _runBuiltInCallable(
      ArgumentList arguments,
      AsyncBuiltInCallable callable,
      AstNode nodeWithSpan,
    ) async {
      var evaluated = await _evaluateArguments(arguments);
  
      var oldCallableNode = _callableNode;
      _callableNode = nodeWithSpan;
  
      var namedSet = MapKeySet(evaluated.named);
      var (overload, callback) = callable.callbackFor(
        evaluated.positional.length,
        namedSet,
      );
      _addExceptionSpan(
        nodeWithSpan,
        () => overload.verify(evaluated.positional.length, namedSet),
      );
  
      var parameters = overload.parameters;
      for (var i = evaluated.positional.length; i < parameters.length; i++) {
        var parameter = parameters[i];
        evaluated.positional.add(
          evaluated.named.remove(parameter.name) ??
              _withoutSlash(
                await parameter.defaultValue!.accept(this),
                parameter.defaultValue!,
              ),
        );
      }
  
      SassArgumentList? argumentList;
      if (overload.restParameter != null) {
        var rest = const <Value>[];
        if (evaluated.positional.length > parameters.length) {
          rest = evaluated.positional.sublist(parameters.length);
          evaluated.positional.removeRange(
            parameters.length,
            evaluated.positional.length,
          );
        }
  
        argumentList = SassArgumentList(
          rest,
          evaluated.named,
          evaluated.separator == ListSeparator.undecided
              ? ListSeparator.comma
              : evaluated.separator,
        );
        evaluated.positional.add(argumentList);
      }
  
      Value result;
      try {
        result = await _addExceptionSpanAsync(
          nodeWithSpan,
          () => callback(evaluated.positional),
        );
      } on SassException {
        rethrow;
      } catch (error, stackTrace) {
        throwWithTrace(
          _exception(_getErrorMessage(error), nodeWithSpan.span),
          error,
          stackTrace,
        );
      }
      _callableNode = oldCallableNode;
  
      if (argumentList == null) return result;
      if (evaluated.named.isEmpty) return result;
      if (argumentList.wereKeywordsAccessed) return result;
  
      throw MultiSpanSassRuntimeException(
        "No ${pluralize('parameter', evaluated.named.keys.length)} named "
            "${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.",
        nodeWithSpan.span,
        "invocation",
        {overload.spanWithName: "declaration"},
        _stackTrace(nodeWithSpan.span),
      );
    }
  
    /// Returns the evaluated values of the given [arguments].
    Future<_ArgumentResults> _evaluateArguments(ArgumentList arguments) async {
      // TODO(nweiz): This used to avoid tracking source spans for arguments if
      // [_sourceMap]s was false or it was being called from
      // [_runBuiltInCallable]. We always have to track them now to produce better
      // warnings for /-as-division, but once those warnings are gone we should go
      // back to tracking conditionally.
  
      var positional = <Value>[];
      var positionalNodes = <AstNode>[];
      for (var expression in arguments.positional) {
        var nodeForSpan = _expressionNode(expression);
        positional.add(_withoutSlash(await expression.accept(this), nodeForSpan));
        positionalNodes.add(nodeForSpan);
      }
  
      var named = <String, Value>{};
      var namedNodes = <String, AstNode>{};
      for (var (name, value) in arguments.named.pairs) {
        var nodeForSpan = _expressionNode(value);
        named[name] = _withoutSlash(await value.accept(this), nodeForSpan);
        namedNodes[name] = nodeForSpan;
      }
  
      var restArgs = arguments.rest;
      if (restArgs == null) {
        return (
          positional: positional,
          positionalNodes: positionalNodes,
          named: named,
          namedNodes: namedNodes,
          separator: ListSeparator.undecided,
        );
      }
  
      var rest = await restArgs.accept(this);
      var restNodeForSpan = _expressionNode(restArgs);
      var separator = ListSeparator.undecided;
      if (rest is SassMap) {
        _addRestMap(named, rest, restArgs, (value) => value);
        namedNodes.addAll({
          for (var key in rest.contents.keys)
            (key as SassString).text: restNodeForSpan,
        });
      } else if (rest is SassList) {
        positional.addAll(
          rest.asList.map((value) => _withoutSlash(value, restNodeForSpan)),
        );
        positionalNodes.addAll(List.filled(rest.lengthAsList, restNodeForSpan));
        separator = rest.separator;
  
        if (rest is SassArgumentList) {
          rest.keywords.forEach((key, value) {
            named[key] = _withoutSlash(value, restNodeForSpan);
            namedNodes[key] = restNodeForSpan;
          });
        }
      } else {
        positional.add(_withoutSlash(rest, restNodeForSpan));
        positionalNodes.add(restNodeForSpan);
      }
  
      var keywordRestArgs = arguments.keywordRest;
      if (keywordRestArgs == null) {
        return (
          positional: positional,
          positionalNodes: positionalNodes,
          named: named,
          namedNodes: namedNodes,
          separator: separator,
        );
      }
  
      var keywordRest = await keywordRestArgs.accept(this);
      var keywordRestNodeForSpan = _expressionNode(keywordRestArgs);
      if (keywordRest is SassMap) {
        _addRestMap(named, keywordRest, keywordRestArgs, (value) => value);
        namedNodes.addAll({
          for (var key in keywordRest.contents.keys)
            (key as SassString).text: keywordRestNodeForSpan,
        });
        return (
          positional: positional,
          positionalNodes: positionalNodes,
          named: named,
          namedNodes: namedNodes,
          separator: separator,
        );
      } else {
        throw _exception(
          "Variable keyword arguments must be a map (was $keywordRest).",
          keywordRestArgs.span,
        );
      }
    }
  
    /// Evaluates the arguments in [arguments] only as much as necessary to
    /// separate out positional and named arguments.
    ///
    /// Returns the arguments as expressions so that they can be lazily evaluated
    /// for macros such as `if()`.
    Future<(List<Expression> positional, Map<String, Expression> named)>
        _evaluateMacroArguments(CallableInvocation invocation) async {
      var restArgs_ = invocation.arguments.rest;
      if (restArgs_ == null) {
        return (invocation.arguments.positional, invocation.arguments.named);
      }
      var restArgs = restArgs_; // dart-lang/sdk#45348
  
      var positional = invocation.arguments.positional.toList();
      var named = Map.of(invocation.arguments.named);
      var rest = await restArgs.accept(this);
      var restNodeForSpan = _expressionNode(restArgs);
      if (rest is SassMap) {
        _addRestMap(
          named,
          rest,
          invocation,
          (value) => ValueExpression(value, restArgs.span),
        );
      } else if (rest is SassList) {
        positional.addAll(
          rest.asList.map(
            (value) => ValueExpression(
              _withoutSlash(value, restNodeForSpan),
              restArgs.span,
            ),
          ),
        );
        if (rest is SassArgumentList) {
          rest.keywords.forEach((key, value) {
            named[key] = ValueExpression(
              _withoutSlash(value, restNodeForSpan),
              restArgs.span,
            );
          });
        }
      } else {
        positional.add(
          ValueExpression(_withoutSlash(rest, restNodeForSpan), restArgs.span),
        );
      }
  
      var keywordRestArgs_ = invocation.arguments.keywordRest;
      if (keywordRestArgs_ == null) return (positional, named);
      var keywordRestArgs = keywordRestArgs_; // dart-lang/sdk#45348
  
      var keywordRest = await keywordRestArgs.accept(this);
      var keywordRestNodeForSpan = _expressionNode(keywordRestArgs);
      if (keywordRest is SassMap) {
        _addRestMap(
          named,
          keywordRest,
          invocation,
          (value) => ValueExpression(
            _withoutSlash(value, keywordRestNodeForSpan),
            keywordRestArgs.span,
          ),
        );
        return (positional, named);
      } else {
        throw _exception(
          "Variable keyword arguments must be a map (was $keywordRest).",
          keywordRestArgs.span,
        );
      }
    }
  
    /// Adds the values in [map] to [values].
    ///
    /// Throws a [SassRuntimeException] associated with [nodeWithSpan]'s source
    /// span if any [map] keys aren't strings.
    ///
    /// If [convert] is passed, that's used to convert the map values to the value
    /// type for [values]. Otherwise, the [Value]s are used as-is.
    ///
    /// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling
    /// [AstNode.span] if the span isn't required, since some nodes need to do
    /// real work to manufacture a source span.
    void _addRestMap<T>(
      Map<String, T> values,
      SassMap map,
      AstNode nodeWithSpan,
      T convert(Value value),
    ) {
      var expressionNode = _expressionNode(nodeWithSpan);
      map.contents.forEach((key, value) {
        if (key is SassString) {
          values[key.text] = convert(_withoutSlash(value, expressionNode));
        } else {
          throw _exception(
            "Variable keyword argument map must have string keys.\n"
            "$key is not a string in $map.",
            nodeWithSpan.span,
          );
        }
      });
    }
  
    /// Throws a [SassRuntimeException] if [positional] and [named] aren't valid
    /// when applied to [parameters].
    void _verifyArguments(
      int positional,
      Map<String, dynamic> named,
      ParameterList parameters,
      AstNode nodeWithSpan,
    ) =>
        _addExceptionSpan(
          nodeWithSpan,
          () => parameters.verify(positional, MapKeySet(named)),
        );
  
    Future<Value> visitSelectorExpression(SelectorExpression node) async =>
        _styleRuleIgnoringAtRoot?.originalSelector.asSassList ?? sassNull;
  
    Future<SassString> visitStringExpression(StringExpression node) async {
      // Don't use [performInterpolation] here because we need to get the raw text
      // from strings, rather than the semantic value.
      var oldInSupportsDeclaration = _inSupportsDeclaration;
      _inSupportsDeclaration = false;
      var result = SassString(
        [
          for (var value in node.text.contents)
            switch (value) {
              String() => value,
              Expression() => switch (await value.accept(this)) {
                  SassString(:var text) => text,
                  var result => _serialize(result, value, quote: false),
                },
              _ => throw UnsupportedError("Unknown interpolation value $value"),
            },
        ].join(),
        quotes: node.hasQuotes,
      );
      _inSupportsDeclaration = oldInSupportsDeclaration;
      return result;
    }
  
    Future<SassString> visitSupportsExpression(
      SupportsExpression expression,
    ) async =>
        SassString(
          await _visitSupportsCondition(expression.condition),
          quotes: false,
        );
  
    // ## Plain CSS
  
    // These methods are used when evaluating CSS syntax trees from `@import`ed
    // stylesheets that themselves contain `@use` rules, and CSS included via the
    // `load-css()` function. When we load a module using one of these constructs,
    // we first convert it to CSS (we can't evaluate it as Sass directly because
    // it may be used elsewhere and it must only be evaluated once). Then we
    // execute that CSS more or less as though it were Sass (we can't inject it
    // into the stylesheet as-is because the `@import` may be nested in other
    // rules). That's what these rules implement.
  
    Future<void> visitCssAtRule(CssAtRule node) async {
      // NOTE: this logic is largely duplicated in [visitAtRule]. Most changes
      // here should be mirrored there.
  
      if (_declarationName != null) {
        throw _exception(
          "At-rules may not be used within nested declarations.",
          node.span,
        );
      }
  
      if (node.isChildless) {
        _copyParentAfterSibling();
        _parent.addChild(
          ModifiableCssAtRule(
            node.name,
            node.span,
            childless: true,
            value: node.value,
          ),
        );
        return;
      }
  
      var wasInKeyframes = _inKeyframes;
      var wasInUnknownAtRule = _inUnknownAtRule;
      if (unvendor(node.name.value) == 'keyframes') {
        _inKeyframes = true;
      } else {
        _inUnknownAtRule = true;
      }
  
      await _withParent(
        ModifiableCssAtRule(node.name, node.span, value: node.value),
        () async {
          // We don't have to check for an unknown at-rule in a style rule here,
          // because the previous compilation has already bubbled the at-rule to the
          // root.
          for (var child in node.children) {
            await child.accept(this);
          }
        },
        through: (node) => node is CssStyleRule,
        scopeWhen: false,
      );
  
      _inUnknownAtRule = wasInUnknownAtRule;
      _inKeyframes = wasInKeyframes;
    }
  
    Future<void> visitCssComment(CssComment node) async {
      // NOTE: this logic is largely duplicated in [visitLoudComment]. Most
      // changes here should be mirrored there.
  
      // Comments are allowed to appear between CSS imports.
      if (_parent == _root && _endOfImports == _root.children.length) {
        _endOfImports++;
      }
  
      _copyParentAfterSibling();
      _parent.addChild(ModifiableCssComment(node.text, node.span));
    }
  
    Future<void> visitCssDeclaration(CssDeclaration node) async {
      _copyParentAfterSibling();
      _parent.addChild(
        ModifiableCssDeclaration(
          node.name,
          node.value,
          node.span,
          parsedAsCustomProperty: node.parsedAsCustomProperty,
          valueSpanForMap: node.valueSpanForMap,
        ),
      );
    }
  
    Future<void> visitCssImport(CssImport node) async {
      // NOTE: this logic is largely duplicated in [_visitStaticImport]. Most
      // changes here should be mirrored there.
  
      var modifiableNode = ModifiableCssImport(
        node.url,
        node.span,
        modifiers: node.modifiers,
      );
      if (_parent != _root) {
        _copyParentAfterSibling();
        _parent.addChild(modifiableNode);
      } else if (_endOfImports == _root.children.length) {
        _root.addChild(modifiableNode);
        _endOfImports++;
      } else {
        (_outOfOrderImports ??= []).add(modifiableNode);
      }
    }
  
    Future<void> visitCssKeyframeBlock(CssKeyframeBlock node) async {
      // NOTE: this logic is largely duplicated in [visitStyleRule]. Most changes
      // here should be mirrored there.
  
      var rule = ModifiableCssKeyframeBlock(node.selector, node.span);
      await _withParent(
        rule,
        () async {
          for (var child in node.children) {
            await child.accept(this);
          }
        },
        through: (node) => node is CssStyleRule,
        scopeWhen: false,
      );
    }
  
    Future<void> visitCssMediaRule(CssMediaRule node) async {
      // NOTE: this logic is largely duplicated in [visitMediaRule]. Most changes
      // here should be mirrored there.
  
      if (_declarationName != null) {
        throw _exception(
          "Media rules may not be used within nested declarations.",
          node.span,
        );
      }
  
      var mergedQueries = _mediaQueries.andThen(
        (mediaQueries) => _mergeMediaQueries(mediaQueries, node.queries),
      );
      if (mergedQueries != null && mergedQueries.isEmpty) return;
  
      var mergedSources = mergedQueries == null
          ? const <CssMediaQuery>{}
          : {..._mediaQuerySources!, ..._mediaQueries!, ...node.queries};
  
      await _withParent(
        ModifiableCssMediaRule(mergedQueries ?? node.queries, node.span),
        () async {
          await _withMediaQueries(
            mergedQueries ?? node.queries,
            mergedSources,
            () async {
              if (_styleRule case var styleRule?) {
                // If we're in a style rule, copy it into the media query so that
                // declarations immediately inside @media have somewhere to go.
                //
                // For example, "a {@media screen {b: c}}" should produce
                // "@media screen {a {b: c}}".
                await _withParent(styleRule.copyWithoutChildren(), () async {
                  for (var child in node.children) {
                    await child.accept(this);
                  }
                }, scopeWhen: false);
              } else {
                for (var child in node.children) {
                  await child.accept(this);
                }
              }
            },
          );
        },
        through: (node) =>
            node is CssStyleRule ||
            (mergedSources.isNotEmpty &&
                node is CssMediaRule &&
                node.queries.every(mergedSources.contains)),
        scopeWhen: false,
      );
    }
  
    Future<void> visitCssStyleRule(CssStyleRule node) async {
      // NOTE: this logic is largely duplicated in [visitStyleRule]. Most changes
      // here should be mirrored there.
  
      if (_declarationName != null) {
        throw _exception(
          "Style rules may not be used within nested declarations.",
          node.span,
        );
      } else if (_inKeyframes && _parent is CssKeyframeBlock) {
        throw _exception(
          "Style rules may not be used within keyframe blocks.",
          node.span,
        );
      }
  
      var styleRule = _styleRule;
      var nest = switch (_styleRule) {
        null => true,
        CssStyleRule(fromPlainCss: true) => false,
        _ => !(node.fromPlainCss && node.selector.containsParentSelector)
      };
      var originalSelector = nest
          ? node.selector.nestWithin(
              styleRule?.originalSelector,
              implicitParent: !_atRootExcludingStyleRule,
              preserveParentSelectors: node.fromPlainCss,
            )
          : node.selector;
      var selector = _extensionStore.addSelector(originalSelector, _mediaQueries);
      var rule = ModifiableCssStyleRule(
        selector,
        node.span,
        originalSelector: originalSelector,
        fromPlainCss: node.fromPlainCss,
      );
      var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule;
      _atRootExcludingStyleRule = false;
      await _withParent(
        rule,
        () async {
          await _withStyleRule(rule, () async {
            for (var child in node.children) {
              await child.accept(this);
            }
          });
        },
        through: nest ? (node) => node is CssStyleRule : null,
        scopeWhen: false,
      );
      _atRootExcludingStyleRule = oldAtRootExcludingStyleRule;
  
      if (_parent.children case [..., var lastChild] when styleRule == null) {
        lastChild.isGroupEnd = true;
      }
    }
  
    Future<void> visitCssStylesheet(CssStylesheet node) async {
      for (var statement in node.children) {
        await statement.accept(this);
      }
    }
  
    Future<void> visitCssSupportsRule(CssSupportsRule node) async {
      // NOTE: this logic is largely duplicated in [visitSupportsRule]. Most
      // changes here should be mirrored there.
  
      if (_declarationName != null) {
        throw _exception(
          "Supports rules may not be used within nested declarations.",
          node.span,
        );
      }
  
      await _withParent(
        ModifiableCssSupportsRule(node.condition, node.span),
        () async {
          if (_styleRule case var styleRule?) {
            // If we're in a style rule, copy it into the supports rule so that
            // declarations immediately inside @supports have somewhere to go.
            //
            // For example, "a {@supports (a: b) {b: c}}" should produce "@supports
            // (a: b) {a {b: c}}".
            await _withParent(styleRule.copyWithoutChildren(), () async {
              for (var child in node.children) {
                await child.accept(this);
              }
            });
          } else {
            for (var child in node.children) {
              await child.accept(this);
            }
          }
        },
        through: (node) => node is CssStyleRule,
        scopeWhen: false,
      );
    }
  
    // ## Utilities
  
    /// Runs [callback] for each value in [list] until it returns a [Value].
    ///
    /// Returns the value returned by [callback], or `null` if it only ever
    /// returned `null`.
    Future<Value?> _handleReturn<T>(
      List<T> list,
      Future<Value?> callback(T value),
    ) async {
      for (var value in list) {
        if (await callback(value) case var result?) return result;
      }
      return null;
    }
  
    /// Runs [callback] with [environment] as the current environment.
    Future<T> _withEnvironment<T>(
      AsyncEnvironment environment,
      Future<T> callback(),
    ) async {
      var oldEnvironment = _environment;
      _environment = environment;
      var result = await callback();
      _environment = oldEnvironment;
      return result;
    }
  
    /// Evaluates [interpolation] and wraps the result in a [CssValue].
    ///
    /// If [trim] is `true`, removes whitespace around the result. If
    /// [warnForColor] is `true`, this will emit a warning for any named color
    /// values passed into the interpolation.
    Future<CssValue<String>> _interpolationToValue(
      Interpolation interpolation, {
      bool trim = false,
      bool warnForColor = false,
    }) async {
      var result = await _performInterpolation(
        interpolation,
        warnForColor: warnForColor,
      );
      return CssValue(
        trim ? trimAscii(result, excludeEscape: true) : result,
        interpolation.span,
      );
    }
  
    /// Evaluates [interpolation].
    ///
    /// If [warnForColor] is `true`, this will emit a warning for any named color
    /// values passed into the interpolation.
    Future<String> _performInterpolation(
      Interpolation interpolation, {
      bool warnForColor = false,
    }) async {
      var (result, _) = await _performInterpolationHelper(
        interpolation,
        sourceMap: false,
        warnForColor: warnForColor,
      );
      return result;
    }
  
    /// Like [_performInterpolation], but also returns a [InterpolationMap] that
    /// can map spans from the resulting string back to the original
    /// [interpolation].
    Future<(String, InterpolationMap)> _performInterpolationWithMap(
      Interpolation interpolation, {
      bool warnForColor = false,
    }) async {
      var (result, map) = await _performInterpolationHelper(
        interpolation,
        sourceMap: true,
        warnForColor: warnForColor,
      );
      return (result, map!);
    }
  
    /// A helper that implements the core logic of both [_performInterpolation]
    /// and [_performInterpolationWithMap].
    Future<(String, InterpolationMap?)> _performInterpolationHelper(
      Interpolation interpolation, {
      required bool sourceMap,
      bool warnForColor = false,
    }) async {
      var targetLocations = sourceMap ? <SourceLocation>[] : null;
      var oldInSupportsDeclaration = _inSupportsDeclaration;
      _inSupportsDeclaration = false;
      var buffer = StringBuffer();
      var first = true;
      for (var value in interpolation.contents) {
        if (!first) targetLocations?.add(SourceLocation(buffer.length));
        first = false;
  
        if (value is String) {
          buffer.write(value);
          continue;
        }
  
        var expression = value as Expression;
        var result = await expression.accept(this);
  
        if (warnForColor && namesByColor.containsKey(result)) {
          var alternative = BinaryOperationExpression(
            BinaryOperator.plus,
            StringExpression(
              Interpolation.plain("", interpolation.span),
              quotes: true,
            ),
            expression,
          );
          _warn(
            "You probably don't mean to use the color value "
            "${namesByColor[result]} in interpolation here.\n"
            "It may end up represented as $result, which will likely produce "
            "invalid CSS.\n"
            "Always quote color names when using them as strings or map keys "
            '(for example, "${namesByColor[result]}").\n'
            "If you really want to use the color value here, use '$alternative'.",
            expression.span,
          );
        }
  
        buffer.write(_serialize(result, expression, quote: false));
      }
      _inSupportsDeclaration = oldInSupportsDeclaration;
  
      return (
        buffer.toString(),
        targetLocations.andThen(
          (targetLocations) => InterpolationMap(interpolation, targetLocations),
        ),
      );
    }
  
    /// Evaluates [expression] and calls `toCssString()` and wraps a
    /// [SassScriptException] to associate it with [span].
    Future<String> _evaluateToCss(
      Expression expression, {
      bool quote = true,
    }) async =>
        _serialize(await expression.accept(this), expression, quote: quote);
  
    /// Calls `value.toCssString()` and wraps a [SassScriptException] to associate
    /// it with [nodeWithSpan]'s source span.
    ///
    /// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling
    /// [AstNode.span] if the span isn't required, since some nodes need to do
    /// real work to manufacture a source span.
    String _serialize(Value value, AstNode nodeWithSpan, {bool quote = true}) =>
        _addExceptionSpan(nodeWithSpan, () => value.toCssString(quote: quote));
  
    /// Returns the [AstNode] whose span should be used for [expression].
    ///
    /// If [expression] is a variable reference, [AstNode]'s span will be the span
    /// where that variable was originally declared. Otherwise, this will just
    /// return [expression].
    ///
    /// This returns an [AstNode] rather than a [FileSpan] so we can avoid calling
    /// [AstNode.span] if the span isn't required, since some nodes need to do
    /// real work to manufacture a source span.
    AstNode _expressionNode(AstNode expression) {
      // TODO(nweiz): This used to return [expression] as-is if source map
      // generation was disabled. We always have to track the original location
      // now to produce better warnings for /-as-division, but once those warnings
      // are gone we should go back to short-circuiting.
  
      if (expression is VariableExpression) {
        return _addExceptionSpan(
              expression,
              () => _environment.getVariableNode(
                expression.name,
                namespace: expression.namespace,
              ),
            ) ??
            expression;
      } else {
        return expression;
      }
    }
  
    /// Adds [node] as a child of the current parent, then runs [callback] with
    /// [node] as the current parent.
    ///
    /// If [through] is passed, [node] is added as a child of the first parent for
    /// which [through] returns `false`. That parent is copied unless it's the
    /// lattermost child of its parent.
    ///
    /// Runs [callback] in a new environment scope unless [scopeWhen] is false.
    Future<T> _withParent<S extends ModifiableCssParentNode, T>(
      S node,
      Future<T> callback(), {
      bool through(CssNode node)?,
      bool scopeWhen = true,
    }) async {
      _addChild(node, through: through);
  
      var oldParent = _parent;
      _parent = node;
      var result = await _environment.scope(callback, when: scopeWhen);
      _parent = oldParent;
  
      return result;
    }
  
    /// If the current [_parent] is not the last child of its grandparent, makes a
    /// new childless copy of it and sets [_parent] to that.
    ///
    /// Otherwise, leaves [_parent] as-is.
    void _copyParentAfterSibling() {
      if (_parent.parent case var grandparent?
          when grandparent.children.last != _parent) {
        var newParent = _parent.copyWithoutChildren();
        grandparent.addChild(newParent);
        _parent = newParent;
      }
    }
  
    /// Adds [node] as a child of the current parent.
    ///
    /// If [through] is passed, [node] is added as a child of the first parent for
    /// which [through] returns `false` instead. That parent is copied unless it's the
    /// lattermost child of its parent.
    void _addChild(ModifiableCssNode node, {bool through(CssNode node)?}) {
      // Go up through parents that match [through].
      var parent = _parent;
      if (through != null) {
        while (through(parent)) {
          if (parent.parent case var grandparent?) {
            parent = grandparent;
          } else {
            throw ArgumentError(
              "through() must return false for at least one parent of $node.",
            );
          }
        }
  
        // If the parent has a (visible) following sibling, we shouldn't add to
        // the parent. Instead, we should create a copy and add it after the
        // interstitial sibling.
        if (parent.hasFollowingSibling) {
          // A node with siblings must have a parent
          var grandparent = parent.parent!;
          if (parent.equalsIgnoringChildren(grandparent.children.last)) {
            // If we've already made a copy of [parent] and nothing else has been
            // added after it, re-use it.
            parent = grandparent.children.last as ModifiableCssParentNode;
          } else {
            parent = parent.copyWithoutChildren();
            grandparent.addChild(parent);
          }
        }
      }
  
      parent.addChild(node);
    }
  
    /// Runs [callback] with [rule] as the current style rule.
    Future<T> _withStyleRule<T>(
      ModifiableCssStyleRule rule,
      Future<T> callback(),
    ) async {
      var oldRule = _styleRuleIgnoringAtRoot;
      _styleRuleIgnoringAtRoot = rule;
      var result = await callback();
      _styleRuleIgnoringAtRoot = oldRule;
      return result;
    }
  
    /// Runs [callback] with [queries] as the current media queries.
    ///
    /// This also sets [sources] as the current set of media queries that were
    /// merged together to create [queries]. This is used to determine when it's
    /// safe to bubble one query through another.
    Future<T> _withMediaQueries<T>(
      List<CssMediaQuery>? queries,
      Set<CssMediaQuery>? sources,
      Future<T> callback(),
    ) async {
      var oldMediaQueries = _mediaQueries;
      var oldSources = _mediaQuerySources;
      _mediaQueries = queries;
      _mediaQuerySources = sources;
      var result = await callback();
      _mediaQueries = oldMediaQueries;
      _mediaQuerySources = oldSources;
      return result;
    }
  
    /// Adds a frame to the stack with the given [member] name, and [nodeWithSpan]
    /// as the site of the new frame.
    ///
    /// Runs [callback] with the new stack.
    ///
    /// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling
    /// [AstNode.span] if the span isn't required, since some nodes need to do
    /// real work to manufacture a source span.
    Future<T> _withStackFrame<T>(
      String member,
      AstNode nodeWithSpan,
      Future<T> callback(),
    ) async {
      _stack.add((_member, nodeWithSpan));
      var oldMember = _member;
      _member = member;
      var result = await callback();
      _member = oldMember;
      _stack.removeLast();
      return result;
    }
  
    /// Like [Value.withoutSlash], but produces a deprecation warning if [value]
    /// was a slash-separated number.
    Value _withoutSlash(Value value, AstNode nodeForSpan) {
      if (value case SassNumber(asSlash: _?)) {
        String recommendation(SassNumber number) => switch (number.asSlash) {
              (var before, var after) =>
                "math.div(${recommendation(before)}, ${recommendation(after)})",
              _ => number.toString(),
            };
  
        _warn(
          "Using / for division is deprecated and will be removed in Dart Sass "
          "2.0.0.\n"
          "\n"
          "Recommendation: ${recommendation(value)}\n"
          "\n"
          "More info and automated migrator: "
          "https://sass-lang.com/d/slash-div",
          nodeForSpan.span,
          Deprecation.slashDiv,
        );
      }
  
      return value.withoutSlash();
    }
  
    /// Creates a new stack frame with location information from [member] and
    /// [span].
    Frame _stackFrame(String member, FileSpan span) => frameForSpan(
          span,
          member,
          url:
              span.sourceUrl.andThen((url) => _importCache?.humanize(url) ?? url),
        );
  
    /// Returns a stack trace at the current point.
    ///
    /// If [span] is passed, it's used for the innermost stack frame.
    Trace _stackTrace([FileSpan? span]) {
      var frames = [
        for (var (member, nodeWithSpan) in _stack)
          _stackFrame(member, nodeWithSpan.span),
        if (span != null) _stackFrame(_member, span),
      ];
      return Trace(frames.reversed);
    }
  
    /// Emits a warning with the given [message] about the given [span].
    void _warn(String message, FileSpan span, [Deprecation? deprecation]) {
      if (_quietDeps && _inDependency) return;
  
      if (!_warningsEmitted.add((message, span))) return;
      var trace = _stackTrace(span);
      if (deprecation == null) {
        _logger.warn(message, span: span, trace: trace);
      } else {
        _logger.warnForDeprecation(
          deprecation,
          message,
          span: span,
          trace: trace,
        );
      }
    }
  
    /// Returns a [SassRuntimeException] with the given [message].
    ///
    /// If [span] is passed, it's used for the innermost stack frame.
    SassRuntimeException _exception(String message, [FileSpan? span]) =>
        SassRuntimeException(
          message,
          span ?? _stack.last.$2.span,
          _stackTrace(span),
        );
  
    /// Returns a [MultiSpanSassRuntimeException] with the given [message],
    /// [primaryLabel], and [secondaryLabels].
    ///
    /// The primary span is taken from the current stack trace span.
    SassRuntimeException _multiSpanException(
      String message,
      String primaryLabel,
      Map<FileSpan, String> secondaryLabels,
    ) =>
        MultiSpanSassRuntimeException(
          message,
          _stack.last.$2.span,
          primaryLabel,
          secondaryLabels,
          _stackTrace(),
        );
  
    /// Runs [callback], and converts any [SassScriptException]s it throws to
    /// [SassRuntimeException]s with [nodeWithSpan]'s source span.
    ///
    /// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling
    /// [AstNode.span] if the span isn't required, since some nodes need to do
    /// real work to manufacture a source span.
    ///
    /// If [addStackFrame] is true (the default), this will add an innermost stack
    /// frame for [nodeWithSpan]. Otherwise, it will use the existing stack as-is.
    T _addExceptionSpan<T>(
      AstNode nodeWithSpan,
      T callback(), {
      bool addStackFrame = true,
    }) {
      try {
        return callback();
      } on SassScriptException catch (error, stackTrace) {
        throwWithTrace(
          error
              .withSpan(nodeWithSpan.span)
              .withTrace(_stackTrace(addStackFrame ? nodeWithSpan.span : null)),
          error,
          stackTrace,
        );
      }
    }
  
    /// Like [_addExceptionSpan], but for an asynchronous [callback].
    Future<T> _addExceptionSpanAsync<T>(
      AstNode nodeWithSpan,
      FutureOr<T> callback(), {
      bool addStackFrame = true,
    }) async {
      try {
        return await callback();
      } on SassScriptException catch (error, stackTrace) {
        throwWithTrace(
          error
              .withSpan(nodeWithSpan.span)
              .withTrace(_stackTrace(addStackFrame ? nodeWithSpan.span : null)),
          error,
          stackTrace,
        );
      }
    }
  
    /// Runs [callback], and converts any [SassException]s that aren't already
    /// [SassRuntimeException]s to [SassRuntimeException]s with the current stack
    /// trace.
    Future<T> _addExceptionTrace<T>(FutureOr<T> callback()) async {
      try {
        return await callback();
      } on SassRuntimeException {
        rethrow;
      } on SassException catch (error, stackTrace) {
        throwWithTrace(
          error.withTrace(_stackTrace(error.span)),
          error,
          stackTrace,
        );
      }
    }
  
    /// Runs [callback], and converts any [SassRuntimeException]s containing an
    /// @error to throw a more relevant [SassRuntimeException] with [nodeWithSpan]'s
    /// source span.
    Future<T> _addErrorSpan<T>(AstNode nodeWithSpan, Future<T> callback()) async {
      try {
        return await callback();
      } on SassRuntimeException catch (error, stackTrace) {
        if (!error.span.text.startsWith("@error")) rethrow;
        throwWithTrace(
          SassRuntimeException(error.message, nodeWithSpan.span, _stackTrace()),
          error,
          stackTrace,
        );
      }
    }
  
    /// Returns the best human-readable message for [error].
    String _getErrorMessage(Object error) {
      // Built-in Dart Error objects often require their full toString()s for
      // full context.
      if (error is Error) return error.toString();
      try {
        return (error as dynamic).message as String;
      } catch (_) {
        return error.toString();
      }
    }
  }
  
  /// A helper class for [_EvaluateVisitor] that adds `@import`ed CSS nodes to the
  /// root stylesheet.
  ///
  /// We can't evaluate the imported stylesheet with the original stylesheet as
  /// its root because it may `@use` modules that need to be injected before the
  /// imported stylesheet's CSS.
  ///
  /// We also can't use [_EvaluateVisitor]'s implementation of [CssVisitor]
  /// because it will add the parent selector to the CSS if the `@import` appeared
  /// in a nested context, but the parent selector was already added when the
  /// imported stylesheet was evaluated.
  final class _ImportedCssVisitor implements ModifiableCssVisitor<void> {
    /// The visitor in whose context this was created.
    final _EvaluateVisitor _visitor;
  
    _ImportedCssVisitor(this._visitor);
  
    void visitCssAtRule(ModifiableCssAtRule node) {
      _visitor._addChild(
        node,
        through: node.isChildless ? null : (node) => node is CssStyleRule,
      );
    }
  
    void visitCssComment(ModifiableCssComment node) => _visitor._addChild(node);
  
    void visitCssDeclaration(ModifiableCssDeclaration node) =>
        _visitor._addChild(node);
  
    void visitCssImport(ModifiableCssImport node) {
      if (_visitor._parent != _visitor._root) {
        _visitor._addChild(node);
      } else if (_visitor._endOfImports == _visitor._root.children.length) {
        _visitor._addChild(node);
        _visitor._endOfImports++;
      } else {
        (_visitor._outOfOrderImports ??= []).add(node);
      }
    }
  
    void visitCssKeyframeBlock(ModifiableCssKeyframeBlock node) {
      assert(false, "visitCssKeyframeBlock() should never be called.");
    }
  
    void visitCssMediaRule(ModifiableCssMediaRule node) {
      // Whether [node.query] has been merged with [_visitor._mediaQueries]. If it
      // has been merged, merging again is a no-op; if it hasn't been merged,
      // merging again will fail.
      var mediaQueries = _visitor._mediaQueries;
      var hasBeenMerged = mediaQueries == null ||
          _visitor._mergeMediaQueries(mediaQueries, node.queries) != null;
  
      _visitor._addChild(
        node,
        through: (node) =>
            node is CssStyleRule || (hasBeenMerged && node is CssMediaRule),
      );
    }
  
    void visitCssStyleRule(ModifiableCssStyleRule node) =>
        _visitor._addChild(node, through: (node) => node is CssStyleRule);
  
    void visitCssStylesheet(ModifiableCssStylesheet node) {
      for (var child in node.children) {
        child.accept(this);
      }
    }
  
    void visitCssSupportsRule(ModifiableCssSupportsRule node) =>
        _visitor._addChild(node, through: (node) => node is CssStyleRule);
  }
  
  /// The result of compiling a Sass document to a CSS tree, along with metadata
  /// about the compilation process.
  typedef EvaluateResult = ({
    /// The CSS syntax tree.
    CssStylesheet stylesheet,
  
    /// The canonical URLs of all stylesheets loaded during compilation.
    Set<Uri> loadedUrls,
  });
  
  /// An implementation of [EvaluationContext] using the information available in
  /// [_EvaluateVisitor].
  final class _EvaluationContext implements EvaluationContext {
    /// The visitor backing this context.
    final _EvaluateVisitor _visitor;
  
    /// The AST node whose span should be used for [warn] if no other span is
    /// available.
    final AstNode _defaultWarnNodeWithSpan;
  
    _EvaluationContext(this._visitor, this._defaultWarnNodeWithSpan);
  
    FileSpan get currentCallableSpan {
      if (_visitor._callableNode case var callableNode?) return callableNode.span;
      throw StateError("No Sass callable is currently being evaluated.");
    }
  
    void warn(String message, [Deprecation? deprecation]) {
      _visitor._warn(
        message,
        _visitor._importSpan ??
            _visitor._callableNode?.span ??
            _defaultWarnNodeWithSpan.span,
        deprecation,
      );
    }
  }
  
  /// The result of evaluating arguments to a function or mixin.
  typedef _ArgumentResults = ({
    /// Arguments passed by position.
    List<Value> positional,
  
    /// The [AstNode]s that hold the spans for each [positional] argument.
    ///
    /// This stores [AstNode]s rather than [FileSpan]s so it can avoid calling
    /// [AstNode.span] if the span isn't required, since some nodes need to do
    /// real work to manufacture a source span.
    List<AstNode> positionalNodes,
  
    /// Arguments passed by name.
    Map<String, Value> named,
  
    /// The [AstNode]s that hold the spans for each [named] argument.
    ///
    /// This stores [AstNode]s rather than [FileSpan]s so it can avoid calling
    /// [AstNode.span] if the span isn't required, since some nodes need to do
    /// real work to manufacture a source span.
    Map<String, AstNode> namedNodes,
  
    /// The separator used for the rest argument list, if any.
    ListSeparator separator,
  });
  
  /// The result of loading a stylesheet via [AsyncEvaluator._loadStylesheet].
  typedef _LoadedStylesheet = (
    /// The stylesheet itself.
    Stylesheet stylesheet, {
    /// The importer that was used to load the stylesheet.
    ///
    /// This is `null` when running in Node Sass compatibility mode.
    AsyncImporter? importer,
  
    /// Whether this load counts as a dependency.
    ///
    /// That is, whether this was (transitively) loaded through a load path or
    /// importer rather than relative to the entrypoint.
    bool isDependency,
  });
  