bazel syntax: assert validity of results from Java calls

Other than legal Starlark values, the implicit conversion to Starlark
now accepts only null, List, and Map.

- make Attribute.{Computed,LateBound}Default implement StarlarkValue
- move SkylarkType.convertToSkylark to Starlark.fromJava.
- move SkylarkType.isSkylarkAcceptable to Starlark.valid.
- move support for legacy implicit NestedSet conversion to MethodDescriptor.
  I was hoping to delete this but there are still a dozen or so clients to fix.

PiperOrigin-RevId: 281382879
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/configuredtargets/AbstractConfiguredTarget.java b/src/main/java/com/google/devtools/build/lib/analysis/configuredtargets/AbstractConfiguredTarget.java
index 2a0ddc9..67d43eb 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/configuredtargets/AbstractConfiguredTarget.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/configuredtargets/AbstractConfiguredTarget.java
@@ -38,10 +38,9 @@
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.EvalUtils;
-import com.google.devtools.build.lib.syntax.Mutability;
 import com.google.devtools.build.lib.syntax.Printer;
 import com.google.devtools.build.lib.syntax.SkylarkClassObject;
-import com.google.devtools.build.lib.syntax.SkylarkType;
+import com.google.devtools.build.lib.syntax.Starlark;
 import com.google.devtools.build.lib.syntax.StarlarkSemantics;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
@@ -144,10 +143,7 @@
         // Depending on subclass, the 'actions' field will either be unsupported or of type
         // java.util.List, which needs to be converted to Sequence before being returned.
         Object result = get(name);
-        if (result != null) {
-          result = SkylarkType.convertToSkylark(result, (Mutability) null);
-        }
-        return result;
+        return result != null ? Starlark.fromJava(result, null) : null;
       default:
         return get(name);
     }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttributesCollection.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttributesCollection.java
index d7a61e9..f4ff4a9 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttributesCollection.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttributesCollection.java
@@ -30,9 +30,7 @@
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Sequence;
-import com.google.devtools.build.lib.syntax.SkylarkType;
 import com.google.devtools.build.lib.syntax.Starlark;
-import com.google.devtools.build.lib.syntax.StarlarkThread;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -166,12 +164,8 @@
       // LABEL_DICT_UNARY was previously not treated as a dependency-bearing type, and was put into
       // Skylark as a Map<String, Label>; this special case preserves that behavior temporarily.
       if (type.getLabelClass() != LabelClass.DEPENDENCY || type == BuildType.LABEL_DICT_UNARY) {
-        attrBuilder.put(
-            skyname,
-            val == null
-                ? Starlark.NONE
-                // Attribute values should be type safe
-                : SkylarkType.convertToSkylark(val, (StarlarkThread) null));
+        // Attribute values should be type safe
+        attrBuilder.put(skyname, Starlark.fromJava(val, null));
         return;
       }
       if (a.isExecutable()) {
@@ -227,8 +221,7 @@
         for (TransitiveInfoCollection prereq : allPrereq) {
           builder.put(prereq, original.get(AliasProvider.getDependencyLabel(prereq)));
         }
-        attrBuilder.put(
-            skyname, SkylarkType.convertToSkylark(builder.build(), (StarlarkThread) null));
+        attrBuilder.put(skyname, Starlark.fromJava(builder.build(), null));
       } else if (type == BuildType.LABEL_DICT_UNARY) {
         Map<Label, TransitiveInfoCollection> prereqsByLabel = new LinkedHashMap<>();
         for (TransitiveInfoCollection target :
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkAttributeTransitionProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkAttributeTransitionProvider.java
index ad4314f..d6eb362 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkAttributeTransitionProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkAttributeTransitionProvider.java
@@ -34,9 +34,7 @@
 import com.google.devtools.build.lib.skylarkbuildapi.SplitTransitionProviderApi;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.syntax.EvalException;
-import com.google.devtools.build.lib.syntax.SkylarkType;
 import com.google.devtools.build.lib.syntax.Starlark;
-import com.google.devtools.build.lib.syntax.StarlarkThread;
 import java.util.LinkedHashMap;
 import java.util.List;
 
@@ -94,9 +92,7 @@
       LinkedHashMap<String, Object> attributes = new LinkedHashMap<>();
       for (String attribute : attributeMap.getAttributeNames()) {
         Object val = attributeMap.get(attribute, attributeMap.getAttributeType(attribute));
-        attributes.put(
-            Attribute.getSkylarkName(attribute),
-            val == null ? Starlark.NONE : SkylarkType.convertToSkylark(val, (StarlarkThread) null));
+        attributes.put(Attribute.getSkylarkName(attribute), Starlark.fromJava(val, null));
       }
       attrObject = StructProvider.STRUCT.create(attributes, ERROR_MESSAGE_FOR_NO_ATTR);
     }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkRuleTransitionProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkRuleTransitionProvider.java
index df22024..84bf516 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkRuleTransitionProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkRuleTransitionProvider.java
@@ -30,9 +30,7 @@
 import com.google.devtools.build.lib.packages.StructImpl;
 import com.google.devtools.build.lib.packages.StructProvider;
 import com.google.devtools.build.lib.syntax.EvalException;
-import com.google.devtools.build.lib.syntax.SkylarkType;
 import com.google.devtools.build.lib.syntax.Starlark;
-import com.google.devtools.build.lib.syntax.StarlarkThread;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Objects;
@@ -90,8 +88,7 @@
           continue;
         }
         attributes.put(
-            Attribute.getSkylarkName(attribute.getPublicName()),
-            val == null ? Starlark.NONE : SkylarkType.convertToSkylark(val, (StarlarkThread) null));
+            Attribute.getSkylarkName(attribute.getPublicName()), Starlark.fromJava(val, null));
       }
       attrObject =
           StructProvider.STRUCT.create(
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
index b6c0481..1b9973f 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/skylark/SkylarkRepositoryContext.java
@@ -54,10 +54,8 @@
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.EvalUtils;
 import com.google.devtools.build.lib.syntax.Sequence;
-import com.google.devtools.build.lib.syntax.SkylarkType;
 import com.google.devtools.build.lib.syntax.Starlark;
 import com.google.devtools.build.lib.syntax.StarlarkSemantics;
-import com.google.devtools.build.lib.syntax.StarlarkThread;
 import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.util.OsUtils;
 import com.google.devtools.build.lib.util.StringUtilities;
@@ -138,13 +136,9 @@
     ImmutableMap.Builder<String, Object> attrBuilder = new ImmutableMap.Builder<>();
     for (String name : attrs.getAttributeNames()) {
       if (!name.equals("$local")) {
-        Object val = attrs.getObject(name);
+        // Attribute values should be type safe
         attrBuilder.put(
-            Attribute.getSkylarkName(name),
-            val == null
-                ? Starlark.NONE
-                // Attribute values should be type safe
-                : SkylarkType.convertToSkylark(val, (StarlarkThread) null));
+            Attribute.getSkylarkName(name), Starlark.fromJava(attrs.getObject(name), null));
       }
     }
     attrObject = StructProvider.STRUCT.create(attrBuilder.build(), "No such attribute '%s'");
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Attribute.java b/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
index 30c6b62..ce7f1d7 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Attribute.java
@@ -41,6 +41,7 @@
 import com.google.devtools.build.lib.packages.Type.LabelClass;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
 import com.google.devtools.build.lib.syntax.ClassObject;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.EvalUtils;
@@ -1287,8 +1288,8 @@
    * must be true:
    *
    * <ol>
-   * <li>The other attribute must be declared in the computed default's constructor
-   * <li>The other attribute must be non-configurable ({@link Builder#nonconfigurable}
+   *   <li>The other attribute must be declared in the computed default's constructor
+   *   <li>The other attribute must be non-configurable ({@link Builder#nonconfigurable}
    * </ol>
    *
    * <p>The reason for enforced declarations is that, since attribute values might be configurable,
@@ -1300,7 +1301,7 @@
    *
    * <p>Implementations of this interface must be immutable.
    */
-  public abstract static class ComputedDefault {
+  public abstract static class ComputedDefault implements SkylarkValue {
     private final ImmutableList<String> dependencies;
 
     /**
@@ -1610,7 +1611,7 @@
    *     Label}, or a {@link List} of {@link Label} objects.
    */
   @Immutable
-  public abstract static class LateBoundDefault<FragmentT, ValueT> {
+  public abstract static class LateBoundDefault<FragmentT, ValueT> implements SkylarkValue {
     /**
      * Functional interface for computing the value of a late-bound attribute.
      *
diff --git a/src/main/java/com/google/devtools/build/lib/packages/SkylarkInfo.java b/src/main/java/com/google/devtools/build/lib/packages/SkylarkInfo.java
index 85960f9..dcf2a77 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/SkylarkInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/SkylarkInfo.java
@@ -30,9 +30,8 @@
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.EvalUtils;
 import com.google.devtools.build.lib.syntax.SkylarkClassObject;
-import com.google.devtools.build.lib.syntax.SkylarkType;
+import com.google.devtools.build.lib.syntax.Starlark;
 import com.google.devtools.build.lib.syntax.StarlarkSemantics;
-import com.google.devtools.build.lib.syntax.StarlarkThread;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -337,7 +336,10 @@
       this.values = new Object[values.length];
       for (int i = 0; i < values.length; i++) {
         // TODO(b/74396075): Phase out this unnecessary conversion.
-        this.values[i] = SkylarkType.convertToSkylark(values[i], (StarlarkThread) null);
+        // NB: fromJava treats null as None, but we need nulls to indicate a field is not present.
+        if (values[i] != null) {
+          this.values[i] = Starlark.fromJava(values[i], null);
+        }
       }
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/packages/SkylarkNativeModule.java b/src/main/java/com/google/devtools/build/lib/packages/SkylarkNativeModule.java
index 2ac982d..f15297b 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/SkylarkNativeModule.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/SkylarkNativeModule.java
@@ -39,7 +39,6 @@
 import com.google.devtools.build.lib.syntax.Mutability;
 import com.google.devtools.build.lib.syntax.NoneType;
 import com.google.devtools.build.lib.syntax.Sequence;
-import com.google.devtools.build.lib.syntax.SkylarkType;
 import com.google.devtools.build.lib.syntax.SkylarkUtils;
 import com.google.devtools.build.lib.syntax.Starlark;
 import com.google.devtools.build.lib.syntax.StarlarkList;
@@ -385,7 +384,7 @@
 
         m.put(key, mapVal);
       }
-      return SkylarkType.convertToSkylark(m, mu);
+      return Starlark.fromJava(m, mu);
     }
     if (val.getClass().isAnonymousClass()) {
       // Computed defaults. They will be represented as
diff --git a/src/main/java/com/google/devtools/build/lib/packages/StructImpl.java b/src/main/java/com/google/devtools/build/lib/packages/StructImpl.java
index 51d7c6a..835d9b6 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/StructImpl.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/StructImpl.java
@@ -31,7 +31,6 @@
 import com.google.devtools.build.lib.syntax.Sequence;
 import com.google.devtools.build.lib.syntax.SkylarkType;
 import com.google.devtools.build.lib.syntax.Starlark;
-import com.google.devtools.build.lib.syntax.StarlarkThread;
 import com.google.protobuf.TextFormat;
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -67,9 +66,7 @@
     Preconditions.checkNotNull(values);
     ImmutableSortedMap.Builder<String, Object> builder = ImmutableSortedMap.naturalOrder();
     for (Map.Entry<String, Object> e : values.entrySet()) {
-      builder.put(
-          Attribute.getSkylarkName(e.getKey()),
-          SkylarkType.convertToSkylark(e.getValue(), (StarlarkThread) null));
+      builder.put(Attribute.getSkylarkName(e.getKey()), Starlark.fromJava(e.getValue(), null));
     }
     return builder.build();
   }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Dict.java b/src/main/java/com/google/devtools/build/lib/syntax/Dict.java
index 06083b6..a8d7079 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Dict.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Dict.java
@@ -356,7 +356,7 @@
   @SuppressWarnings("unchecked")
   private <KK extends K, VV extends V> Dict<K, V> putAllUnsafe(Map<KK, VV> m) {
     for (Map.Entry<KK, VV> e : m.entrySet()) {
-      contents.put(e.getKey(), (VV) SkylarkType.convertToSkylark(e.getValue(), mutability));
+      contents.put(e.getKey(), (VV) Starlark.fromJava(e.getValue(), mutability));
     }
     return this;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
index b468fd5..10401f5 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/EvalUtils.java
@@ -19,7 +19,6 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
-import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkInterfaceUtils;
@@ -87,8 +86,8 @@
             return Integer.compare((Integer) o1, (Integer) o2);
           }
 
-          o1 = SkylarkType.convertToSkylark(o1, (StarlarkThread) null);
-          o2 = SkylarkType.convertToSkylark(o2, (StarlarkThread) null);
+          o1 = Starlark.fromJava(o1, null);
+          o2 = Starlark.fromJava(o2, null);
 
           if (o1 instanceof Sequence
               && o2 instanceof Sequence
@@ -169,20 +168,6 @@
         || c.equals(Boolean.class);
   }
 
-  /**
-   * Returns true if the type is acceptable to be returned to the Skylark language.
-   */
-  public static boolean isSkylarkAcceptable(Class<?> c) {
-    return SkylarkValue.class.isAssignableFrom(c) // implements SkylarkValue
-        || c.equals(String.class) // basic values
-        || c.equals(Integer.class)
-        || c.equals(Boolean.class)
-        // TODO(adonovan): delete those below, and order those above by cost.
-        // there is a registered Skylark ancestor class (useful e.g. when using AutoValue)
-        || SkylarkInterfaceUtils.getSkylarkModule(c) != null
-        || ImmutableMap.class.isAssignableFrom(c); // will be converted to Dict
-  }
-
   // TODO(bazel-team): move the following few type-related functions to SkylarkType
   /**
    * Return the Skylark-type of {@code c}
@@ -288,18 +273,6 @@
     }
   }
 
-  public static Object checkNotNull(Expression expr, Object obj) throws EvalException {
-    if (obj == null) {
-      throw new EvalException(
-          expr.getLocation(),
-          "unexpected null value, please send a bug report. "
-              + "This was generated by expression '"
-              + expr
-              + "'");
-    }
-    return obj;
-  }
-
   public static Collection<?> toCollection(Object o, Location loc) throws EvalException {
     if (o instanceof Collection) {
       return (Collection<?>) o;
@@ -549,24 +522,7 @@
       // ClassObjects may have fields that are annotated with @SkylarkCallable.
       // Since getValue() does not know about those, we cannot expect that result is a valid object.
       if (result != null) {
-        result = SkylarkType.convertToSkylark(result, thread);
-        // If we access NestedSets using ClassObject.getValue() we won't know the generic type,
-        // so we have to disable it. This should not happen.
-
-        // TODO(bazel-team): Unify this check with the logic in getSkylarkType. Might
-        // break some providers whose contents don't implement SkylarkValue, aren't wrapped in
-        // Sequence, etc.
-        // TODO(adonovan): this is still far too permissive. Replace with isSkylarkAcceptable.
-        if (result instanceof NestedSet
-            || (result instanceof List && !(result instanceof Sequence))) {
-          throw new EvalException(
-              loc,
-              "internal error: type '"
-                  + result.getClass().getSimpleName()
-                  + "' is not allowed as a "
-                  + "Starlark value");
-        }
-        return result;
+        return Starlark.fromJava(result, thread.mutability());
       }
     }
     if (method != null) {
@@ -983,10 +939,10 @@
       throws EvalException, InterruptedException {
     if (object instanceof SkylarkIndexable) {
       Object result = ((SkylarkIndexable) object).getIndex(key, loc);
-      // TODO(bazel-team): We shouldn't have this convertToSkylark call here. If it's needed at all,
+      // TODO(bazel-team): We shouldn't have this fromJava call here. If it's needed at all,
       // it should go in the implementations of SkylarkIndexable#getIndex that produce non-Skylark
       // values.
-      return SkylarkType.convertToSkylark(result, thread);
+      return result == null ? null : Starlark.fromJava(result, thread.mutability());
     } else if (object instanceof String) {
       String string = (String) object;
       int index = getSequenceIndex(key, string.length(), loc);
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/MethodDescriptor.java b/src/main/java/com/google/devtools/build/lib/syntax/MethodDescriptor.java
index c9cc1f5..c1414a2 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/MethodDescriptor.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/MethodDescriptor.java
@@ -17,10 +17,14 @@
 import com.google.common.base.Preconditions;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
 import java.util.Arrays;
 
 /**
@@ -150,6 +154,9 @@
       return Starlark.NONE;
     }
     if (result == null) {
+      // TODO(adonovan): eliminate allowReturnNones. Given that we convert
+      // String/Integer/Boolean/List/Map, it seems obtuse to crash instead
+      // of converting null too.
       if (isAllowReturnNones()) {
         return Starlark.NONE;
       } else {
@@ -157,15 +164,37 @@
             "method invocation returned None: " + getName() + Tuple.copyOf(Arrays.asList(args)));
       }
     }
-    // TODO(bazel-team): get rid of this, by having everyone use the Skylark data structures
-    result = SkylarkType.convertToSkylark(result, method, thread);
-    if (result != null && !EvalUtils.isSkylarkAcceptable(result.getClass())) {
-      throw new EvalException(
-          loc,
-          Printer.format(
-              "method '%s' returns an object of invalid type %r", getName(), result.getClass()));
+
+    // TODO(adonovan): eliminate this hack.
+    // There are about a dozen methods that return NestedSet directly.
+    if (result instanceof NestedSet<?>) {
+      return SkylarkNestedSet.of(resultType(), (NestedSet<?>) result);
     }
-    return result;
+
+    // Careful: thread may be null when we are called by invokeStructField.
+    return Starlark.fromJava(result, thread != null ? thread.mutability() : null);
+  }
+
+  // legacy hack for NestedSet
+  private SkylarkType resultType() {
+    // This is where we can infer generic type information, so SkylarkNestedSets can be
+    // created in a safe way. Eventually we should probably do something with Lists and Maps too.
+    ParameterizedType t = (ParameterizedType) method.getGenericReturnType();
+    Type type = t.getActualTypeArguments()[0];
+    if (type instanceof Class) {
+      return SkylarkType.of((Class<?>) type);
+    }
+    if (type instanceof WildcardType) {
+      WildcardType wildcard = (WildcardType) type;
+      Type upperBound = wildcard.getUpperBounds()[0];
+      if (upperBound instanceof Class) {
+        // i.e. List<? extends SuperClass>
+        return SkylarkType.of((Class<?>) upperBound);
+      }
+    }
+    // It means someone annotated a method with @SkylarkCallable with no specific generic type info.
+    // We shouldn't annotate methods which return List<?> or List<T>.
+    throw new IllegalStateException("Cannot infer type from method signature " + method);
   }
 
   /** @see SkylarkCallable#name() */
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkNestedSet.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkNestedSet.java
index 755a328..d276581 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkNestedSet.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkNestedSet.java
@@ -181,7 +181,7 @@
   // TODO(adonovan): enforce that we never construct a SkylarkNestedSet with a StarlarkType
   // that represents a non-Skylark type (e.g. NestedSet<PathFragment>).
   // One way to do that is to disallow constructing StarlarkTypes for classes
-  // that would fail isSkylarkAcceptable; however remains the problem that
+  // that would fail Starlark.valid; however remains the problem that
   // Object.class means "any Starlark value" but in fact allows any Java value.
   public static <T> SkylarkNestedSet of(SkylarkType contentType, NestedSet<T> set) {
     return new SkylarkNestedSet(contentType, set, null, null);
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkType.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkType.java
index 39fe0bf..7367f40 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkType.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkType.java
@@ -21,16 +21,10 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Interner;
-import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.concurrent.BlazeInterners;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization;
-import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
-import java.lang.reflect.Method;
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
-import java.lang.reflect.WildcardType;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -553,9 +547,6 @@
     public static SkylarkType of(SkylarkType t1, SkylarkType t2) {
       return of(ImmutableList.of(t1, t2));
     }
-    public static SkylarkType of(Class<?> t1, Class<?> t2) {
-      return of(Simple.forClass(t1), Simple.forClass(t2));
-    }
   }
 
   // TODO(adonovan): eliminate this function: a value may belong to many types.
@@ -728,63 +719,9 @@
     return (Map<KEY_TYPE, VALUE_TYPE>) obj;
   }
 
-  private static Class<?> getGenericTypeFromMethod(Method method) {
-    // This is where we can infer generic type information, so SkylarkNestedSets can be
-    // created in a safe way. Eventually we should probably do something with Lists and Maps too.
-    ParameterizedType t = (ParameterizedType) method.getGenericReturnType();
-    Type type = t.getActualTypeArguments()[0];
-    if (type instanceof Class) {
-      return (Class<?>) type;
-    }
-    if (type instanceof WildcardType) {
-      WildcardType wildcard = (WildcardType) type;
-      Type upperBound = wildcard.getUpperBounds()[0];
-      if (upperBound instanceof Class) {
-        // i.e. List<? extends SuperClass>
-        return (Class<?>) upperBound;
-      }
-    }
-    // It means someone annotated a method with @SkylarkCallable with no specific generic type info.
-    // We shouldn't annotate methods which return List<?> or List<T>.
-    throw new IllegalStateException("Cannot infer type from method signature " + method);
-  }
-
-  /** Converts an object retrieved from a Java method to a Skylark-compatible type. */
-  static Object convertToSkylark(Object object, Method method, @Nullable StarlarkThread thread) {
-    if (object instanceof NestedSet<?>) {
-      return SkylarkNestedSet.of(
-          SkylarkType.of(getGenericTypeFromMethod(method)), (NestedSet<?>) object);
-    }
-    return convertToSkylark(object, thread);
-  }
-
-  /** Converts an object to a Skylark-compatible type if possible. */
-  public static Object convertToSkylark(Object object, @Nullable StarlarkThread thread) {
-    return convertToSkylark(object, thread == null ? null : thread.mutability());
-  }
-
-  /**
-   * Converts an object to a Skylark-compatible type if possible.
-   */
-  public static Object convertToSkylark(Object object, @Nullable Mutability mutability) {
-    if (object instanceof List && !(object instanceof Sequence)) {
-      return StarlarkList.copyOf(mutability, (List<?>) object);
-    }
-    if (object instanceof SkylarkValue) {
-      return object;
-    }
-    if (object instanceof Map) {
-      return Dict.<Object, Object>copyOf(mutability, (Map<?, ?>) object);
-    }
-    // TODO(bazel-team): ensure everything is a SkylarkValue at all times.
-    // Preconditions.checkArgument(EvalUtils.isSkylarkAcceptable(
-    //    object.getClass()),
-    //    "invalid object %s of class %s not convertible to a Skylark value",
-    //    object,
-    //    object.getClass());
-    return object;
-  }
-
+  // TODO(adonovan): eliminate 4 uses outside this package and make it private.
+  // The check is trivial (instanceof) and clients can usually produce a better
+  // error in context, without prematurely constructing a description.
   public static void checkType(Object object, Class<?> type, @Nullable Object description)
       throws EvalException {
     if (!type.isInstance(object)) {
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Starlark.java b/src/main/java/com/google/devtools/build/lib/syntax/Starlark.java
index 875f2d6..79214b2 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Starlark.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Starlark.java
@@ -20,6 +20,7 @@
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
 import com.google.devtools.build.lib.util.Pair;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import javax.annotation.Nullable;
@@ -79,6 +80,51 @@
   }
 
   /**
+   * Reports whether the argument is a legal Starlark value: a string, boolean, integer, or
+   * StarlarkValue.
+   */
+  public static boolean valid(Object x) {
+    return x instanceof SkylarkValue
+        || x instanceof String
+        || x instanceof Boolean
+        || x instanceof Integer;
+  }
+
+  /**
+   * Returns {@code x} if it is a {@link #valid} Starlark value, otherwise throws
+   * IllegalArgumentException.
+   */
+  public static <T> T checkValid(T x) {
+    if (!valid(x)) {
+      throw new IllegalArgumentException("invalid Starlark value: " + x.getClass());
+    }
+    return x;
+  }
+
+  /**
+   * Converts a Java value {@code x} to a Starlark one, if x is not already a valid Starlark value.
+   * A Java List or Map is converted to a Starlark list or dict, respectively, and null becomes
+   * {@link #NONE}. Any other non-Starlark value causes the function to throw
+   * IllegalArgumentException.
+   *
+   * <p>This function is applied to the results of @SkylarkCallable-annotated Java methods.
+   */
+  public static Object fromJava(Object x, @Nullable Mutability mutability) {
+    if (x == null) {
+      return NONE;
+    } else if (Starlark.valid(x)) {
+      return x;
+    } else if (x instanceof List) {
+      return StarlarkList.copyOf(mutability, (List<?>) x);
+    } else if (x instanceof Map) {
+      return Dict.copyOf(mutability, (Map<?, ?>) x);
+    } else {
+      throw new IllegalArgumentException(
+          "cannot expose internal type to Starlark: " + x.getClass());
+    }
+  }
+
+  /**
    * Returns the truth value of a valid Starlark value, as if by the Starlark expression {@code
    * bool(x)}.
    */
diff --git a/src/test/java/com/google/devtools/build/lib/packages/SkylarkInfoTest.java b/src/test/java/com/google/devtools/build/lib/packages/SkylarkInfoTest.java
index 37e28b4..a75e311 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/SkylarkInfoTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/SkylarkInfoTest.java
@@ -23,6 +23,7 @@
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.packages.SkylarkInfo.Layout;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
 import com.google.devtools.build.lib.syntax.EvalException;
 import java.util.Map;
 import javax.annotation.Nullable;
@@ -122,8 +123,9 @@
   @Test
   public void mutableIfContentsAreMutable() throws Exception {
     SkylarkProvider provider = makeExportedProvider();
-    SkylarkInfo mapInfo = makeSchemalessInfoWithF1F2Values(provider, 5, new Object());
-    SkylarkInfo compactInfo = makeSchemafulInfoWithF1F2Values(provider, 5, new Object());
+    SkylarkValue v = new SkylarkValue() {};
+    SkylarkInfo mapInfo = makeSchemalessInfoWithF1F2Values(provider, 5, v);
+    SkylarkInfo compactInfo = makeSchemafulInfoWithF1F2Values(provider, 5, v);
     assertThat(mapInfo.isImmutable()).isFalse();
     assertThat(compactInfo.isImmutable()).isFalse();
   }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java
index 33363fb..95f27fa 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java
@@ -18,7 +18,6 @@
 import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.packages.StructProvider;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
@@ -101,7 +100,6 @@
       StarlarkList.of(thread, "1", "2", "3"),
       Dict.of(thread, "key", 123),
       Dict.of(thread, 123, "value"),
-      NestedSetBuilder.stableOrder().add(1).add(2).add(3).build(),
       StructProvider.STRUCT.create(ImmutableMap.of("key", (Object) "value"), "no field %s"),
     };
 
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
index 902e964..6b8bb04 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
@@ -146,7 +146,7 @@
     public void value() {}
     @SkylarkCallable(name = "return_bad", documented = false)
     public Bad returnBad() {
-      return new Bad();
+      return new Bad(); // not a legal Starlark value
     }
     @SkylarkCallable(name = "struct_field", documented = false, structField = true)
     public String structField() {
@@ -520,7 +520,8 @@
     public Object getValue(String name) {
       switch (name) {
         case "field": return "a";
-        case "nset": return NestedSetBuilder.stableOrder().build();
+        case "nset":
+          return NestedSetBuilder.stableOrder().build(); // not a legal Starlark value
         default: return null;
       }
     }
@@ -1457,11 +1458,15 @@
   }
 
   @Test
-  public void testJavaFunctionReturnsMutableObject() throws Exception {
-    new SkylarkTest()
-        .update("mock", new Mock())
-        .testIfExactError(
-            "method 'return_bad' returns an object of invalid type Bad", "mock.return_bad()");
+  public void testJavaFunctionReturnsIllegalValue() throws Exception {
+    update("mock", new Mock());
+    IllegalArgumentException e =
+        assertThrows(IllegalArgumentException.class, () -> eval("mock.return_bad()"));
+    assertThat(e)
+        .hasMessageThat()
+        .contains(
+            "cannot expose internal type to Starlark: class"
+                + " com.google.devtools.build.lib.syntax.SkylarkEvaluationTest$Bad");
   }
 
   @Test
@@ -1501,10 +1506,15 @@
   }
 
   @Test
-  public void testClassObjectCannotAccessNestedSet() throws Exception {
-    new SkylarkTest()
-        .update("mock", new MockClassObject())
-        .testIfErrorContains("internal error: type 'NestedSet' is not allowed", "v = mock.nset");
+  public void testFieldReturnsNestedSet() throws Exception {
+    update("mock", new MockClassObject());
+    IllegalArgumentException e =
+        assertThrows(IllegalArgumentException.class, () -> eval("mock.nset"));
+    assertThat(e)
+        .hasMessageThat()
+        .contains(
+            "cannot expose internal type to Starlark: class"
+                + " com.google.devtools.build.lib.collect.nestedset.NestedSet");
   }
 
   @Test