Refactor `FlagSetFunction` to depend on a `ProjectValue` directly, instead of depending on its own `BzlLoadValue`.

The dependency chain is now `FlagSetFunction` -> `ProjectFunction` -> `BzlLoadFunction`.

`FlagSetFunction` is kept distinct from `ProjectFunction` so as to not burden other reverse dependencies of `ProjectFunction` from the extra Skyframe lookup costs from `FlagSetFunction` (i.e. `ParsedFlagsFunction`). `ProjectFunction` is implemented as an indirection to `BzlLoadValue`, so there's a place for PROJECT.scl specific validations and getters in the future.

PiperOrigin-RevId: 641126914
Change-Id: I4e799395817c8d0f96a0af3e6c6520b29fafdf08
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index 978c8f4..9929f02 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -432,7 +432,7 @@
         "//src/main/java/com/google/devtools/build/lib/skyframe:package_roots_no_symlink_creation",
         "//src/main/java/com/google/devtools/build/lib/skyframe:package_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:precomputed_value",
-        "//src/main/java/com/google/devtools/build/lib/skyframe:project_owned_code_paths_value",
+        "//src/main/java/com/google/devtools/build/lib/skyframe:project_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:repository_mapping_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:sky_functions",
         "//src/main/java/com/google/devtools/build/lib/skyframe:skyfocus",
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
index f993501..c4915a6 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java
@@ -66,7 +66,7 @@
 import com.google.devtools.build.lib.server.FailureDetails.BuildConfiguration.Code;
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.skyframe.BuildResultListener;
-import com.google.devtools.build.lib.skyframe.ProjectOwnedCodePathsValue;
+import com.google.devtools.build.lib.skyframe.ProjectValue;
 import com.google.devtools.build.lib.skyframe.RepositoryMappingValue.RepositoryMappingResolutionException;
 import com.google.devtools.build.lib.skyframe.SequencedSkyframeExecutor;
 import com.google.devtools.build.lib.skyframe.SkyframeBuildView.BuildDriverKeyTestContext;
@@ -816,7 +816,7 @@
   private static ImmutableSet<PathFragment> getProjectDirectories(
       Label projectFile, SkyframeExecutor skyframeExecutor, ExtendedEventHandler eventHandler)
       throws InvalidConfigurationException {
-    ProjectOwnedCodePathsValue.Key key = new ProjectOwnedCodePathsValue.Key(projectFile);
+    ProjectValue.Key key = new ProjectValue.Key(projectFile);
     EvaluationResult<SkyValue> result =
         skyframeExecutor.evaluateSkyKeys(
             eventHandler, ImmutableList.of(key), /* keepGoing= */ false);
@@ -829,7 +829,7 @@
           Code.INVALID_PROJECT);
     }
 
-    return ((ProjectOwnedCodePathsValue) result.get(key))
+    return ((ProjectValue) result.get(key))
         .getOwnedCodePaths().stream().map(PathFragment::create).collect(toImmutableSet());
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
index 4095a7a..5721ad6 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -167,7 +167,7 @@
         ":prerequisite_package_function",
         ":progress_event_suppressing_environment",
         ":project_files_lookup_function",
-        ":project_owned_code_paths_function",
+        ":project_function",
         ":recursive_filesystem_traversal",
         ":recursive_package_provider_backed_target_pattern_resolver",
         ":recursive_pkg_function",
@@ -1175,10 +1175,11 @@
 )
 
 java_library(
-    name = "project_owned_code_paths_function",
-    srcs = ["ProjectOwnedCodePathsFunction.java"],
+    name = "project_function",
+    srcs = ["ProjectFunction.java"],
     deps = [
-        ":project_owned_code_paths_value",
+        ":project_value",
+        "//src/main/java/com/google/devtools/build/lib/skyframe:bzl_load_failed_exception",
         "//src/main/java/com/google/devtools/build/lib/skyframe:bzl_load_value",
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
         "//third_party:guava",
@@ -1187,13 +1188,14 @@
 )
 
 java_library(
-    name = "project_owned_code_paths_value",
-    srcs = ["ProjectOwnedCodePathsValue.java"],
+    name = "project_value",
+    srcs = ["ProjectValue.java"],
     deps = [
         ":sky_functions",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
         "//third_party:guava",
+        "//third_party:jsr305",
     ],
 )
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ProjectFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ProjectFunction.java
new file mode 100644
index 0000000..8f63ab8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ProjectFunction.java
@@ -0,0 +1,103 @@
+// Copyright 2024 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.devtools.build.skyframe.SkyFunctionException.Transience.PERSISTENT;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import java.util.Collection;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/** A {@link SkyFunction} that loads metadata from a PROJECT.scl file. */
+public class ProjectFunction implements SkyFunction {
+
+  private static final String OWNED_CODE_PATHS_KEY = "owned_code_paths";
+
+  // The set of top level reserved globals in the PROJECT.scl file.
+  private static final ImmutableSet<String> RESERVED_GLOBALS =
+      ImmutableSet.of(OWNED_CODE_PATHS_KEY);
+
+  @Nullable
+  @Override
+  public SkyValue compute(SkyKey skyKey, Environment env)
+      throws ProjectFunctionException, InterruptedException {
+    ProjectValue.Key key = (ProjectValue.Key) skyKey.argument();
+
+    BzlLoadValue bzlLoadValue;
+    try {
+      bzlLoadValue =
+          (BzlLoadValue)
+              env.getValueOrThrow(
+                  BzlLoadValue.keyForBuild(key.getProjectFile()), BzlLoadFailedException.class);
+    } catch (BzlLoadFailedException e) {
+      throw new ProjectFunctionException(e, PERSISTENT);
+    }
+    if (bzlLoadValue == null) {
+      return null;
+    }
+
+    Object ownedCodePathsRaw = bzlLoadValue.getModule().getGlobal(OWNED_CODE_PATHS_KEY);
+
+    // Crude typechecking to prevent server crashes.
+    @SuppressWarnings("unchecked")
+    Collection<? extends String> ownedCodePaths =
+        switch (ownedCodePathsRaw) {
+          case null -> ImmutableSet.of();
+          case Collection<?> xs -> {
+            for (Object x : xs) {
+              if (!(x instanceof String)) {
+                throw new ProjectFunctionException(
+                    new TypecheckFailureException(
+                        "expected a list of strings, got element of " + x.getClass()));
+              }
+            }
+            yield (Collection<String>) xs;
+          }
+          default ->
+              throw new ProjectFunctionException(
+                  new TypecheckFailureException(
+                      "expected a list of strings, got " + ownedCodePathsRaw.getClass()));
+        };
+
+    ImmutableMap<String, Object> residualGlobals =
+        bzlLoadValue.getModule().getGlobals().entrySet().stream()
+            .filter(entry -> !RESERVED_GLOBALS.contains(entry.getKey()))
+            .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
+
+    return new ProjectValue(ImmutableSet.copyOf(ownedCodePaths), residualGlobals);
+  }
+
+  private static final class TypecheckFailureException extends Exception {
+    TypecheckFailureException(String msg) {
+      super(msg);
+    }
+  }
+
+  private static final class ProjectFunctionException extends SkyFunctionException {
+    ProjectFunctionException(TypecheckFailureException cause) {
+      super(cause, PERSISTENT);
+    }
+
+    ProjectFunctionException(BzlLoadFailedException e, Transience transience) {
+      super(e, transience);
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ProjectOwnedCodePathsFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ProjectOwnedCodePathsFunction.java
deleted file mode 100644
index 33ed08c..0000000
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ProjectOwnedCodePathsFunction.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2024 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.devtools.build.lib.skyframe;
-
-import com.google.common.collect.ImmutableSet;
-import com.google.devtools.build.skyframe.SkyFunction;
-import com.google.devtools.build.skyframe.SkyFunctionException;
-import com.google.devtools.build.skyframe.SkyKey;
-import com.google.devtools.build.skyframe.SkyValue;
-import java.util.Collection;
-import javax.annotation.Nullable;
-
-/** A {@link SkyFunction} that loads the owned code paths from a project file. */
-public class ProjectOwnedCodePathsFunction implements SkyFunction {
-
-  private static final String TOP_LEVEL_VARIABLE_NAME = "owned_code_paths";
-
-  @Nullable
-  @Override
-  public SkyValue compute(SkyKey skyKey, Environment env)
-      throws SkyFunctionException, InterruptedException {
-    ProjectOwnedCodePathsValue.Key key = (ProjectOwnedCodePathsValue.Key) skyKey.argument();
-
-    BzlLoadValue bzlLoadValue =
-        (BzlLoadValue) env.getValue(BzlLoadValue.keyForBuild(key.getProjectFile()));
-    if (bzlLoadValue == null) {
-      return null;
-    }
-
-    Object ret = bzlLoadValue.getModule().getGlobal(TOP_LEVEL_VARIABLE_NAME);
-    if (ret == null) {
-      return new ProjectOwnedCodePathsValue(ImmutableSet.of());
-    } else {
-      @SuppressWarnings("unchecked")
-      Collection<? extends String> dirs = (Collection<? extends String>) ret;
-      return new ProjectOwnedCodePathsValue(ImmutableSet.copyOf(dirs));
-    }
-  }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ProjectOwnedCodePathsValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ProjectOwnedCodePathsValue.java
deleted file mode 100644
index 041d50b..0000000
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ProjectOwnedCodePathsValue.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright 2024 The Bazel Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.devtools.build.lib.skyframe;
-
-import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableSet;
-import com.google.devtools.build.lib.cmdline.Label;
-import com.google.devtools.build.skyframe.SkyFunctionName;
-import com.google.devtools.build.skyframe.SkyKey;
-import com.google.devtools.build.skyframe.SkyValue;
-
-/** A SkyValue representing the code paths that are owned by a project. */
-public final class ProjectOwnedCodePathsValue implements SkyValue {
-
-  private final ImmutableSet<String> ownedCodePaths;
-
-  public ProjectOwnedCodePathsValue(ImmutableSet<String> ownedCodePaths) {
-    this.ownedCodePaths = ownedCodePaths;
-  }
-
-  public ImmutableSet<String> getOwnedCodePaths() {
-    return ownedCodePaths;
-  }
-
-  /** The SkyKey. Uses the label of the project file as the input. */
-  public static final class Key implements SkyKey {
-    private final Label projectFile;
-
-    public Key(Label projectFile) {
-      this.projectFile = Preconditions.checkNotNull(projectFile);
-    }
-
-    public Label getProjectFile() {
-      return projectFile;
-    }
-
-    @Override
-    public SkyFunctionName functionName() {
-      return SkyFunctions.PROJECT_DIRECTORIES;
-    }
-  }
-}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ProjectValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ProjectValue.java
new file mode 100644
index 0000000..462d482
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ProjectValue.java
@@ -0,0 +1,92 @@
+// Copyright 2024 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** A SkyValue representing the parsed definitions from a PROJECT.scl file. */
+public final class ProjectValue implements SkyValue {
+
+  private final ImmutableSet<String> ownedCodePaths;
+
+  private final ImmutableMap<String, Object> residualGlobals;
+
+  public ProjectValue(
+      ImmutableSet<String> ownedCodePaths, ImmutableMap<String, Object> residualGlobals) {
+    this.ownedCodePaths = ownedCodePaths;
+    this.residualGlobals = residualGlobals;
+  }
+
+  /**
+   * Returns the residual global referenced by the {@code key} found in the PROJECT file.
+   *
+   * <p>This returns null for non-existent keys and reserved globals. Use the dedicated getters to
+   * access the reserved globals. See {@code ProjectFunction.RESERVED_GLOBALS} for the list.
+   */
+  @Nullable
+  public Object getResidualGlobal(String key) {
+    return residualGlobals.get(key);
+  }
+
+  /**
+   * Returns the list of code paths defined by the {@code ProjectFunction.OWNED_CODE_PATHS_KEY}. If
+   * the list is not defined in the file, returns an empty set.
+   */
+  public ImmutableSet<String> getOwnedCodePaths() {
+    return ownedCodePaths;
+  }
+
+  /** The SkyKey. Uses the label of the project file as the input. */
+  public static final class Key implements SkyKey {
+    private final Label projectFile;
+
+    public Key(Label projectFile) {
+      this.projectFile = Preconditions.checkNotNull(projectFile);
+    }
+
+    public Label getProjectFile() {
+      return projectFile;
+    }
+
+    @Override
+    public SkyFunctionName functionName() {
+      return SkyFunctions.PROJECT;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      ProjectValue.Key key = (ProjectValue.Key) o;
+      return Objects.equals(projectFile, key.projectFile);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(projectFile);
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
index 3b541aa..aa593e8 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
@@ -38,8 +38,7 @@
       SkyFunctionName.createHermetic("PACKAGE_LOOKUP");
   public static final SkyFunctionName CONTAINING_PACKAGE_LOOKUP =
       SkyFunctionName.createHermetic("CONTAINING_PACKAGE_LOOKUP");
-  public static final SkyFunctionName PROJECT_DIRECTORIES =
-      SkyFunctionName.createHermetic("PROJECT_DIRECTORIES");
+  public static final SkyFunctionName PROJECT = SkyFunctionName.createHermetic("PROJECT");
   public static final SkyFunctionName PROJECT_FILES_LOOKUP =
       SkyFunctionName.createHermetic("PROJECT_FILES_LOOKUP");
   public static final SkyFunctionName BZL_COMPILE = SkyFunctionName.createHermetic("BZL_COMPILE");
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
index 38892da..b3b45ff 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -635,7 +635,7 @@
             buildFilesByPriority,
             externalPackageHelper));
     map.put(SkyFunctions.CONTAINING_PACKAGE_LOOKUP, new ContainingPackageLookupFunction());
-    map.put(SkyFunctions.PROJECT_DIRECTORIES, new ProjectOwnedCodePathsFunction());
+    map.put(SkyFunctions.PROJECT, new ProjectFunction());
     map.put(SkyFunctions.PROJECT_FILES_LOOKUP, new ProjectFilesLookupFunction());
     map.put(
         SkyFunctions.BZL_COMPILE, // TODO rename
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/config/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/config/BUILD
index 9e94bb2..9d0113a 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/config/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/config/BUILD
@@ -52,6 +52,7 @@
         "//src/main/java/com/google/devtools/build/lib/skyframe:bzl_load_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:package_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:precomputed_value",
+        "//src/main/java/com/google/devtools/build/lib/skyframe:project_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:repository_mapping_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:workspace_name_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe/toolchains:platform_lookup_util",
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetFunction.java
index 37a5ade..607108c 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetFunction.java
@@ -13,17 +13,16 @@
 // limitations under the License.
 package com.google.devtools.build.lib.skyframe.config;
 
+import static com.google.devtools.build.lib.cmdline.RepositoryName.MAIN;
+
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.analysis.config.BuildOptions;
 import com.google.devtools.build.lib.analysis.config.BuildOptionsView;
 import com.google.devtools.build.lib.analysis.config.transitions.PatchTransition;
-import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.Label.RepoContext;
-import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.events.EventHandler;
-import com.google.devtools.build.lib.skyframe.BzlLoadFailedException;
-import com.google.devtools.build.lib.skyframe.BzlLoadValue;
+import com.google.devtools.build.lib.skyframe.ProjectValue;
 import com.google.devtools.build.lib.skyframe.RepositoryMappingValue;
 import com.google.devtools.build.lib.skyframe.config.ParsedFlagsFunction.ParsedFlagsFunctionException;
 import com.google.devtools.build.skyframe.SkyFunction;
@@ -60,25 +59,25 @@
       return FlagSetValue.create(key.getTargetOptions());
     }
 
-    BzlLoadValue sclLoadValue = loadSclFile(key.getProjectFile(), env);
-
-    if (sclLoadValue == null) {
+    ProjectValue projectValue =
+        (ProjectValue) env.getValue(new ProjectValue.Key(key.getProjectFile()));
+    if (projectValue == null) {
       return null;
     }
 
     RepositoryMappingValue mainRepositoryMappingValue =
-        (RepositoryMappingValue) env.getValue(RepositoryMappingValue.key(RepositoryName.MAIN));
+        (RepositoryMappingValue) env.getValue(RepositoryMappingValue.key(MAIN));
     if (mainRepositoryMappingValue == null) {
       return null;
     }
 
     RepoContext mainRepoContext =
-        RepoContext.of(RepositoryName.MAIN, mainRepositoryMappingValue.getRepositoryMapping());
+        RepoContext.of(MAIN, mainRepositoryMappingValue.getRepositoryMapping());
 
     List<String> rawFlags = new ArrayList<>();
-    if (sclLoadValue.getModule().getGlobal(key.getSclConfig()) != null) {
+    if (projectValue.getResidualGlobal(key.getSclConfig()) != null) {
       rawFlags.addAll(
-          (Collection<? extends String>) sclLoadValue.getModule().getGlobal(key.getSclConfig()));
+          (Collection<? extends String>) projectValue.getResidualGlobal(key.getSclConfig()));
     } else {
       return FlagSetValue.create(key.getTargetOptions());
     }
@@ -114,20 +113,6 @@
     return FlagSetValue.create(adjustedBuildOptions);
   }
 
-  private BzlLoadValue loadSclFile(Label sclFileLabel, Environment env)
-      throws FlagSetFunctionException, InterruptedException {
-    BzlLoadValue bzlLoadValue;
-    try {
-      bzlLoadValue =
-          (BzlLoadValue)
-              env.getValueOrThrow(
-                  BzlLoadValue.keyForBuild(sclFileLabel), BzlLoadFailedException.class);
-    } catch (BzlLoadFailedException e) {
-      throw new FlagSetFunctionException(e, Transience.PERSISTENT);
-    }
-    return bzlLoadValue;
-  }
-
   private static final class FlagSetFunctionException extends SkyFunctionException {
     FlagSetFunctionException(Exception cause, Transience transience) {
       super(cause, transience);
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetValue.java
index ae2a4c2..232c51c 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/config/FlagSetValue.java
@@ -33,7 +33,6 @@
   @AutoCodec
   public static final class Key implements SkyKey {
     private static final SkyKeyInterner<Key> interner = SkyKey.newInterner();
-    // private final String sclFile;
     private final Label projectFile;
     private final String sclConfig;
     private final BuildOptions targetOptions;
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
index c8c3f0e..f803b79 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/BUILD
@@ -680,6 +680,21 @@
 )
 
 java_test(
+    name = "ProjectFunctionTest",
+    srcs = ["ProjectFunctionTest.java"],
+    deps = [
+        ":testutil",
+        "//src/main/java/com/google/devtools/build/lib/cmdline",
+        "//src/main/java/com/google/devtools/build/lib/skyframe:project_value",
+        "//src/main/java/com/google/devtools/build/skyframe",
+        "//src/main/java/net/starlark/java/eval",
+        "//src/test/java/com/google/devtools/build/lib/analysis/util",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
+java_test(
     name = "ActionExecutionValueTransformSharedTreeArtifactsTest",
     timeout = "short",
     srcs = ["ActionExecutionValueTransformSharedTreeArtifactsTest.java"],
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ProjectFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ProjectFunctionTest.java
new file mode 100644
index 0000000..c68a7e2
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ProjectFunctionTest.java
@@ -0,0 +1,139 @@
+// Copyright 2024 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.skyframe;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import java.util.Collection;
+import net.starlark.java.eval.StarlarkInt;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ProjectFunctionTest extends BuildViewTestCase {
+
+  @Before
+  public void setUp() throws Exception {
+    setBuildLanguageOptions("--experimental_enable_scl_dialect=true");
+  }
+
+  @Test
+  public void projectFunction_emptyFile_isValid() throws Exception {
+    scratch.file("test/PROJECT.scl", "");
+    scratch.file("test/BUILD");
+    ProjectValue.Key key = new ProjectValue.Key(Label.parseCanonical("//test:PROJECT.scl"));
+
+    EvaluationResult<ProjectValue> result =
+        SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, false, reporter);
+    assertThat(result.hasError()).isFalse();
+
+    ProjectValue value = result.get(key);
+    assertThat(value.getOwnedCodePaths()).isEmpty();
+  }
+
+  @Test
+  public void projectFunction_returnsOwnedCodePaths() throws Exception {
+    scratch.file("test/PROJECT.scl", "owned_code_paths = ['a', 'b/c']");
+    scratch.file("test/BUILD");
+    ProjectValue.Key key = new ProjectValue.Key(Label.parseCanonical("//test:PROJECT.scl"));
+
+    EvaluationResult<ProjectValue> result =
+        SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, false, reporter);
+    assertThat(result.hasError()).isFalse();
+
+    ProjectValue value = result.get(key);
+    assertThat(value.getOwnedCodePaths()).containsExactly("a", "b/c");
+  }
+
+  @Test
+  public void projectFunction_incorrectType() throws Exception {
+    scratch.file("test/PROJECT.scl", "owned_code_paths = 42");
+    scratch.file("test/BUILD");
+    ProjectValue.Key key = new ProjectValue.Key(Label.parseCanonical("//test:PROJECT.scl"));
+
+    EvaluationResult<ProjectValue> result =
+        SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, false, reporter);
+    assertThat(result.hasError()).isTrue();
+    assertThat(result.getError().getException())
+        .hasMessageThat()
+        .matches("expected a list of strings, got .+Int32");
+  }
+
+  @Test
+  public void projectFunction_incorrectType_inList() throws Exception {
+    scratch.file("test/PROJECT.scl", "owned_code_paths = [42]");
+    scratch.file("test/BUILD");
+    ProjectValue.Key key = new ProjectValue.Key(Label.parseCanonical("//test:PROJECT.scl"));
+
+    EvaluationResult<ProjectValue> result =
+        SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, false, reporter);
+    assertThat(result.hasError()).isTrue();
+    assertThat(result.getError().getException())
+        .hasMessageThat()
+        .matches("expected a list of strings, got element of .+Int32");
+  }
+
+  @Test
+  public void projectFunction_parsesResidualGlobals() throws Exception {
+    scratch.file(
+        "test/PROJECT.scl",
+        """
+        owned_code_paths = ["a", "b/c"]
+        foo = [0, 1]
+        bar = 'str'
+        """);
+    scratch.file("test/BUILD");
+    ProjectValue.Key key = new ProjectValue.Key(Label.parseCanonical("//test:PROJECT.scl"));
+
+    EvaluationResult<ProjectValue> result =
+        SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, false, reporter);
+    assertThat(result.hasError()).isFalse();
+
+    ProjectValue value = result.get(key);
+    assertThat(value.getOwnedCodePaths()).containsExactly("a", "b/c");
+    assertThat(value.getResidualGlobal("owned_code_paths")).isNull();
+    assertThat(value.getResidualGlobal("nonexistent_global")).isNull();
+
+    @SuppressWarnings("unchecked")
+    Collection<StarlarkInt> fooValue = (Collection<StarlarkInt>) value.getResidualGlobal("foo");
+    assertThat(fooValue).containsExactly(StarlarkInt.of(0), StarlarkInt.of(1));
+
+    String barValue = (String) value.getResidualGlobal("bar");
+    assertThat(barValue).isEqualTo("str");
+  }
+
+  @Test
+  public void projectFunction_catchSyntaxError() throws Exception {
+    scratch.file(
+        "test/PROJECT.scl",
+        """
+        something_is_wrong =
+        """);
+    scratch.file("test/BUILD");
+    ProjectValue.Key key = new ProjectValue.Key(Label.parseCanonical("//test:PROJECT.scl"));
+
+    AssertionError e =
+        assertThrows(
+            AssertionError.class,
+            () -> SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, false, reporter));
+    assertThat(e).hasMessageThat().contains("syntax error at 'newline': expected expression");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/config/FlagSetsFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/config/FlagSetsFunctionTest.java
index a8c7352..7c46302 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/config/FlagSetsFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/config/FlagSetsFunctionTest.java
@@ -87,7 +87,7 @@
   }
 
   @Test
-  public void flagSetsFunction_returns_origional_buildOptions() throws Exception {
+  public void flagSetsFunction_returns_original_buildOptions() throws Exception {
     // given original BuildOptions and an empty scl config name
     BuildOptions buildOptions =
         BuildOptions.getDefaultBuildOptionsForFragments(