Support starlark split transitions returning a list of toOptions instead of a dict (the keys of the dict were never used).

RELNOTES: None.
PiperOrigin-RevId: 246008663
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/StarlarkDefinedConfigTransition.java b/src/main/java/com/google/devtools/build/lib/analysis/config/StarlarkDefinedConfigTransition.java
index b6768ac..67968e7 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/StarlarkDefinedConfigTransition.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/StarlarkDefinedConfigTransition.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Mutability;
 import com.google.devtools.build.lib.syntax.SkylarkDict;
+import com.google.devtools.build.lib.syntax.SkylarkList;
 import com.google.devtools.build.lib.syntax.StarlarkSemantics;
 import java.util.List;
 import java.util.Map;
@@ -98,7 +99,7 @@
    * @throws EvalException if there is an error evaluating the transition
    * @throws InterruptedException if evaluating the transition is interrupted
    */
-  public abstract ImmutableList<Map<String, Object>> getChangedSettings(
+  public abstract ImmutableList<Map<String, Object>> evaluate(
       Map<String, Object> previousSettings, StructImpl attributeMap)
       throws EvalException, InterruptedException;
 
@@ -130,7 +131,7 @@
     }
 
     @Override
-    public ImmutableList<Map<String, Object>> getChangedSettings(
+    public ImmutableList<Map<String, Object>> evaluate(
         Map<String, Object> previousSettings, StructImpl attributeMapper) {
       return ImmutableList.of(changedSettings);
     }
@@ -183,8 +184,25 @@
       return false;
     }
 
+    /**
+     * This method evaluates the implementation function of the transition.
+     *
+     * <p>In the case of a {@link
+     * com.google.devtools.build.lib.analysis.config.transitions.PatchTransition}, the impl fxn
+     * returns a {@link SkylarkDict} of option name strings to option value object.
+     *
+     * <p>In the case of {@link
+     * com.google.devtools.build.lib.analysis.config.transitions.SplitTransition}, the impl fxn can
+     * return either a {@link SkylarkDict} of String keys to {@link SkylarkDict} values. Or it can
+     * return a list of {@link SkylarkDict}s in cases where the consumer doesn't care about
+     * differentiating between the splits (i.e. accessing later via {@code ctx.split_attrs}).
+     *
+     * @param previousSettings a map representing the previous build settings
+     * @param attributeMapper a map of attributes
+     */
+    // TODO(bazel-team): integrate dict-of-dicts return type with ctx.split_attr
     @Override
-    public ImmutableList<Map<String, Object>> getChangedSettings(
+    public ImmutableList<Map<String, Object>> evaluate(
         Map<String, Object> previousSettings, StructImpl attributeMapper)
         throws EvalException, InterruptedException {
       Object result;
@@ -194,43 +212,48 @@
         throw new EvalException(impl.getLocation(), e.getMessage());
       }
 
-      if (!(result instanceof SkylarkDict<?, ?>)) {
-        throw new EvalException(
-            impl.getLocation(), "Transition function must return a dictionary.");
-      }
-
-      // The result is either:
-      // 1. a dictionary mapping option name to new option value (for a single transition), or
-      // 2. a dictionary of such dictionaries (for a split transition).
-      //
-      // First try to parse the result as a dictionary of option dictionaries; then try it as an
-      // option dictionary.
-      SkylarkDict<?, ?> dictOrDictOfDict = (SkylarkDict<?, ?>) result;
-
-      try {
-        Map<String, SkylarkDict> dictOfDict =
-            dictOrDictOfDict.getContents(
-                String.class, SkylarkDict.class, "dictionary of option dictionaries");
-
+      if (result instanceof SkylarkDict<?, ?>) {
+        // TODO(bazel-team): integrate keys with ctx.split_attr. Currently ctx.split_attr always
+        // keys on cpu value - we should be able to key on the keys returned here.
+        try {
+          Map<String, SkylarkDict> dictOfDict =
+              ((SkylarkDict<?, ?>) result)
+                  .getContents(
+                      String.class, SkylarkDict.class, "dictionary of options dictionaries");
+          ImmutableList.Builder<Map<String, Object>> builder = ImmutableList.builder();
+          for (Map.Entry<String, SkylarkDict> entry : dictOfDict.entrySet()) {
+            Map<String, Object> dict =
+                entry.getValue().getContents(String.class, Object.class, "an option dictionary");
+            builder.add(dict);
+          }
+          return builder.build();
+        } catch (EvalException e) {
+          // fall through
+        }
+        try {
+          return ImmutableList.of(
+              ((SkylarkDict<?, ?>) result)
+                  .getContents(String.class, Object.class, "dictionary of options"));
+        } catch (EvalException e) {
+          throw new EvalException(impl.getLocation(), e.getMessage());
+        }
+      } else if (result instanceof SkylarkList<?>) {
         ImmutableList.Builder<Map<String, Object>> builder = ImmutableList.builder();
-        for (Map.Entry<String, SkylarkDict> entry : dictOfDict.entrySet()) {
-          Map<String, Object> dict =
-              entry.getValue().getContents(String.class, Object.class, "an option dictionary");
-          builder.add(dict);
+        try {
+          for (SkylarkDict<?, ?> toOptions :
+              ((SkylarkList<?>) result)
+                  .getContents(SkylarkDict.class, "dictionary of options dictionaries")) {
+            builder.add(toOptions.getContents(String.class, Object.class, "dictionary of options"));
+          }
+        } catch (EvalException e) {
+          throw new EvalException(impl.getLocation(), e.getMessage());
         }
         return builder.build();
-      } catch (EvalException e) {
-        // Fall through.
+      } else {
+        throw new EvalException(
+            impl.getLocation(),
+            "Transition function must return a dictionary or list of dictionaries.");
       }
-
-      Map<String, Object> dict;
-      try {
-        dict = dictOrDictOfDict.getContents(String.class, Object.class, "an option dictionary");
-      } catch (EvalException e) {
-        throw new EvalException(impl.getLocation(), e.getMessage());
-      }
-
-      return ImmutableList.of(dict);
     }
 
     @Override
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java
index 07f2649..22ced4d 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/FunctionTransitionUtil.java
@@ -70,22 +70,22 @@
       throws EvalException, InterruptedException {
     // TODO(waltl): consider building this once and use it across different split
     // transitions.
-      Map<String, OptionInfo> optionInfoMap = buildOptionInfo(buildOptions);
-      SkylarkDict<String, Object> settings =
-          buildSettings(buildOptions, optionInfoMap, starlarkTransition);
+    Map<String, OptionInfo> optionInfoMap = buildOptionInfo(buildOptions);
+    SkylarkDict<String, Object> settings =
+        buildSettings(buildOptions, optionInfoMap, starlarkTransition);
 
-      ImmutableList.Builder<BuildOptions> splitBuildOptions = ImmutableList.builder();
+    ImmutableList.Builder<BuildOptions> splitBuildOptions = ImmutableList.builder();
 
-      ImmutableList<Map<String, Object>> transitions =
-          starlarkTransition.getChangedSettings(settings, attrObject);
+    ImmutableList<Map<String, Object>> transitions =
+        starlarkTransition.evaluate(settings, attrObject);
     validateFunctionOutputsMatchesDeclaredOutputs(transitions, starlarkTransition);
 
-      for (Map<String, Object> transition : transitions) {
-        BuildOptions transitionedOptions =
-            applyTransition(buildOptions, transition, optionInfoMap, starlarkTransition);
-        splitBuildOptions.add(transitionedOptions);
-      }
-      return splitBuildOptions.build();
+    for (Map<String, Object> transition : transitions) {
+      BuildOptions transitionedOptions =
+          applyTransition(buildOptions, transition, optionInfoMap, starlarkTransition);
+      splitBuildOptions.add(transitionedOptions);
+    }
+    return splitBuildOptions.build();
   }
 
   /**
@@ -263,6 +263,10 @@
                 starlarkTransition.getLocationForErrorReporting(),
                 "Invalid value type for option '" + optionName + "'");
           }
+        } catch (IllegalArgumentException e) {
+          throw new EvalException(
+              starlarkTransition.getLocationForErrorReporting(),
+              "IllegalArgumentError for option '" + optionName + "': " + e.getMessage());
         } catch (IllegalAccessException e) {
           throw new RuntimeException(
               "IllegalAccess for option " + optionName + ": " + e.getMessage());
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/StarlarkAttrTransitionProviderTest.java b/src/test/java/com/google/devtools/build/lib/analysis/StarlarkAttrTransitionProviderTest.java
index ce33de9..7965774 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/StarlarkAttrTransitionProviderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/StarlarkAttrTransitionProviderTest.java
@@ -70,6 +70,45 @@
         "test/skylark/my_rule.bzl",
         "load('//myinfo:myinfo.bzl', 'MyInfo')",
         "def transition_func(settings, attr):",
+        "  return [",
+        "    {'//command_line_option:cpu': 'k8'},",
+        "    {'//command_line_option:cpu': 'armeabi-v7a'}",
+        "  ]",
+        "my_transition = transition(implementation = transition_func, inputs = [],",
+        "  outputs = ['//command_line_option:cpu'])",
+        "def impl(ctx): ",
+        "  return MyInfo(",
+        "    split_attr_deps = ctx.split_attr.deps,",
+        "    split_attr_dep = ctx.split_attr.dep,",
+        "    k8_deps = ctx.split_attr.deps.get('k8', None),",
+        "    attr_deps = ctx.attr.deps,",
+        "    attr_dep = ctx.attr.dep)",
+        "my_rule = rule(",
+        "  implementation = impl,",
+        "  attrs = {",
+        "    'deps': attr.label_list(cfg = my_transition),",
+        "    'dep':  attr.label(cfg = my_transition),",
+        "    '_whitelist_function_transition': attr.label(",
+        "        default = '//tools/whitelists/function_transition_whitelist',",
+        "    ),",
+        "  })");
+
+    scratch.file(
+        "test/skylark/BUILD",
+        "load('//test/skylark:my_rule.bzl', 'my_rule')",
+        "my_rule(name = 'test', deps = [':main1', ':main2'], dep = ':main1')",
+        "cc_binary(name = 'main1', srcs = ['main1.c'])",
+        "cc_binary(name = 'main2', srcs = ['main2.c'])");
+  }
+
+  private void writeBasicTestFiles_dictOfDict() throws Exception {
+    setSkylarkSemanticsOptions("--experimental_starlark_config_transitions=true");
+    writeWhitelistFile();
+    getAnalysisMock().ccSupport().setupCcToolchainConfigForCpu(mockToolsConfig, "armeabi-v7a");
+    scratch.file(
+        "test/skylark/my_rule.bzl",
+        "load('//myinfo:myinfo.bzl', 'MyInfo')",
+        "def transition_func(settings, attr):",
         "  return {",
         "      't0': {'//command_line_option:cpu': 'k8'},",
         "      't1': {'//command_line_option:cpu': 'armeabi-v7a'},",
@@ -102,6 +141,12 @@
   }
 
   @Test
+  public void testFunctionSplitTransitionCheckSplitAttrDeps_dictOfDict() throws Exception {
+    writeBasicTestFiles_dictOfDict();
+    testSplitTransitionCheckSplitAttrDeps(getConfiguredTarget("//test/skylark:test"));
+  }
+
+  @Test
   public void testFunctionSplitTransitionCheckSplitAttrDeps() throws Exception {
     writeBasicTestFiles();
     testSplitTransitionCheckSplitAttrDeps(getConfiguredTarget("//test/skylark:test"));
@@ -140,10 +185,10 @@
         "test/not_whitelisted/my_rule.bzl",
         "load('//myinfo:myinfo.bzl', 'MyInfo')",
         "def transition_func(settings, attr):",
-        "  return {",
-        "      't0': {'//command_line_option:cpu': 'k8'},",
-        "      't1': {'//command_line_option:cpu': 'armeabi-v7a'},",
-        "  }",
+        "  return [",
+        "    {'//command_line_option:cpu': 'k8'},",
+        "    {'//command_line_option:cpu': 'armeabi-v7a'}",
+        "  ]",
         "my_transition = transition(implementation = transition_func, inputs = [],",
         "  outputs = ['//command_line_option:cpu'])",
         "def impl(ctx): ",
@@ -258,11 +303,9 @@
         "test/skylark/my_rule.bzl",
         "load('//myinfo:myinfo.bzl', 'MyInfo')",
         "def transition_func(settings, attr):",
-        "  transitions = {}",
+        "  transitions = []",
         "  for cpu in settings['//command_line_option:fat_apk_cpu']:",
-        "    transitions[cpu] = {",
-        "      '//command_line_option:cpu': cpu,",
-        "    }",
+        "    transitions.append({'//command_line_option:cpu': cpu,})",
         "  return transitions",
         "my_transition = transition(implementation = transition_func, ",
         "  inputs = ['//command_line_option:fat_apk_cpu'],",
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/StarlarkRuleTransitionProviderTest.java b/src/test/java/com/google/devtools/build/lib/analysis/StarlarkRuleTransitionProviderTest.java
index fd9bbe0..742a28f 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/StarlarkRuleTransitionProviderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/StarlarkRuleTransitionProviderTest.java
@@ -95,6 +95,35 @@
   }
 
   @Test
+  public void testBadReturnTypeFromTransition() throws Exception {
+    writeWhitelistFile();
+    scratch.file(
+        "test/transitions.bzl",
+        "def _impl(settings, attr):",
+        "  return 'cpu=k8'",
+        "my_transition = transition(implementation = _impl, inputs = [],",
+        "  outputs = ['//command_line_option:test_arg'])");
+    scratch.file(
+        "test/rules.bzl",
+        "load('//test:transitions.bzl', 'my_transition')",
+        "def _impl(ctx):",
+        "  return []",
+        "my_rule = rule(",
+        "  implementation = _impl,",
+        "  cfg = my_transition,",
+        "  attrs = {",
+        "    '_whitelist_function_transition': attr.label(",
+        "        default = '//tools/whitelists/function_transition_whitelist',",
+        "    ),",
+        "  })");
+    scratch.file("test/BUILD", "load('//test:rules.bzl', 'my_rule')", "my_rule(name = 'test')");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//test");
+    assertContainsEvent("Transition function must return a dictionary or list of dictionaries.");
+  }
+
+  @Test
   public void testOutputOnlyTransition() throws Exception {
     writeWhitelistFile();
     scratch.file(
@@ -223,10 +252,10 @@
     scratch.file(
         "test/transitions.bzl",
         "def _impl(settings, attr):",
-        "  return {",
-        "      't0': {'//command_line_option:test_arg': ['split_one']},",
-        "      't1': {'//command_line_option:test_arg': ['split_two']},",
-        "  }",
+        "  return [",
+        "      {'//command_line_option:test_arg': ['split_one']},",
+        "      {'//command_line_option:test_arg': ['split_two']},",
+        "  ]",
         "my_transition = transition(implementation = _impl, inputs = [],",
         "  outputs = ['//command_line_option:test_arg'])");
     scratch.file(