Add support for loading .scl files

The Starlark Configuration Language (SCL) is a dialect of Starlark that resembles pure Starlark with just a few Bazel symbols (namely `visibility()` and `struct`), and a few restrictions on `load()` syntax and non-ASCII string data.

This CL adds support for loading .scl files anywhere a .bzl could be used. The restrictions on `load()` are implemented in this CL, but follow-up CLs will handle differentiating the top-level environment from .bzl, restricting non-ASCII data, and adding user documentation.

The dialect is gated on the flag `--experimental_enable_scl_dialect`.

- `BzlLoadValue` gains a new bool field on its abstract Key type to indicate whether it is for .scl. This was chosen instead of creating a new Key subclass so that .scl can work equally well in different contexts (loaded by BUILD, WORKSPACE, MODULE.bazel, or even @_builtins).

- `BzlLoadFunction.checkValidLoadLabel` and `.getLoadLabels` are both split into a public and private method. The public methods assume the load is coming from a .bzl file for validation purposes, and take a `StarlarkSemantics` to check whether the experimental flag is enabled.

- Eliminate `BzlLoadFunction.getBUILDLabel()` helper, which is obsolete.

- Modify existing tests to incidentally check that bzlmod .bzls can load .scl files and that .scl files can appear in transitive loads as reported by the Package (for `loadfiles()`).

RELNOTES: None
PiperOrigin-RevId: 528563228
Change-Id: I8493d1f33d35e1af8003dc61e5fdb626676d7e53
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionEvalFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionEvalFunction.java
index 74f9f63..222eb67 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionEvalFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/SingleExtensionEvalFunction.java
@@ -111,8 +111,7 @@
 
     // Check that the .bzl label isn't crazy.
     try {
-      BzlLoadFunction.checkValidLoadLabel(
-          extensionId.getBzlFileLabel(), /*fromBuiltinsRepo=*/ false);
+      BzlLoadFunction.checkValidLoadLabel(extensionId.getBzlFileLabel(), starlarkSemantics);
     } catch (LabelSyntaxException e) {
       throw new SingleExtensionEvalFunctionException(
           ExternalDepsException.withCauseAndMessage(
diff --git a/src/main/java/com/google/devtools/build/lib/packages/semantics/BuildLanguageOptions.java b/src/main/java/com/google/devtools/build/lib/packages/semantics/BuildLanguageOptions.java
index 4cf7d12..2641aa3 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/semantics/BuildLanguageOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/semantics/BuildLanguageOptions.java
@@ -197,6 +197,15 @@
   public boolean experimentalEnableAndroidMigrationApis;
 
   @Option(
+      name = "experimental_enable_scl_dialect",
+      defaultValue = "false",
+      documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS,
+      effectTags = OptionEffectTag.BUILD_FILE_SEMANTICS,
+      // TODO(brandjon): point to more extensive user documentation somewhere
+      help = "If set to true, .scl files may be used in load() statements.")
+  public boolean experimentalEnableSclDialect;
+
+  @Option(
       name = "enable_bzlmod",
       oldName = "experimental_enable_bzlmod",
       defaultValue = "false",
@@ -677,6 +686,7 @@
             .setBool(CHECK_BZL_VISIBILITY, checkBzlVisibility)
             .setBool(
                 EXPERIMENTAL_ENABLE_ANDROID_MIGRATION_APIS, experimentalEnableAndroidMigrationApis)
+            .setBool(EXPERIMENTAL_ENABLE_SCL_DIALECT, experimentalEnableSclDialect)
             .setBool(ENABLE_BZLMOD, enableBzlmod)
             .setBool(
                 EXPERIMENTAL_JAVA_PROTO_LIBRARY_DEFAULT_HAS_SERVICES,
@@ -770,6 +780,7 @@
       "-experimental_disable_external_package";
   public static final String EXPERIMENTAL_ENABLE_ANDROID_MIGRATION_APIS =
       "-experimental_enable_android_migration_apis";
+  public static final String EXPERIMENTAL_ENABLE_SCL_DIALECT = "-experimental_enable_scl_dialect";
   public static final String ENABLE_BZLMOD = "-enable_bzlmod";
   public static final String EXPERIMENTAL_JAVA_PROTO_LIBRARY_DEFAULT_HAS_SERVICES =
       "+experimental_java_proto_library_default_has_services";
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BzlLoadFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/BzlLoadFunction.java
index a9e7b0e..f6012b0 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BzlLoadFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BzlLoadFunction.java
@@ -86,7 +86,11 @@
 import net.starlark.java.syntax.StringLiteral;
 
 /**
- * A Skyframe function to look up and load a single .bzl module.
+ * A Skyframe function to look up and load a single .bzl (or .scl) module.
+ *
+ * <p>Note: Historically, all modules had the .bzl suffix, but this is no longer true now that Bazel
+ * supports the .scl dialect. In identifiers, code comments, and documentation, you should generally
+ * assume any "bzl" term could mean a .scl file as well.
  *
  * <p>Given a {@link Label} referencing a .bzl file, attempts to locate the file and load it. The
  * Label must be absolute, and must not reference the special {@code external} package. If loading
@@ -736,6 +740,14 @@
     Label label = key.getLabel();
     PackageIdentifier pkg = label.getPackageIdentifier();
 
+    boolean isSclFlagEnabled =
+        builtins.starlarkSemantics.getBool(BuildLanguageOptions.EXPERIMENTAL_ENABLE_SCL_DIALECT);
+    if (key.isSclDialect() && !isSclFlagEnabled) {
+      throw new BzlLoadFailedException(
+          "loading .scl files requires setting --experimental_enable_scl_dialect",
+          Code.PARSE_ERROR);
+    }
+
     // Determine dependency BzlLoadValue keys for the load statements in this bzl.
     // Labels are resolved relative to the current repo mapping.
     RepositoryMapping repoMapping = getRepositoryMapping(key, builtins.starlarkSemantics, env);
@@ -744,7 +756,13 @@
     }
     ImmutableList<Pair<String, Location>> programLoads = getLoadsFromProgram(prog);
     ImmutableList<Label> loadLabels =
-        getLoadLabels(env.getListener(), programLoads, pkg, repoMapping);
+        getLoadLabels(
+            env.getListener(),
+            programLoads,
+            pkg,
+            repoMapping,
+            key.isSclDialect(),
+            isSclFlagEnabled);
     if (loadLabels == null) {
       throw new BzlLoadFailedException(
           String.format(
@@ -933,11 +951,59 @@
     return repositoryMappingValue.getRepositoryMapping();
   }
 
-  public static void checkValidLoadLabel(Label label, boolean fromBuiltinsRepo)
+  /**
+   * Validates a label appearing in a {@code load()} statement, throwing {@link
+   * LabelSyntaxException} on failure.
+   *
+   * <p>Different restrictions apply depending on what type of source file the load appears in. For
+   * all kinds of files, {@code label}:
+   *
+   * <ul>
+   *   <li>may not be within {@code @//external}.
+   *   <li>must end with either {@code .bzl} or {@code .scl}.
+   * </ul>
+   *
+   * <p>For source files appearing within {@code @_builtins}, {@code label} must also be within
+   * {@code @_builtins}. (The reverse, that those files may not be loaded by user-defined files, is
+   * enforced by the fact that the {@code @_builtins} pseudorepo cannot be resolved as an ordinary
+   * repo.)
+   *
+   * <p>For .scl files only, {@code label} must end with {@code .scl} (not {@code .bzl}). (Loads in
+   * .scl also should always begin with {@code //}, but that's syntactic and can't be enforced in
+   * this method.)
+   *
+   * @param label the label to validate
+   * @param fromBuiltinsRepo true if the file containing the load is within {@code @_builtins}
+   * @param withinSclDialect true if the file containing the load is a .scl file
+   * @param mentionSclInErrorMessage true if ".scl" should be advertised as a possible extension in
+   *     error messaging
+   */
+  private static void checkValidLoadLabel(
+      Label label,
+      boolean fromBuiltinsRepo,
+      boolean withinSclDialect,
+      boolean mentionSclInErrorMessage)
       throws LabelSyntaxException {
-    if (!label.getName().endsWith(".bzl")) {
-      throw new LabelSyntaxException("The label must reference a file with extension '.bzl'");
+    // Check file extension.
+    String baseName = label.getName();
+    if (withinSclDialect) {
+      if (!baseName.endsWith(".scl")) {
+        String msg = "The label must reference a file with extension \".scl\"";
+        if (baseName.endsWith(".bzl")) {
+          msg += " (.scl files cannot load .bzl files)";
+        }
+        throw new LabelSyntaxException(msg);
+      }
+    } else {
+      if (!(baseName.endsWith(".scl") || baseName.endsWith(".bzl"))) {
+        String msg = "The label must reference a file with extension \".bzl\"";
+        if (mentionSclInErrorMessage) {
+          msg += " or \".scl\"";
+        }
+        throw new LabelSyntaxException(msg);
+      }
     }
+
     if (label.getPackageIdentifier().equals(LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER)) {
       throw new LabelSyntaxException(
           "Starlark files may not be loaded from the //external package");
@@ -949,6 +1015,66 @@
   }
 
   /**
+   * Validates a label appearing in a {@code load()} statement, throwing {@link
+   * LabelSyntaxException} on failure.
+   */
+  public static void checkValidLoadLabel(Label label, StarlarkSemantics starlarkSemantics)
+      throws LabelSyntaxException {
+    checkValidLoadLabel(
+        label,
+        /* fromBuiltinsRepo= */ false,
+        /* withinSclDialect= */ false,
+        /* mentionSclInErrorMessage= */ starlarkSemantics.getBool(
+            BuildLanguageOptions.EXPERIMENTAL_ENABLE_SCL_DIALECT));
+  }
+
+  /**
+   * Given a list of {@code load("module")} strings and their locations, in source order, returns a
+   * corresponding list of Labels they each resolve to. Labels are resolved relative to {@code
+   * base}, the file's package. If any label is malformed, the function reports one or more errors
+   * to the handler and returns null.
+   *
+   * <p>If {@code withinSclDialect} is true, the labels are validated according to the rules of the
+   * .scl dialect: Only strings beginning with {@code //} are allowed (no repo syntax, no relative
+   * labels), and only .scl files may be loaded (not .bzl). If {@code isSclFlagEnabled} is true,
+   * then ".scl" is mentioned as a possible file extension in error messages.
+   */
+  @Nullable
+  private static ImmutableList<Label> getLoadLabels(
+      EventHandler handler,
+      ImmutableList<Pair<String, Location>> loads,
+      PackageIdentifier base,
+      RepositoryMapping repoMapping,
+      boolean withinSclDialect,
+      boolean isSclFlagEnabled) {
+    boolean ok = true;
+
+    ImmutableList.Builder<Label> loadLabels = ImmutableList.builderWithExpectedSize(loads.size());
+    for (Pair<String, Location> load : loads) {
+      // Parse the load statement's module string as a label. Validate the unparsed string for
+      // syntax and the parsed label for structure.
+      String unparsedLabel = load.first;
+      try {
+        if (withinSclDialect && !unparsedLabel.startsWith("//")) {
+          throw new LabelSyntaxException("in .scl files, load labels must begin with \"//\"");
+        }
+        Label label =
+            Label.parseWithPackageContext(unparsedLabel, PackageContext.of(base, repoMapping));
+        checkValidLoadLabel(
+            label,
+            /* fromBuiltinsRepo= */ StarlarkBuiltinsValue.isBuiltinsRepo(base.getRepository()),
+            /* withinSclDialect= */ withinSclDialect,
+            /* mentionSclInErrorMessage= */ isSclFlagEnabled);
+        loadLabels.add(label);
+      } catch (LabelSyntaxException ex) {
+        handler.handle(Event.error(load.second, "in load statement: " + ex.getMessage()));
+        ok = false;
+      }
+    }
+    return ok ? loadLabels.build() : null;
+  }
+
+  /**
    * Given a list of {@code load("module")} strings and their locations, in source order, returns a
    * corresponding list of Labels they each resolve to. Labels are resolved relative to {@code
    * base}, the file's package. If any label is malformed, the function reports one or more errors
@@ -959,31 +1085,16 @@
       EventHandler handler,
       ImmutableList<Pair<String, Location>> loads,
       PackageIdentifier base,
-      RepositoryMapping repoMapping) {
-    // It's redundant that getRelativeWithRemapping needs a Label;
-    // a PackageIdentifier should suffice. Make one here.
-    Label buildLabel = getBUILDLabel(base);
-
-    boolean ok = true;
-
-    ImmutableList.Builder<Label> loadLabels = ImmutableList.builderWithExpectedSize(loads.size());
-    for (Pair<String, Location> load : loads) {
-      // Parse the load statement's module string as a label.
-      // It must end in .bzl and not be in package "//external".
-      try {
-        Label label =
-            Label.parseWithPackageContext(
-                load.first, PackageContext.of(buildLabel.getPackageIdentifier(), repoMapping));
-        checkValidLoadLabel(
-            label,
-            /* fromBuiltinsRepo= */ StarlarkBuiltinsValue.isBuiltinsRepo(base.getRepository()));
-        loadLabels.add(label);
-      } catch (LabelSyntaxException ex) {
-        handler.handle(Event.error(load.second, "in load statement: " + ex.getMessage()));
-        ok = false;
-      }
-    }
-    return ok ? loadLabels.build() : null;
+      RepositoryMapping repoMapping,
+      StarlarkSemantics starlarkSemantics) {
+    return getLoadLabels(
+        handler,
+        loads,
+        base,
+        repoMapping,
+        /* withinSclDialect= */ false,
+        /* isSclFlagEnabled= */ starlarkSemantics.getBool(
+            BuildLanguageOptions.EXPERIMENTAL_ENABLE_SCL_DIALECT));
   }
 
   /** Extracts load statements from compiled program (see {@link #getLoadLabels}). */
@@ -1010,15 +1121,6 @@
     return loads.build();
   }
 
-  private static Label getBUILDLabel(PackageIdentifier pkgid) {
-    try {
-      return Label.create(pkgid, "BUILD");
-    } catch (LabelSyntaxException e) {
-      // Shouldn't happen; the Label is well-formed by construction.
-      throw new IllegalStateException(e);
-    }
-  }
-
   /**
    * Computes the BzlLoadValue for all given .bzl load keys using ordinary Skyframe evaluation,
    * returning {@code null} if Skyframe deps were missing and have been requested. {@code
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BzlLoadValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/BzlLoadValue.java
index d78c26a..ab14bb5 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BzlLoadValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BzlLoadValue.java
@@ -31,7 +31,11 @@
 import net.starlark.java.eval.Module;
 
 /**
- * A value that represents the .bzl module loaded by a Starlark {@code load()} statement.
+ * A value that represents the .bzl (or .scl) module loaded by a Starlark {@code load()} statement.
+ *
+ * <p>Note: Historically, all modules had the .bzl suffix, but this is no longer true now that Bazel
+ * supports the .scl dialect. In identifiers, code comments, and documentation, you should generally
+ * assume any "bzl" term could mean a .scl file as well.
  *
  * <p>The key consists of an absolute {@link Label} and the context in which the load occurs. The
  * Label should not reference the special {@code external} package.
@@ -95,6 +99,22 @@
       return false;
     }
 
+    /** Returns true if the requested file follows the .scl dialect. */
+    // Note: Just as with .bzl, the same .scl file can be referred to from multiple key types, for
+    // instance if a BUILD file and a module rule both load foo.scl. Conceptually, .scl files
+    // shouldn't depend on what kind of top-level file caused them to load, but in practice, this
+    // implementation quirk means that the .scl file will be loaded twice as separate copies.
+    //
+    // This shouldn't matter except in rare edge cases, such as if a Starlark function is loaded
+    // from both copies and compared for equality. Performance wise, it also means that all
+    // transitive .scl files will be double-loaded, but we don't expect that to be significant.
+    //
+    // The alternative is to use a separate key type just for .scl, but that complicates repo logic;
+    // see BzlLoadFunction#getRepositoryMapping.
+    final boolean isSclDialect() {
+      return getLabel().getName().endsWith(".scl");
+    }
+
     /**
      * Constructs a new key suitable for evaluating a {@code load()} dependency of this key's .bzl
      * file.
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BzlmodRepoRuleFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/BzlmodRepoRuleFunction.java
index a7c5c48..2e18581 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BzlmodRepoRuleFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BzlmodRepoRuleFunction.java
@@ -220,7 +220,8 @@
             env.getListener(),
             programLoads,
             PackageIdentifier.EMPTY_PACKAGE_ID,
-            EMPTY_MAIN_REPO_MAPPING);
+            EMPTY_MAIN_REPO_MAPPING,
+            starlarkSemantics);
     if (loadLabels == null) {
       NoSuchPackageException e =
           PackageFunction.PackageFunctionException.builder()
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
index ebeeccd..c0ae3f7 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
@@ -1288,7 +1288,11 @@
             BzlLoadFunction.getLoadsFromProgram(compiled.prog);
         ImmutableList<Label> loadLabels =
             BzlLoadFunction.getLoadLabels(
-                env.getListener(), programLoads, packageId, repositoryMapping);
+                env.getListener(),
+                programLoads,
+                packageId,
+                repositoryMapping,
+                starlarkBuiltinsValue.starlarkSemantics);
         if (loadLabels == null) {
           throw PackageFunctionException.builder()
               .setType(PackageFunctionException.Type.BUILD_FILE_CONTAINS_ERRORS)
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java
index 768586c..d9539f9 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java
@@ -297,7 +297,8 @@
     ImmutableList<Pair<String, Location>> programLoads =
         BzlLoadFunction.getLoadsFromStarlarkFiles(chunk);
     ImmutableList<Label> loadLabels =
-        BzlLoadFunction.getLoadLabels(env.getListener(), programLoads, rootPackage, repoMapping);
+        BzlLoadFunction.getLoadLabels(
+            env.getListener(), programLoads, rootPackage, repoMapping, starlarkSemantics);
     if (loadLabels == null) {
       NoSuchPackageException e =
           PackageFunction.PackageFunctionException.builder()
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BzlLoadFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/BzlLoadFunctionTest.java
index a673ee2..cee118c 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/BzlLoadFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/BzlLoadFunctionTest.java
@@ -210,6 +210,101 @@
     checkSuccessfulLookup("//pkg:subdir/ext2.bzl");
   }
 
+  @Test
+  public void testLoadBadExtension_sclDisabled() throws Exception {
+    setBuildLanguageOptions("--experimental_enable_scl_dialect=false");
+
+    scratch.file("pkg/BUILD");
+    scratch.file("pkg/ext.bzl", "load(':foo.garbage', 'a')");
+    reporter.removeHandler(failFastHandler);
+    checkFailingLookup("//pkg:ext.bzl", "has invalid load statements");
+    assertContainsEvent("The label must reference a file with extension \".bzl\"");
+    assertDoesNotContainEvent(".scl");
+  }
+
+  @Test
+  public void testLoadBadExtension_sclEnabled() throws Exception {
+    setBuildLanguageOptions("--experimental_enable_scl_dialect=true");
+
+    scratch.file("pkg/BUILD");
+    scratch.file("pkg/ext.bzl", "load(':foo.garbage', 'a')");
+    reporter.removeHandler(failFastHandler);
+    checkFailingLookup("//pkg:ext.bzl", "has invalid load statements");
+    assertContainsEvent("The label must reference a file with extension \".bzl\" or \".scl\"");
+  }
+
+  @Test
+  public void testLoadingSclRequiresExperimentalFlag() throws Exception {
+    setBuildLanguageOptions("--experimental_enable_scl_dialect=false");
+
+    scratch.file("pkg/BUILD");
+    scratch.file("pkg/ext.scl");
+    reporter.removeHandler(failFastHandler);
+    checkFailingLookup(
+        "//pkg:ext.scl", "loading .scl files requires setting --experimental_enable_scl_dialect");
+  }
+
+  @Test
+  public void testCanLoadScl() throws Exception {
+    setBuildLanguageOptions("--experimental_enable_scl_dialect=true");
+
+    scratch.file("pkg/BUILD");
+    scratch.file("pkg/ext.scl");
+    checkSuccessfulLookup("//pkg:ext.scl");
+  }
+
+  @Test
+  public void testCanLoadSclFromBzlAndScl() throws Exception {
+    setBuildLanguageOptions("--experimental_enable_scl_dialect=true");
+
+    scratch.file("pkg/BUILD");
+    scratch.file("pkg/ext1.scl", "a = 1");
+    // Can use relative load label syntax from ext2a.bzl, but not from ext2b.scl.
+    scratch.file("pkg/ext2a.bzl", "load(':ext1.scl', 'a')");
+    scratch.file("pkg/ext2b.scl", "load('//pkg:ext1.scl', 'a')");
+
+    checkSuccessfulLookup("//pkg:ext2a.bzl");
+    checkSuccessfulLookup("//pkg:ext2b.scl");
+  }
+
+  @Test
+  public void testSclCannotLoadNonSclFiles() throws Exception {
+    setBuildLanguageOptions("--experimental_enable_scl_dialect=true");
+
+    scratch.file("pkg/BUILD");
+    scratch.file("pkg/ext1a.bzl", "a = 1");
+    scratch.file("pkg/ext1a.garbage", "a = 1");
+    // Cannot use relative label.
+    scratch.file("pkg/ext2a.scl", "load('//pkg:ext1a.bzl', 'a')");
+    scratch.file("pkg/ext2b.scl", "load('//pkg:ext1b.garbage', 'a')");
+
+    reporter.removeHandler(failFastHandler);
+    checkFailingLookup("//pkg:ext2a.scl", "has invalid load statements");
+    assertContainsEvent(
+        "The label must reference a file with extension \".scl\" (.scl files cannot load .bzl"
+            + " files)");
+    eventCollector.clear();
+    checkFailingLookup("//pkg:ext2b.scl", "has invalid load statements");
+    assertContainsEvent("The label must reference a file with extension \".scl\"");
+    assertDoesNotContainEvent(".bzl");
+  }
+
+  @Test
+  public void testSclCanOnlyLoadLabelsRelativeToDefaultRepoRoot() throws Exception {
+    setBuildLanguageOptions("--experimental_enable_scl_dialect=true");
+
+    scratch.file("pkg/BUILD");
+    scratch.file("pkg/ext1.scl", "load(':foo.scl', 'a')");
+    scratch.file("pkg/ext2.scl", "load('@repo//:foo.scl', 'a')");
+
+    reporter.removeHandler(failFastHandler);
+    checkFailingLookup("//pkg:ext1.scl", "has invalid load statements");
+    assertContainsEvent("in .scl files, load labels must begin with \"//\"");
+    eventCollector.clear();
+    checkFailingLookup("//pkg:ext2.scl", "has invalid load statements");
+    assertContainsEvent("in .scl files, load labels must begin with \"//\"");
+  }
+
   private EvaluationResult<BzlLoadValue> get(SkyKey skyKey) throws Exception {
     EvaluationResult<BzlLoadValue> result =
         SkyframeExecutorTestUtils.evaluate(
@@ -944,7 +1039,7 @@
 
   @Test
   public void testLoadBzlFileFromBzlmod() throws Exception {
-    setBuildLanguageOptions("--enable_bzlmod");
+    setBuildLanguageOptions("--enable_bzlmod", "--experimental_enable_scl_dialect");
     scratch.overwriteFile("MODULE.bazel", "bazel_dep(name='foo',version='1.0')");
     registry
         .addModule(
@@ -957,12 +1052,13 @@
     scratch.file(fooDir.getRelative("BUILD").getPathString());
     scratch.file(
         fooDir.getRelative("test.bzl").getPathString(),
-        "load('@bar_alias//:test.bzl', 'haha')",
+        // Also test that bzlmod .bzl files can load .scl files.
+        "load('@bar_alias//:test.scl', 'haha')",
         "hoho = haha");
     Path barDir = moduleRoot.getRelative("bar~2.0");
     scratch.file(barDir.getRelative("WORKSPACE").getPathString());
     scratch.file(barDir.getRelative("BUILD").getPathString());
-    scratch.file(barDir.getRelative("test.bzl").getPathString(), "haha = 5");
+    scratch.file(barDir.getRelative("test.scl").getPathString(), "haha = 5");
 
     SkyKey skyKey = BzlLoadValue.keyForBzlmod(Label.parseCanonical("@@foo~1.0//:test.bzl"));
     EvaluationResult<BzlLoadValue> result =
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java
index 220f5e2..79c290f 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/PackageFunctionTest.java
@@ -610,19 +610,21 @@
   public void testTransitiveStarlarkDepsStoredInPackage() throws Exception {
     scratch.file("foo/BUILD", "load('//bar:ext.bzl', 'a')");
     scratch.file("bar/BUILD");
-    scratch.file("bar/ext.bzl", "load('//baz:ext.bzl', 'b')", "a = b");
+    scratch.file("bar/ext.bzl", "load('//baz:ext.scl', 'b')", "a = b");
     scratch.file("baz/BUILD");
-    scratch.file("baz/ext.bzl", "b = 1");
+    scratch.file("baz/ext.scl", "b = 1");
     scratch.file("qux/BUILD");
     scratch.file("qux/ext.bzl", "c = 1");
 
     preparePackageLoading(rootDirectory);
+    // must be done after preparePackageLoading()
+    setBuildLanguageOptions("--experimental_enable_scl_dialect=true");
 
     SkyKey skyKey = PackageIdentifier.createInMainRepo("foo");
     Package pkg = validPackageWithoutErrors(skyKey);
     assertThat(pkg.getStarlarkFileDependencies())
         .containsExactly(
-            Label.parseCanonical("//bar:ext.bzl"), Label.parseCanonical("//baz:ext.bzl"));
+            Label.parseCanonical("//bar:ext.bzl"), Label.parseCanonical("//baz:ext.scl"));
 
     scratch.overwriteFile("bar/ext.bzl", "load('//qux:ext.bzl', 'c')", "a = c");
     getSkyframeExecutor()
@@ -766,7 +768,7 @@
     reporter.removeHandler(failFastHandler);
     SkyKey key = PackageIdentifier.createInMainRepo("p");
     SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, /*keepGoing=*/ false, reporter);
-    assertContainsEvent("The label must reference a file with extension '.bzl'");
+    assertContainsEvent("The label must reference a file with extension \".bzl\"");
   }
 
   @Test