Target pattern parsing cleanup
================

* `TargetPattern#renameRepository` is merged into `TargetPattern.Parser#parse`. This is much less error-prone -- repo mapping should not be an afterthought, but something that should always be applied if possible (sometimes it's not, for example for `blaze query`).
  * This also fixes the bug where calling `native.register_toolchains("//:all")` in `@some_repo//:defs.bzl` registers `@//:all` instead of `@some_repo//:all` (see change in RegisteredExecutionPlatformsTest)
* A new class `SignedTargetPattern` is introduced, which can store whether the pattern is positive or negative (with the `sign` method).
* `TargetPatternValue#keys` is greatly simplified thanks to the changes above; the exception throwing is confined to the parsing step, and the construction of `TargetPatternKey` can happen as a separate step, obviating the whole "skykey or exception" dance.
* Following from the above, the //external package now stores registered toolchains and execution platforms as parsed target patterns, instead of simple strings.

Among other things, this would help implement toolchain registration in bzlmod.

PiperOrigin-RevId: 400720457
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/commands/SyncCommand.java b/src/main/java/com/google/devtools/build/lib/bazel/commands/SyncCommand.java
index 3b78846..ea765a6 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/commands/SyncCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/commands/SyncCommand.java
@@ -13,6 +13,8 @@
 // limitations under the License.
 package com.google.devtools.build.lib.bazel.commands;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -24,6 +26,7 @@
 import com.google.devtools.build.lib.cmdline.LabelConstants;
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.ExtendedEventHandler.ResolvedEvent;
 import com.google.devtools.build.lib.packages.Rule;
@@ -166,12 +169,17 @@
       env.getReporter()
           .post(
               genericArgsCall(
-                  "register_toolchains", fileValue.getPackage().getRegisteredToolchains()));
+                  "register_toolchains",
+                  fileValue.getPackage().getRegisteredToolchains().stream()
+                      .map(TargetPattern::getOriginalPattern)
+                      .collect(toImmutableList())));
       env.getReporter()
           .post(
               genericArgsCall(
                   "register_execution_platforms",
-                  fileValue.getPackage().getRegisteredExecutionPlatforms()));
+                  fileValue.getPackage().getRegisteredExecutionPlatforms().stream()
+                      .map(TargetPattern::getOriginalPattern)
+                      .collect(toImmutableList())));
       env.getReporter().post(new RepositoryOrderEvent(repositoryOrder.build()));
 
       // take all Starlark workspace rules and get their values
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisAndExecutionPhaseRunner.java b/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisAndExecutionPhaseRunner.java
index e9c3e2c..850bdec 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisAndExecutionPhaseRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisAndExecutionPhaseRunner.java
@@ -228,7 +228,7 @@
       CommandEnvironment env, List<String> requestedTargetPatterns)
       throws ViewCreationFailedException {
     ImmutableSet.Builder<String> explicitTargetPatterns = ImmutableSet.builder();
-    TargetPattern.Parser parser = new TargetPattern.Parser(env.getRelativeWorkingDirectory());
+    TargetPattern.Parser parser = TargetPattern.mainRepoParser(env.getRelativeWorkingDirectory());
 
     for (String requestedTargetPattern : requestedTargetPatterns) {
       if (requestedTargetPattern.startsWith("-")) {
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisPhaseRunner.java b/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisPhaseRunner.java
index aeb2397..3e0b428 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisPhaseRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/AnalysisPhaseRunner.java
@@ -315,7 +315,7 @@
       CommandEnvironment env, List<String> requestedTargetPatterns)
       throws ViewCreationFailedException {
     ImmutableSet.Builder<String> explicitTargetPatterns = ImmutableSet.builder();
-    TargetPattern.Parser parser = new TargetPattern.Parser(env.getRelativeWorkingDirectory());
+    TargetPattern.Parser parser = TargetPattern.mainRepoParser(env.getRelativeWorkingDirectory());
 
     for (String requestedTargetPattern : requestedTargetPatterns) {
       if (requestedTargetPattern.startsWith("-")) {
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/BUILD b/src/main/java/com/google/devtools/build/lib/cmdline/BUILD
index bd3fff0..1f204fe 100644
--- a/src/main/java/com/google/devtools/build/lib/cmdline/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/BUILD
@@ -16,6 +16,7 @@
         "LabelConstants.java",
         "LabelSerializationProxy.java",
         "ResolvedTargets.java",
+        "SignedTargetPattern.java",
         "TargetParsingException.java",
         "TargetPattern.java",
         "TargetPatternResolver.java",
@@ -38,6 +39,7 @@
         "//src/main/java/net/starlark/java/annot",
         "//src/main/java/net/starlark/java/eval",
         "//src/main/protobuf:failure_details_java_proto",
+        "//third_party:auto_value",
         "//third_party:error_prone_annotations",
         "//third_party:guava",
         "//third_party:jsr305",
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/SignedTargetPattern.java b/src/main/java/com/google/devtools/build/lib/cmdline/SignedTargetPattern.java
new file mode 100644
index 0000000..a8dc762
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/SignedTargetPattern.java
@@ -0,0 +1,44 @@
+// Copyright 2021 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.cmdline;
+
+import com.google.auto.value.AutoValue;
+
+/** A {@link TargetPattern} with a potential minus sign in front of it, signifying exclusion. */
+@AutoValue
+public abstract class SignedTargetPattern {
+  public abstract TargetPattern pattern();
+
+  public abstract Sign sign();
+
+  public static SignedTargetPattern create(TargetPattern pattern, Sign sign) {
+    return new AutoValue_SignedTargetPattern(pattern, sign);
+  }
+
+  /** Whether this target pattern begins with a minus sign (NEGATIVE) or not (POSITIVE). */
+  public enum Sign {
+    POSITIVE,
+    NEGATIVE
+  }
+
+  public static SignedTargetPattern parse(String pattern, TargetPattern.Parser parser)
+      throws TargetParsingException {
+    if (pattern.startsWith("-")) {
+      return create(parser.parse(pattern.substring(1)), SignedTargetPattern.Sign.NEGATIVE);
+    } else {
+      return create(parser.parse(pattern), SignedTargetPattern.Sign.POSITIVE);
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/TargetPattern.java b/src/main/java/com/google/devtools/build/lib/cmdline/TargetPattern.java
index 4015c1a..536c0f7 100644
--- a/src/main/java/com/google/devtools/build/lib/cmdline/TargetPattern.java
+++ b/src/main/java/com/google/devtools/build/lib/cmdline/TargetPattern.java
@@ -31,6 +31,7 @@
 import com.google.devtools.build.lib.cmdline.LabelValidator.PackageAndTarget;
 import com.google.devtools.build.lib.concurrent.BatchCallback;
 import com.google.devtools.build.lib.server.FailureDetails.TargetPatterns;
+import com.google.devtools.build.lib.server.FailureDetails.TargetPatterns.Code;
 import com.google.devtools.build.lib.supplier.InterruptibleSupplier;
 import com.google.devtools.build.lib.util.StringUtilities;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -63,19 +64,28 @@
   private static final Splitter SLASH_SPLITTER = Splitter.on('/');
   private static final Joiner SLASH_JOINER = Joiner.on('/');
 
-  private static final Parser DEFAULT_PARSER = new Parser(PathFragment.EMPTY_FRAGMENT);
+  private static final Parser DEFAULT_PARSER = mainRepoParser(PathFragment.EMPTY_FRAGMENT);
 
   private final String originalPattern;
-  private final PathFragment offset;
 
   /**
-   * Returns a parser with no offset. Note that the Parser class is immutable, so this method may
-   * return the same instance on subsequent calls.
+   * Returns a parser defaulting to the main repo, with no offset or repo mapping. Note that the
+   * Parser class is immutable, so this method may return the same instance on subsequent calls.
    */
   public static Parser defaultParser() {
     return DEFAULT_PARSER;
   }
 
+  /**
+   * Returns a parser defaulting to the main repo, with repo mapping, but using the given offset.
+   */
+  // NOTE(wyv): This is only strictly correct within a monorepo. If external repos exist, there
+  // should always be a proper repo mapping. We should audit calls to this function and add a repo
+  // mapping wherever appropriate.
+  public static Parser mainRepoParser(PathFragment offset) {
+    return new Parser(offset, RepositoryName.MAIN, RepositoryMapping.ALWAYS_FALLBACK);
+  }
+
   private static String removeSuffix(String s, String suffix) {
     if (s.endsWith(suffix)) {
       return s.substring(0, s.length() - suffix.length());
@@ -119,10 +129,9 @@
     return SLASH_JOINER.join(pieces);
   }
 
-  private TargetPattern(String originalPattern, PathFragment offset) {
+  private TargetPattern(String originalPattern) {
     // Don't allow inheritance outside this class.
     this.originalPattern = Preconditions.checkNotNull(originalPattern);
-    this.offset = Preconditions.checkNotNull(offset);
   }
 
   /**
@@ -138,11 +147,6 @@
     return originalPattern;
   }
 
-  /** Returns the offset this target pattern was parsed with. */
-  public PathFragment getOffset() {
-    return offset;
-  }
-
   /**
    * Evaluates the current target pattern, excluding targets under directories in both {@code
    * ignoredSubdirectories} and {@code excludedSubdirectories}, and returns the result.
@@ -163,7 +167,7 @@
    * Evaluates this {@link TargetPattern} synchronously, feeding the result to the given {@code
    * callback}, and then returns an appropriate immediate {@link ListenableFuture}.
    *
-   * <p>If the returned {@link ListenableFuture}'s {@link ListenableFuture#get} throws an {@link
+   * <p>If the returned {@link ListenableFuture}'s {@link ListenableFuture#get} throws an {@code
    * ExecutionException}, the cause will be an instance of either {@link TargetParsingException} or
    * the given {@code exceptionClass}.
    */
@@ -192,7 +196,7 @@
    * Returns a {@link ListenableFuture} representing the asynchronous evaluation of this {@link
    * TargetPattern} that feeds the results to the given {@code callback}.
    *
-   * <p>If the returned {@link ListenableFuture}'s {@link ListenableFuture#get} throws an {@link
+   * <p>If the returned {@link ListenableFuture}'s {@link ListenableFuture#get} throws an {@code
    * ExecutionException}, the cause will be an instance of either {@link TargetParsingException} or
    * the given {@code exceptionClass}.
    */
@@ -256,12 +260,8 @@
     private final String targetName;
     private final PackageIdentifier directory;
 
-    private SingleTarget(
-        String targetName,
-        PackageIdentifier directory,
-        String originalPattern,
-        PathFragment offset) {
-      super(originalPattern, offset);
+    private SingleTarget(String targetName, PackageIdentifier directory, String originalPattern) {
+      super(originalPattern);
       this.targetName = Preconditions.checkNotNull(targetName);
       this.directory = Preconditions.checkNotNull(directory);
     }
@@ -323,8 +323,8 @@
   private static final class InterpretPathAsTarget extends TargetPattern {
     private final String path;
 
-    private InterpretPathAsTarget(String path, String originalPattern, PathFragment offset) {
-      super(originalPattern, offset);
+    private InterpretPathAsTarget(String path, String originalPattern) {
+      super(originalPattern);
       this.path = normalize(Preconditions.checkNotNull(path));
     }
 
@@ -412,13 +412,12 @@
 
     private TargetsInPackage(
         String originalPattern,
-        PathFragment offset,
         PackageIdentifier packageIdentifier,
         String suffix,
         boolean wasOriginallyAbsolute,
         boolean rulesOnly,
         boolean checkWildcardConflict) {
-      super(originalPattern, offset);
+      super(originalPattern);
       Preconditions.checkArgument(!packageIdentifier.getRepository().isDefault());
       this.packageIdentifier = packageIdentifier;
       this.suffix = Preconditions.checkNotNull(suffix);
@@ -476,16 +475,24 @@
         return false;
       }
       TargetsInPackage that = (TargetsInPackage) o;
-      return wasOriginallyAbsolute == that.wasOriginallyAbsolute && rulesOnly == that.rulesOnly
+      return wasOriginallyAbsolute == that.wasOriginallyAbsolute
+          && rulesOnly == that.rulesOnly
           && checkWildcardConflict == that.checkWildcardConflict
           && getOriginalPattern().equals(that.getOriginalPattern())
-          && packageIdentifier.equals(that.packageIdentifier) && suffix.equals(that.suffix);
+          && packageIdentifier.equals(that.packageIdentifier)
+          && suffix.equals(that.suffix);
     }
 
     @Override
     public int hashCode() {
-      return Objects.hash(getType(), getOriginalPattern(), packageIdentifier, suffix,
-          wasOriginallyAbsolute, rulesOnly, checkWildcardConflict);
+      return Objects.hash(
+          getType(),
+          getOriginalPattern(),
+          packageIdentifier,
+          suffix,
+          wasOriginallyAbsolute,
+          rulesOnly,
+          checkWildcardConflict);
     }
 
     /**
@@ -538,11 +545,8 @@
     private final boolean rulesOnly;
 
     private TargetsBelowDirectory(
-        String originalPattern,
-        PathFragment offset,
-        PackageIdentifier directory,
-        boolean rulesOnly) {
-      super(originalPattern, offset);
+        String originalPattern, PackageIdentifier directory, boolean rulesOnly) {
+      super(originalPattern);
       Preconditions.checkArgument(!directory.getRepository().isDefault());
       this.directory = Preconditions.checkNotNull(directory);
       this.rulesOnly = rulesOnly;
@@ -800,7 +804,8 @@
         return false;
       }
       TargetsBelowDirectory that = (TargetsBelowDirectory) o;
-      return rulesOnly == that.rulesOnly && getOriginalPattern().equals(that.getOriginalPattern())
+      return rulesOnly == that.rulesOnly
+          && getOriginalPattern().equals(that.getOriginalPattern())
           && directory.equals(that.directory);
     }
 
@@ -810,42 +815,6 @@
     }
   }
 
-  /**
-   * Apply a renaming to the repository part of a pattern string, returning the renamed pattern
-   * string. This function only looks at the repository part of the pattern string, not the rest; so
-   * any syntactic errors will not be handled here, but simply remain. Similarly, if the repository
-   * part of the pattern is not syntactically valid, the renaming simply does not match and the
-   * string is returned unchanged.
-   */
-  public static String renameRepository(String pattern, RepositoryMapping renaming) {
-    if (pattern.startsWith("-")) {
-      return "-" + renameRepositoryInternal(pattern.substring(1), renaming);
-    }
-    return renameRepositoryInternal(pattern, renaming);
-  }
-
-  private static String renameRepositoryInternal(String pattern, RepositoryMapping renaming) {
-    if (!pattern.startsWith("@")) {
-      return pattern;
-    }
-    int pkgStart = pattern.indexOf("//");
-    if (pkgStart < 0) {
-      return pattern;
-    }
-    RepositoryName repository;
-    try {
-      repository = RepositoryName.create(pattern.substring(0, pkgStart));
-    } catch (LabelSyntaxException e) {
-      return pattern;
-    }
-    RepositoryName newRepository = renaming.get(repository);
-    if (newRepository == null) {
-      // No renaming required
-      return pattern;
-    }
-    return newRepository.getName() + pattern.substring(pkgStart);
-  }
-
   @Immutable
   public static final class Parser {
     // A valid pattern either starts with exactly 0 slashes (relative pattern) or exactly two
@@ -902,9 +871,19 @@
      */
     private final PathFragment relativeDirectory;
 
+    // The repo to use for any repo-relative target patterns (so "//foo" becomes
+    // "@currentRepo//foo").
+    private final RepositoryName currentRepo;
+
+    // The repo mapping to use for the @repo part of target patterns.
+    private final RepositoryMapping repoMapping;
+
     /** Creates a new parser with the given offset for relative patterns. */
-    public Parser(PathFragment relativeDirectory) {
+    public Parser(
+        PathFragment relativeDirectory, RepositoryName currentRepo, RepositoryMapping repoMapping) {
       this.relativeDirectory = relativeDirectory;
+      this.currentRepo = currentRepo;
+      this.repoMapping = repoMapping;
     }
 
     /**
@@ -919,18 +898,27 @@
 
       String originalPattern = pattern;
       final boolean includesRepo = pattern.startsWith("@");
-      RepositoryName repository = null;
-      if (includesRepo) {
+      RepositoryName repository;
+      if (!includesRepo) {
+        repository = currentRepo;
+      } else {
         int pkgStart = pattern.indexOf("//");
         if (pkgStart < 0) {
           throw new TargetParsingException(
               "Couldn't find package in target " + pattern, TargetPatterns.Code.PACKAGE_NOT_FOUND);
         }
         try {
-          repository = RepositoryName.create(pattern.substring(0, pkgStart));
+          repository = repoMapping.get(RepositoryName.create(pattern.substring(0, pkgStart)));
         } catch (LabelSyntaxException e) {
           throw new TargetParsingException(e.getMessage(), TargetPatterns.Code.LABEL_SYNTAX_ERROR);
         }
+        if (!repository.isVisible()) {
+          throw new TargetParsingException(
+              String.format(
+                  "%s is not visible from %s",
+                  repository.getName(), repository.getOwnerRepoIfNotVisible()),
+              Code.PACKAGE_NOT_FOUND);
+        }
 
         pattern = pattern.substring(pkgStart);
       }
@@ -968,10 +956,6 @@
             TargetPatterns.Code.PACKAGE_PART_CANNOT_END_IN_SLASH);
       }
 
-      if (repository == null) {
-        repository = RepositoryName.MAIN;
-      }
-
       if (packagePart.endsWith("/...")) {
         String realPackagePart = removeSuffix(packagePart, "/...");
         PackageIdentifier packageIdentifier;
@@ -984,11 +968,9 @@
               TargetPatterns.Code.LABEL_SYNTAX_ERROR);
         }
         if (targetPart.isEmpty() || ALL_RULES_IN_SUFFIXES.contains(targetPart)) {
-          return new TargetsBelowDirectory(
-              originalPattern, relativeDirectory, packageIdentifier, true);
+          return new TargetsBelowDirectory(originalPattern, packageIdentifier, true);
         } else if (ALL_TARGETS_IN_SUFFIXES.contains(targetPart)) {
-          return new TargetsBelowDirectory(
-              originalPattern, relativeDirectory, packageIdentifier, false);
+          return new TargetsBelowDirectory(originalPattern, packageIdentifier, false);
         }
       }
 
@@ -1001,8 +983,8 @@
               "Invalid package name '" + packagePart + "': " + e.getMessage(),
               TargetPatterns.Code.LABEL_SYNTAX_ERROR);
         }
-        return new TargetsInPackage(originalPattern, relativeDirectory, packageIdentifier,
-            targetPart, wasOriginallyAbsolute, true, true);
+        return new TargetsInPackage(
+            originalPattern, packageIdentifier, targetPart, wasOriginallyAbsolute, true, true);
       }
 
       if (ALL_TARGETS_IN_SUFFIXES.contains(targetPart)) {
@@ -1014,8 +996,8 @@
               "Invalid package name '" + packagePart + "': " + e.getMessage(),
               TargetPatterns.Code.LABEL_SYNTAX_ERROR);
         }
-        return new TargetsInPackage(originalPattern, relativeDirectory, packageIdentifier,
-            targetPart, wasOriginallyAbsolute, false, true);
+        return new TargetsInPackage(
+            originalPattern, packageIdentifier, targetPart, wasOriginallyAbsolute, false, true);
       }
 
       if (includesRepo || wasOriginallyAbsolute || pattern.contains(":")) {
@@ -1030,7 +1012,7 @@
           String error = "invalid target format '" + originalPattern + "': " + e.getMessage();
           throw new TargetParsingException(error, TargetPatterns.Code.TARGET_FORMAT_INVALID);
         }
-        return new SingleTarget(fullLabel, packageIdentifier, originalPattern, relativeDirectory);
+        return new SingleTarget(fullLabel, packageIdentifier, originalPattern);
       }
 
       // This is a stripped-down version of interpretPathAsTarget that does no I/O.  We have a basic
@@ -1050,7 +1032,7 @@
             "Bad target pattern '" + originalPattern + "': " + e.getMessage(),
             TargetPatterns.Code.LABEL_SYNTAX_ERROR);
       }
-      return new InterpretPathAsTarget(pattern, originalPattern, relativeDirectory);
+      return new InterpretPathAsTarget(pattern, originalPattern);
     }
 
     /**
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Package.java b/src/main/java/com/google/devtools/build/lib/packages/Package.java
index 77b7298..25c5112 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Package.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Package.java
@@ -34,6 +34,7 @@
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.RepositoryMapping;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.collect.CollectionUtils;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
 import com.google.devtools.build.lib.events.Event;
@@ -238,8 +239,8 @@
 
   private ImmutableSet<String> features;
 
-  private ImmutableList<String> registeredExecutionPlatforms;
-  private ImmutableList<String> registeredToolchains;
+  private ImmutableList<TargetPattern> registeredExecutionPlatforms;
+  private ImmutableList<TargetPattern> registeredToolchains;
 
   private long computationSteps;
 
@@ -801,11 +802,11 @@
     return defaultRestrictedTo;
   }
 
-  public ImmutableList<String> getRegisteredExecutionPlatforms() {
+  public ImmutableList<TargetPattern> getRegisteredExecutionPlatforms() {
     return registeredExecutionPlatforms;
   }
 
-  public ImmutableList<String> getRegisteredToolchains() {
+  public ImmutableList<TargetPattern> getRegisteredToolchains() {
     return registeredToolchains;
   }
 
@@ -994,8 +995,8 @@
 
     private ImmutableList<Label> starlarkFileDependencies = ImmutableList.of();
 
-    private final List<String> registeredExecutionPlatforms = new ArrayList<>();
-    private final List<String> registeredToolchains = new ArrayList<>();
+    private final List<TargetPattern> registeredExecutionPlatforms = new ArrayList<>();
+    private final List<TargetPattern> registeredToolchains = new ArrayList<>();
 
     private ThirdPartyLicenseExistencePolicy thirdPartyLicenceExistencePolicy =
         ThirdPartyLicenseExistencePolicy.USER_CONTROLLABLE;
@@ -1730,11 +1731,11 @@
       ruleLabels.put(rule, labels);
     }
 
-    void addRegisteredExecutionPlatforms(List<String> platforms) {
+    void addRegisteredExecutionPlatforms(List<TargetPattern> platforms) {
       this.registeredExecutionPlatforms.addAll(platforms);
     }
 
-    void addRegisteredToolchains(List<String> toolchains) {
+    void addRegisteredToolchains(List<TargetPattern> toolchains) {
       this.registeredToolchains.addAll(toolchains);
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactoryHelper.java b/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactoryHelper.java
index 06c1180..6e27b10 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactoryHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/WorkspaceFactoryHelper.java
@@ -23,6 +23,8 @@
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.events.StoredEventHandler;
 import com.google.devtools.build.lib.packages.RuleFactory.BuildLangTypedAttributeValuesMap;
 import java.util.Map;
@@ -56,7 +58,18 @@
       Label nameLabel = Label.parseAbsolute("//external:" + entry.getKey(), ImmutableMap.of());
       addBindRule(pkg, bindRuleClass, nameLabel, entry.getValue(), semantics, callstack);
     }
-    pkg.addRegisteredToolchains(ruleClass.getToolchainsToRegisterFunction().apply(rule));
+    // NOTE(wyv): What is this madness?? This is the only instance where a repository rule can
+    // register toolchains upon being instantiated. We should look into converting
+    // android_{s,n}dk_repository into module extensions.
+    ImmutableList.Builder<TargetPattern> toolchains = new ImmutableList.Builder<>();
+    for (String pattern : ruleClass.getToolchainsToRegisterFunction().apply(rule)) {
+      try {
+        toolchains.add(TargetPattern.defaultParser().parse(pattern));
+      } catch (TargetParsingException e) {
+        throw new LabelSyntaxException(e.getMessage());
+      }
+    }
+    pkg.addRegisteredToolchains(toolchains.build());
     return rule;
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/packages/WorkspaceGlobals.java b/src/main/java/com/google/devtools/build/lib/packages/WorkspaceGlobals.java
index dc4eb35..a531eb2 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/WorkspaceGlobals.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/WorkspaceGlobals.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.cmdline.LabelValidator;
 import com.google.devtools.build.lib.cmdline.RepositoryMapping;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
 import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.packages.Package.NameConflictException;
 import com.google.devtools.build.lib.packages.RuleFactory.InvalidRuleException;
@@ -232,15 +233,23 @@
     return label.getRepository();
   }
 
-  private static ImmutableList<String> renamePatterns(
-      List<String> patterns, Package.Builder builder, StarlarkThread thread) {
+  private static ImmutableList<TargetPattern> parsePatterns(
+      List<String> patterns, Package.Builder builder, StarlarkThread thread) throws EvalException {
     BazelModuleContext bzlModule =
         BazelModuleContext.of(Module.ofInnermostEnclosingStarlarkFunction(thread));
     RepositoryName myName = getRepositoryName((bzlModule != null ? bzlModule.label() : null));
     RepositoryMapping renaming = builder.getRepositoryMappingFor(myName);
-    return patterns.stream()
-        .map(patternEntry -> TargetPattern.renameRepository(patternEntry, renaming))
-        .collect(ImmutableList.toImmutableList());
+    TargetPattern.Parser parser =
+        new TargetPattern.Parser(PathFragment.EMPTY_FRAGMENT, myName, renaming);
+    ImmutableList.Builder<TargetPattern> parsedPatterns = ImmutableList.builder();
+    for (String pattern : patterns) {
+      try {
+        parsedPatterns.add(parser.parse(pattern));
+      } catch (TargetParsingException e) {
+        throw Starlark.errorf("error parsing target pattern \"%s\": %s", pattern, e.getMessage());
+      }
+    }
+    return parsedPatterns.build();
   }
 
   @Override
@@ -249,7 +258,7 @@
     // Add to the package definition for later.
     Package.Builder builder = PackageFactory.getContext(thread).pkgBuilder;
     List<String> patterns = Sequence.cast(platformLabels, String.class, "platform_labels");
-    builder.addRegisteredExecutionPlatforms(renamePatterns(patterns, builder, thread));
+    builder.addRegisteredExecutionPlatforms(parsePatterns(patterns, builder, thread));
   }
 
   @Override
@@ -258,7 +267,7 @@
     // Add to the package definition for later.
     Package.Builder builder = PackageFactory.getContext(thread).pkgBuilder;
     List<String> patterns = Sequence.cast(toolchainLabels, String.class, "toolchain_labels");
-    builder.addRegisteredToolchains(renamePatterns(patterns, builder, thread));
+    builder.addRegisteredToolchains(parsePatterns(patterns, builder, thread));
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicies.java b/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicies.java
index 327951c..3f21242 100644
--- a/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicies.java
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicies.java
@@ -41,15 +41,15 @@
     return new AndFilteringPolicy(x, y);
   }
 
-  public static FilteringPolicy ruleType(String ruleName, boolean keepExplicit) {
-    return RuleTypeFilter.create(ruleName, keepExplicit);
+  public static FilteringPolicy ruleTypeExplicit(String ruleName) {
+    return RuleTypeFilter.create(ruleName, /*keepExplicit=*/ true);
   }
 
   private FilteringPolicies() {
   }
 
   /** Base class for singleton filtering policies. */
-  private abstract static class AbstractFilteringPolicy extends FilteringPolicy {
+  private abstract static class AbstractFilteringPolicy implements FilteringPolicy {
     private final int hashCode = getClass().getSimpleName().hashCode();
 
     @Override
@@ -109,7 +109,7 @@
   /** FilteringPolicy that only matches a specific rule name. */
   @AutoValue
   @AutoCodec
-  abstract static class RuleTypeFilter extends FilteringPolicy {
+  abstract static class RuleTypeFilter implements FilteringPolicy {
     abstract String ruleName();
 
     abstract boolean keepExplicit();
@@ -134,7 +134,7 @@
   }
 
   /** FilteringPolicy for combining FilteringPolicies. */
-  public static class AndFilteringPolicy extends FilteringPolicy {
+  public static class AndFilteringPolicy implements FilteringPolicy {
     private final FilteringPolicy firstPolicy;
     private final FilteringPolicy secondPolicy;
 
diff --git a/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicy.java b/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicy.java
index 3790fbc..edb95b6 100644
--- a/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicy.java
+++ b/src/main/java/com/google/devtools/build/lib/pkgcache/FilteringPolicy.java
@@ -21,7 +21,7 @@
  * A filtering policy defines how target patterns are matched. For instance, we may wish to select
  * only tests or no tests.
  */
-public abstract class FilteringPolicy implements Serializable {
+public interface FilteringPolicy extends Serializable {
 
   /**
    * Returns true if this target should be retained.
@@ -30,5 +30,5 @@
    *     a wildcard.
    */
   @ThreadSafety.ThreadSafe
-  public abstract boolean shouldRetain(Target target, boolean explicit);
+  boolean shouldRetain(Target target, boolean explicit);
 }
diff --git a/src/main/java/com/google/devtools/build/lib/query2/PostAnalysisQueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/PostAnalysisQueryEnvironment.java
index fa5fd78..505298c 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/PostAnalysisQueryEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/PostAnalysisQueryEnvironment.java
@@ -69,8 +69,6 @@
 import com.google.devtools.build.lib.skyframe.SimplePackageIdentifierBatchingCallback;
 import com.google.devtools.build.lib.skyframe.SkyFunctions;
 import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
-import com.google.devtools.build.lib.skyframe.TargetPatternValue;
-import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternKey;
 import com.google.devtools.build.lib.supplier.InterruptibleSupplier;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.SkyKey;
@@ -249,10 +247,7 @@
   protected abstract T getValueFromKey(SkyKey key) throws InterruptedException;
 
   protected TargetPattern getPattern(String pattern) throws TargetParsingException {
-    TargetPatternKey targetPatternKey =
-        ((TargetPatternKey)
-            TargetPatternValue.key(pattern, FilteringPolicies.NO_FILTER, parserPrefix).argument());
-    return targetPatternKey.getParsedPattern();
+    return TargetPattern.mainRepoParser(parserPrefix).parse(pattern);
   }
 
   public ThreadSafeMutableSet<T> getFwdDeps(Iterable<T> targets) throws InterruptedException {
diff --git a/src/main/java/com/google/devtools/build/lib/query2/SkyQueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/SkyQueryEnvironment.java
index 677f485..c9247a8 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/SkyQueryEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/SkyQueryEnvironment.java
@@ -40,6 +40,7 @@
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.cmdline.SignedTargetPattern;
 import com.google.devtools.build.lib.cmdline.TargetParsingException;
 import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.cmdline.TargetPatternResolver;
@@ -382,7 +383,7 @@
     if (constantUniverseScopeList.size() != 1) {
       return QueryExpressionMapper.identity();
     }
-    TargetPattern.Parser targetPatternParser = new TargetPattern.Parser(parserPrefix);
+    TargetPattern.Parser targetPatternParser = TargetPattern.mainRepoParser(parserPrefix);
     String universeScopePatternString = Iterables.getOnlyElement(constantUniverseScopeList);
     TargetPattern absoluteUniverseScopePattern = null;
     try {
@@ -805,7 +806,10 @@
       QueryExpression owner, String pattern, Callback<Target> callback) {
     TargetPatternKey targetPatternKey;
     try {
-      targetPatternKey = TargetPatternValue.key(pattern, FilteringPolicies.NO_FILTER, parserPrefix);
+      targetPatternKey =
+          TargetPatternValue.key(
+              SignedTargetPattern.parse(pattern, TargetPattern.mainRepoParser(parserPrefix)),
+              FilteringPolicies.NO_FILTER);
     } catch (TargetParsingException tpe) {
       try {
         handleError(owner, tpe.getMessage(), tpe.getDetailedExitCode());
diff --git a/src/main/java/com/google/devtools/build/lib/rules/genquery/GenQuery.java b/src/main/java/com/google/devtools/build/lib/rules/genquery/GenQuery.java
index fc0dff4..fd52445 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/genquery/GenQuery.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/genquery/GenQuery.java
@@ -44,6 +44,7 @@
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.SignedTargetPattern;
 import com.google.devtools.build.lib.cmdline.TargetParsingException;
 import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.collect.compacthashset.CompactHashSet;
@@ -490,7 +491,8 @@
         checkValidPatternType(pattern);
         patternKeys.put(
             TargetPatternValue.key(
-                pattern, FilteringPolicies.NO_FILTER, PathFragment.EMPTY_FRAGMENT),
+                SignedTargetPattern.parse(pattern, TargetPattern.defaultParser()),
+                FilteringPolicies.NO_FILTER),
             pattern);
       }
       Set<SkyKey> packageKeys = new HashSet<>();
@@ -553,8 +555,7 @@
     }
 
     private void checkValidPatternType(String pattern) throws TargetParsingException {
-      TargetPattern.Type type =
-          new TargetPattern.Parser(PathFragment.EMPTY_FRAGMENT).parse(pattern).getType();
+      TargetPattern.Type type = TargetPattern.defaultParser().parse(pattern).getType();
       if (type == TargetPattern.Type.PATH_AS_TARGET) {
         throw new TargetParsingException(
             String.format("couldn't determine target from filename '%s'", pattern),
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternValue.java
index 7bb24a88..20f77b8 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternValue.java
@@ -15,6 +15,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Interner;
+import com.google.devtools.build.lib.cmdline.SignedTargetPattern;
 import com.google.devtools.build.lib.cmdline.TargetParsingException;
 import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.cmdline.TargetPattern.Type;
@@ -23,12 +24,10 @@
 import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
 import com.google.devtools.build.lib.server.FailureDetails.TargetPatterns;
 import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternKey;
-import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternSkyKeyOrException;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.AbstractSkyKey;
 import com.google.devtools.build.skyframe.SkyFunctionName;
-import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.build.skyframe.SkyValue;
 import java.util.List;
 
@@ -92,16 +91,15 @@
         ImmutableList.builder();
     ImmutableList.Builder<PrepareDepsOfPatternSkyKeyException> resultExceptionsBuilder =
         ImmutableList.builder();
-    Iterable<TargetPatternSkyKeyOrException> keysMaybe =
-        TargetPatternValue.keys(patterns, FilteringPolicies.NO_FILTER, offset);
+    TargetPattern.Parser parser = TargetPattern.mainRepoParser(offset);
     ImmutableList.Builder<TargetPatternKey> targetPatternKeysBuilder = ImmutableList.builder();
-    for (TargetPatternSkyKeyOrException keyMaybe : keysMaybe) {
+    for (String pattern : patterns) {
       try {
-        SkyKey key = keyMaybe.getSkyKey();
-        targetPatternKeysBuilder.add((TargetPatternKey) key.argument());
+        targetPatternKeysBuilder.add(
+            TargetPatternValue.key(
+                SignedTargetPattern.parse(pattern, parser), FilteringPolicies.NO_FILTER));
       } catch (TargetParsingException e) {
-        resultExceptionsBuilder.add(
-            new PrepareDepsOfPatternSkyKeyException(e, keyMaybe.getOriginalPattern()));
+        resultExceptionsBuilder.add(new PrepareDepsOfPatternSkyKeyException(e, pattern));
       }
     }
     // This code path is evaluated only for query universe preloading, and the quadratic cost of
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternsFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternsFunction.java
index 8a94bce..9d40971 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternsFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrepareDepsOfPatternsFunction.java
@@ -87,6 +87,7 @@
   @Override
   public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException {
     ExtendedEventHandler eventHandler = env.getListener();
+    // TODO(wyv): use the repo mapping of the main repo here.
     ImmutableList<SkyKey> skyKeys = getSkyKeys(skyKey, eventHandler);
 
     Map<SkyKey, ValueOrException<TargetParsingException>> tokensByKey =
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RegisteredExecutionPlatformsFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RegisteredExecutionPlatformsFunction.java
index fb5cca9..107620b 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RegisteredExecutionPlatformsFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RegisteredExecutionPlatformsFunction.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 
-import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
@@ -28,7 +27,10 @@
 import com.google.devtools.build.lib.bazel.bzlmod.ExternalDepsException;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.LabelConstants;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.cmdline.SignedTargetPattern;
 import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.packages.Package;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.pkgcache.FilteringPolicy;
@@ -37,8 +39,10 @@
 import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
 import com.google.devtools.build.lib.server.FailureDetails.Toolchain;
 import com.google.devtools.build.lib.skyframe.PlatformLookupUtil.InvalidPlatformException;
+import com.google.devtools.build.lib.skyframe.TargetPatternUtil.InvalidTargetPatternException;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.util.DetailedExitCode;
+import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.SkyFunction;
 import com.google.devtools.build.skyframe.SkyFunctionException;
 import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
@@ -52,6 +56,10 @@
 /** {@link SkyFunction} that returns all registered execution platforms available. */
 public class RegisteredExecutionPlatformsFunction implements SkyFunction {
 
+  @AutoCodec
+  static final FilteringPolicy HAS_PLATFORM_INFO =
+      (Target target, boolean explicit) -> explicit || PlatformLookupUtil.hasPlatformInfo(target);
+
   @Nullable
   @Override
   public SkyValue compute(SkyKey skyKey, Environment env)
@@ -60,34 +68,47 @@
     BuildConfigurationValue buildConfigurationValue =
         (BuildConfigurationValue)
             env.getValue(((RegisteredExecutionPlatformsValue.Key) skyKey).getConfigurationKey());
+    RepositoryMappingValue mainRepoMapping =
+        (RepositoryMappingValue) env.getValue(RepositoryMappingValue.key(RepositoryName.MAIN));
     if (env.valuesMissing()) {
       return null;
     }
     BuildConfiguration configuration = buildConfigurationValue.getConfiguration();
 
-    ImmutableList.Builder<String> targetPatternBuilder = new ImmutableList.Builder<>();
+    TargetPattern.Parser mainRepoParser =
+        new TargetPattern.Parser(
+            PathFragment.EMPTY_FRAGMENT,
+            RepositoryName.MAIN,
+            mainRepoMapping.getRepositoryMapping());
+    ImmutableList.Builder<SignedTargetPattern> targetPatternBuilder = new ImmutableList.Builder<>();
 
     // Get the execution platforms from the configuration.
     PlatformConfiguration platformConfiguration =
         configuration.getFragment(PlatformConfiguration.class);
     if (platformConfiguration != null) {
-      targetPatternBuilder.addAll(platformConfiguration.getExtraExecutionPlatforms());
+      try {
+        targetPatternBuilder.addAll(
+            TargetPatternUtil.parseAllSigned(
+                platformConfiguration.getExtraExecutionPlatforms(), mainRepoParser));
+      } catch (InvalidTargetPatternException e) {
+        throw new RegisteredExecutionPlatformsFunctionException(
+            new InvalidExecutionPlatformLabelException(e), Transience.PERSISTENT);
+      }
     }
 
     // Get the registered execution platforms from the WORKSPACE.
-    List<String> workspaceExecutionPlatforms = getWorkspaceExecutionPlatforms(env);
+    ImmutableList<TargetPattern> workspaceExecutionPlatforms = getWorkspaceExecutionPlatforms(env);
     if (workspaceExecutionPlatforms == null) {
       return null;
     }
-    targetPatternBuilder.addAll(workspaceExecutionPlatforms);
-
-    ImmutableList<String> targetPatterns = targetPatternBuilder.build();
+    targetPatternBuilder.addAll(TargetPatternUtil.toSigned(workspaceExecutionPlatforms));
 
     // Expand target patterns.
     ImmutableList<Label> platformLabels;
     try {
       platformLabels =
-          TargetPatternUtil.expandTargetPatterns(env, targetPatterns, HasPlatformInfo.create());
+          TargetPatternUtil.expandTargetPatterns(
+              env, targetPatternBuilder.build(), HAS_PLATFORM_INFO);
       if (env.valuesMissing()) {
         return null;
       }
@@ -113,7 +134,7 @@
    */
   @Nullable
   @VisibleForTesting
-  public static ImmutableList<String> getWorkspaceExecutionPlatforms(Environment env)
+  public static ImmutableList<TargetPattern> getWorkspaceExecutionPlatforms(Environment env)
       throws InterruptedException {
     PackageValue externalPackageValue =
         (PackageValue) env.getValue(PackageValue.key(LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER));
@@ -231,24 +252,4 @@
       super(cause, transience);
     }
   }
-
-  // This class uses AutoValue solely to get default equals/hashCode behavior, which is needed to
-  // make skyframe serialization work properly.
-  @AutoValue
-  @AutoCodec
-  abstract static class HasPlatformInfo extends FilteringPolicy {
-
-    @Override
-    public boolean shouldRetain(Target target, boolean explicit) {
-      if (explicit) {
-        return true;
-      }
-      return PlatformLookupUtil.hasPlatformInfo(target);
-    }
-
-    @AutoCodec.Instantiator
-    static HasPlatformInfo create() {
-      return new AutoValue_RegisteredExecutionPlatformsFunction_HasPlatformInfo();
-    }
-  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RegisteredToolchainsFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RegisteredToolchainsFunction.java
index 0b7400c..bd33592 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RegisteredToolchainsFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RegisteredToolchainsFunction.java
@@ -27,10 +27,15 @@
 import com.google.devtools.build.lib.bazel.bzlmod.ExternalDepsException;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.LabelConstants;
+import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.cmdline.SignedTargetPattern;
 import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.packages.Package;
 import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
 import com.google.devtools.build.lib.server.FailureDetails.Toolchain.Code;
+import com.google.devtools.build.lib.skyframe.TargetPatternUtil.InvalidTargetPatternException;
+import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.SkyFunction;
 import com.google.devtools.build.skyframe.SkyFunctionException;
 import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
@@ -54,33 +59,45 @@
     BuildConfigurationValue buildConfigurationValue =
         (BuildConfigurationValue)
             env.getValue(((RegisteredToolchainsValue.Key) skyKey).getConfigurationKey());
+    RepositoryMappingValue mainRepoMapping =
+        (RepositoryMappingValue) env.getValue(RepositoryMappingValue.key(RepositoryName.MAIN));
     if (env.valuesMissing()) {
       return null;
     }
     BuildConfiguration configuration = buildConfigurationValue.getConfiguration();
 
-    ImmutableList.Builder<String> targetPatternBuilder = new ImmutableList.Builder<>();
+    TargetPattern.Parser mainRepoParser =
+        new TargetPattern.Parser(
+            PathFragment.EMPTY_FRAGMENT,
+            RepositoryName.MAIN,
+            mainRepoMapping.getRepositoryMapping());
+    ImmutableList.Builder<SignedTargetPattern> targetPatternBuilder = new ImmutableList.Builder<>();
 
     // Get the toolchains from the configuration.
     PlatformConfiguration platformConfiguration =
         configuration.getFragment(PlatformConfiguration.class);
-    targetPatternBuilder.addAll(platformConfiguration.getExtraToolchains());
+    try {
+      targetPatternBuilder.addAll(
+          TargetPatternUtil.parseAllSigned(
+              platformConfiguration.getExtraToolchains(), mainRepoParser));
+    } catch (InvalidTargetPatternException e) {
+      throw new RegisteredToolchainsFunctionException(
+          new InvalidToolchainLabelException(e), Transience.PERSISTENT);
+    }
 
     // Get the registered toolchains from the WORKSPACE.
-    ImmutableList<String> workspaceToolchains = getWorkspaceToolchains(env);
+    ImmutableList<TargetPattern> workspaceToolchains = getWorkspaceToolchains(env);
     if (workspaceToolchains == null) {
       return null;
     }
-    targetPatternBuilder.addAll(workspaceToolchains);
-
-    ImmutableList<String> targetPatterns = targetPatternBuilder.build();
+    targetPatternBuilder.addAll(TargetPatternUtil.toSigned(workspaceToolchains));
 
     // Expand target patterns.
     ImmutableList<Label> toolchainLabels;
     try {
       toolchainLabels =
           TargetPatternUtil.expandTargetPatterns(
-              env, targetPatterns, FilteringPolicies.ruleType("toolchain", true));
+              env, targetPatternBuilder.build(), FilteringPolicies.ruleTypeExplicit("toolchain"));
       if (env.valuesMissing()) {
         return null;
       }
@@ -106,7 +123,7 @@
    */
   @Nullable
   @VisibleForTesting
-  public static ImmutableList<String> getWorkspaceToolchains(Environment env)
+  public static ImmutableList<TargetPattern> getWorkspaceToolchains(Environment env)
       throws InterruptedException {
     PackageValue externalPackageValue =
         (PackageValue) env.getValue(PackageValue.key(LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER));
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java
index fe79140..081dfd9 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.SignedTargetPattern;
 import com.google.devtools.build.lib.cmdline.TargetParsingException;
 import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.events.Event;
@@ -146,7 +147,9 @@
       throws TargetParsingException {
     try {
       TargetPatternKey key =
-          TargetPatternValue.key(targetPattern, FilteringPolicies.NO_FILTER, offset);
+          TargetPatternValue.key(
+              SignedTargetPattern.parse(targetPattern, TargetPattern.mainRepoParser(offset)),
+              FilteringPolicies.NO_FILTER);
       return isSimple(key.getParsedPattern())
           ? new SimpleLookup(targetPattern, key)
           : new NormalLookup(targetPattern, key);
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetExcludingFilteringPolicy.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetExcludingFilteringPolicy.java
index 4069ea6..ef23fd2 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TargetExcludingFilteringPolicy.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetExcludingFilteringPolicy.java
@@ -17,12 +17,13 @@
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.pkgcache.FilteringPolicy;
+import java.util.Objects;
 
 /**
  * A filtering policy that excludes multiple single targets. These are not expected to be a part of
  * any SkyKey and it's expected that the number of targets is not too large.
  */
-class TargetExcludingFilteringPolicy extends FilteringPolicy {
+class TargetExcludingFilteringPolicy implements FilteringPolicy {
   private final ImmutableSet<Label> excludedSingleTargets;
 
   TargetExcludingFilteringPolicy(ImmutableSet<Label> excludedSingleTargets) {
@@ -38,4 +39,21 @@
   public String toString() {
     return String.format("excludedTargets%s", excludedSingleTargets);
   }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof TargetExcludingFilteringPolicy)) {
+      return false;
+    }
+    TargetExcludingFilteringPolicy that = (TargetExcludingFilteringPolicy) o;
+    return Objects.equals(excludedSingleTargets, that.excludedSingleTargets);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(excludedSingleTargets);
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseFunction.java
index 54d1833..8e8722f 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseFunction.java
@@ -29,6 +29,7 @@
 import com.google.devtools.build.lib.cmdline.RepositoryMapping;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.SignedTargetPattern;
 import com.google.devtools.build.lib.cmdline.TargetParsingException;
 import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.events.Event;
@@ -41,6 +42,7 @@
 import com.google.devtools.build.lib.pkgcache.AbstractRecursivePackageProvider.MissingDepException;
 import com.google.devtools.build.lib.pkgcache.CompileOneDependencyTransformer;
 import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicy;
 import com.google.devtools.build.lib.pkgcache.LoadingPhaseCompleteEvent;
 import com.google.devtools.build.lib.pkgcache.ParsingFailedEvent;
 import com.google.devtools.build.lib.pkgcache.TargetParsingCompleteEvent;
@@ -48,7 +50,6 @@
 import com.google.devtools.build.lib.repository.ExternalPackageHelper;
 import com.google.devtools.build.lib.skyframe.TargetPatternPhaseValue.TargetPatternPhaseKey;
 import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternKey;
-import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternSkyKeyOrException;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.SkyFunction;
 import com.google.devtools.build.skyframe.SkyKey;
@@ -107,8 +108,13 @@
     // set as build list as well.
     ResolvedTargets<Target> testTargets = null;
     if (options.getDetermineTests() || options.getBuildTestsOnly()) {
-      testTargets = determineTests(env,
-          options.getTargetPatterns(), options.getOffset(), options.getTestFilter());
+      testTargets =
+          determineTests(
+              env,
+              options.getTargetPatterns(),
+              options.getOffset(),
+              repositoryMappingValue.getRepositoryMapping(),
+              options.getTestFilter());
       Preconditions.checkState(env.valuesMissing() || (testTargets != null));
     }
 
@@ -274,43 +280,34 @@
       RepositoryMapping repoMapping,
       List<String> failedPatterns)
       throws InterruptedException {
-
-    ImmutableList.Builder<String> canonicalPatterns = new ImmutableList.Builder<>();
-    for (String rawPattern : options.getTargetPatterns()) {
-      canonicalPatterns.add(TargetPattern.renameRepository(rawPattern, repoMapping));
-    }
-
+    TargetPattern.Parser parser =
+        new TargetPattern.Parser(options.getOffset(), RepositoryName.MAIN, repoMapping);
+    FilteringPolicy policy =
+        options.getBuildManualTests()
+            ? FilteringPolicies.NO_FILTER
+            : FilteringPolicies.FILTER_MANUAL;
     List<TargetPatternKey> patternSkyKeys = new ArrayList<>(options.getTargetPatterns().size());
-    for (TargetPatternSkyKeyOrException keyOrException :
-        TargetPatternValue.keys(
-            canonicalPatterns.build(),
-            options.getBuildManualTests()
-                ? FilteringPolicies.NO_FILTER
-                : FilteringPolicies.FILTER_MANUAL,
-            options.getOffset())) {
+    for (String pattern : options.getTargetPatterns()) {
       try {
-        patternSkyKeys.add(keyOrException.getSkyKey());
+        patternSkyKeys.add(
+            TargetPatternValue.key(SignedTargetPattern.parse(pattern, parser), policy));
       } catch (TargetParsingException e) {
-        failedPatterns.add(keyOrException.getOriginalPattern());
+        failedPatterns.add(pattern);
         // We post a PatternExpandingError here - the pattern could not be parsed, so we don't even
         // get to run TargetPatternFunction.
-        env.getListener().post(
-            PatternExpandingError.failed(keyOrException.getOriginalPattern(), e.getMessage()));
+        env.getListener().post(PatternExpandingError.failed(pattern, e.getMessage()));
         // We generally skip patterns that don't parse. We report a parsing failed exception to the
         // event bus here, but not in determineTests below, which goes through the same list. Note
         // that the TargetPatternFunction otherwise reports these events (but only if the target
         // pattern could be parsed successfully).
-        env.getListener().post(
-            new ParsingFailedEvent(keyOrException.getOriginalPattern(), e.getMessage()));
+        env.getListener().post(new ParsingFailedEvent(pattern, e.getMessage()));
         try {
           env.getValueOrThrow(
               TargetPatternErrorFunction.key(e.getMessage()), TargetParsingException.class);
         } catch (TargetParsingException ignore) {
           // We ignore this. Keep going is active.
         }
-        env.getListener().handle(
-            Event.error(
-                "Skipping '" + keyOrException.getOriginalPattern() + "': " + e.getMessage()));
+        env.getListener().handle(Event.error("Skipping '" + pattern + "': " + e.getMessage()));
       }
     }
 
@@ -398,16 +395,24 @@
    * of targets, handling the filter flags, and expanding test suites.
    *
    * @param targetPatterns the list of command-line target patterns specified by the user
+   * @param repoMapping the repository mapping to apply to repos in the patterns
    * @param testFilter the test filter
    */
   private static ResolvedTargets<Target> determineTests(
-      Environment env, List<String> targetPatterns, PathFragment offset, TestFilter testFilter)
+      Environment env,
+      List<String> targetPatterns,
+      PathFragment offset,
+      RepositoryMapping repoMapping,
+      TestFilter testFilter)
       throws InterruptedException {
+    TargetPattern.Parser parser =
+        new TargetPattern.Parser(offset, RepositoryName.MAIN, repoMapping);
     List<TargetPatternKey> patternSkyKeys = new ArrayList<>();
-    for (TargetPatternSkyKeyOrException keyOrException :
-        TargetPatternValue.keys(targetPatterns, FilteringPolicies.FILTER_TESTS, offset)) {
+    for (String pattern : targetPatterns) {
       try {
-        patternSkyKeys.add(keyOrException.getSkyKey());
+        patternSkyKeys.add(
+            TargetPatternValue.key(
+                SignedTargetPattern.parse(pattern, parser), FilteringPolicies.FILTER_TESTS));
       } catch (TargetParsingException e) {
         // Skip.
       }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternUtil.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternUtil.java
index fd5e61c..411d24e 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternUtil.java
@@ -14,70 +14,48 @@
 
 package com.google.devtools.build.lib.skyframe;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.cmdline.SignedTargetPattern;
+import com.google.devtools.build.lib.cmdline.SignedTargetPattern.Sign;
 import com.google.devtools.build.lib.cmdline.TargetParsingException;
-import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
+import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.pkgcache.FilteringPolicy;
-import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternKey;
 import com.google.devtools.build.skyframe.SkyFunction.Environment;
 import com.google.devtools.build.skyframe.SkyKey;
 import com.google.devtools.build.skyframe.ValueOrException;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
 
-/**
- * Utility class to help with evaluating target patterns.
- */
+/** Utility class to help with evaluating target patterns. */
 public class TargetPatternUtil {
 
   /**
-   * Expand the given {@code targetPatterns}. This handles the needed underlying Skyframe calls (via
-   * {@code env}), and will return {@code null} to signal a Skyframe restart.
-   */
-  @Nullable
-  public static ImmutableList<Label> expandTargetPatterns(
-      Environment env, List<String> targetPatterns)
-      throws InvalidTargetPatternException, InterruptedException {
-
-    return expandTargetPatterns(env, targetPatterns, FilteringPolicies.NO_FILTER);
-  }
-
-  /**
    * Expand the given {@code targetPatterns}, using the {@code filteringPolicy}. This handles the
    * needed underlying Skyframe calls (via {@code env}), and will return {@code null} to signal a
    * Skyframe restart.
    */
   @Nullable
   public static ImmutableList<Label> expandTargetPatterns(
-      Environment env, List<String> targetPatterns, FilteringPolicy filteringPolicy)
+      Environment env, List<SignedTargetPattern> targetPatterns, FilteringPolicy filteringPolicy)
       throws InvalidTargetPatternException, InterruptedException {
 
     if (targetPatterns.isEmpty()) {
       return ImmutableList.of();
     }
 
-    // First parse the patterns, and throw any errors immediately.
-    List<TargetPatternValue.TargetPatternKey> patternKeys = new ArrayList<>();
-    for (TargetPatternValue.TargetPatternSkyKeyOrException keyOrException :
-        TargetPatternValue.keys(targetPatterns, filteringPolicy, PathFragment.EMPTY_FRAGMENT)) {
-
-      try {
-        patternKeys.add(keyOrException.getSkyKey());
-      } catch (TargetParsingException e) {
-        throw new InvalidTargetPatternException(keyOrException.getOriginalPattern(), e);
-      }
-    }
-
-    // Then, resolve the patterns.
+    Iterable<TargetPatternKey> targetPatternKeys =
+        TargetPatternValue.keys(targetPatterns, filteringPolicy);
     Map<SkyKey, ValueOrException<TargetParsingException>> resolvedPatterns =
-        env.getValuesOrThrow(patternKeys, TargetParsingException.class);
+        env.getValuesOrThrow(targetPatternKeys, TargetParsingException.class);
     boolean valuesMissing = env.valuesMissing();
     ImmutableList.Builder<Label> labels = valuesMissing ? null : new ImmutableList.Builder<>();
 
-    for (TargetPatternValue.TargetPatternKey pattern : patternKeys) {
+    for (TargetPatternKey pattern : targetPatternKeys) {
       TargetPatternValue value;
       try {
         value = (TargetPatternValue) resolvedPatterns.get(pattern).get();
@@ -96,8 +74,31 @@
     return labels.build();
   }
 
+  // TODO(bazel-team): look into moving this into SignedTargetPattern itself.
+  public static ImmutableList<SignedTargetPattern> parseAllSigned(
+      List<String> patterns, TargetPattern.Parser parser) throws InvalidTargetPatternException {
+    ImmutableList.Builder<SignedTargetPattern> parsedPatterns = ImmutableList.builder();
+    for (String pattern : patterns) {
+      try {
+        parsedPatterns.add(SignedTargetPattern.parse(pattern, parser));
+      } catch (TargetParsingException e) {
+        throw new InvalidTargetPatternException(pattern, e);
+      }
+    }
+    return parsedPatterns.build();
+  }
+
+  /** Converts patterns to signed patterns, considering all input patterns positive. */
+  public static ImmutableList<SignedTargetPattern> toSigned(List<TargetPattern> patterns) {
+    return patterns.stream()
+        .map(pattern -> SignedTargetPattern.create(pattern, Sign.POSITIVE))
+        .collect(toImmutableList());
+  }
+
   /** Exception used when an error occurs in {@link #expandTargetPatterns}. */
-  static final class InvalidTargetPatternException extends Exception {
+  // TODO(bazel-team): Consolidate this and TargetParsingException. Just have the latter store the
+  //   original unparsed pattern too.
+  public static final class InvalidTargetPatternException extends Exception {
     private String invalidPattern;
     private TargetParsingException tpe;
 
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternValue.java
index fecdf3d..5fecef1 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternValue.java
@@ -13,15 +13,17 @@
 // limitations under the License.
 package com.google.devtools.build.lib.skyframe;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
 import com.google.devtools.build.lib.cmdline.ResolvedTargets;
-import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.cmdline.SignedTargetPattern;
+import com.google.devtools.build.lib.cmdline.SignedTargetPattern.Sign;
 import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.cmdline.TargetPattern.TargetsBelowDirectory;
 import com.google.devtools.build.lib.cmdline.TargetPattern.TargetsBelowDirectory.ContainsResult;
@@ -102,55 +104,28 @@
   }
 
   /**
-   * Create a target pattern {@link SkyKey}. Throws {@link TargetParsingException} if the provided
-   * {@code pattern} cannot be parsed.
+   * Create a target pattern {@link SkyKey}.
    *
-   * @param pattern The pattern, eg "-foo/biz...". If the first character is "-", the pattern is
-   *     treated as a negative pattern.
+   * @param pattern The pattern, eg "-foo/biz...".
    * @param policy The filtering policy, eg "only return test targets"
-   * @param offset The offset to apply to relative target patterns.
    */
   @ThreadSafe
-  public static TargetPatternKey key(String pattern, FilteringPolicy policy, PathFragment offset)
-      throws TargetParsingException {
-    return Iterables.getOnlyElement(keys(ImmutableList.of(pattern), policy, offset)).getSkyKey();
+  public static TargetPatternKey key(SignedTargetPattern pattern, FilteringPolicy policy) {
+    return new TargetPatternKey(
+        pattern, pattern.sign() == Sign.POSITIVE ? policy : FilteringPolicies.NO_FILTER);
   }
 
   /**
-   * Returns an iterable of {@link TargetPatternSkyKeyOrException}, with {@link TargetPatternKey}
-   * arguments, in the same order as the list of patterns provided as input. If a provided pattern
-   * fails to parse, the element in the returned iterable corresponding to it will throw when its
-   * {@link TargetPatternSkyKeyOrException#getSkyKey} method is called.
+   * Returns an iterable of {@link TargetPatternKey}s, in the same order as the list of patterns
+   * provided as input.
    *
-   * @param patterns The list of patterns, eg "-foo/biz...". If a pattern's first character is "-",
-   *     it is treated as a negative pattern.
+   * @param patterns The list of patterns, eg "-foo/biz...".
    * @param policy The filtering policy, eg "only return test targets"
-   * @param offset The offset to apply to relative target patterns.
    */
   @ThreadSafe
-  public static Iterable<TargetPatternSkyKeyOrException> keys(
-      List<String> patterns, FilteringPolicy policy, PathFragment offset) {
-    TargetPattern.Parser parser = new TargetPattern.Parser(offset);
-    ImmutableList.Builder<TargetPatternSkyKeyOrException> builder = ImmutableList.builder();
-    for (String pattern : patterns) {
-      boolean positive = !pattern.startsWith("-");
-      String absoluteValueOfPattern = positive ? pattern : pattern.substring(1);
-      TargetPattern targetPattern;
-      try {
-        targetPattern = parser.parse(absoluteValueOfPattern);
-      } catch (TargetParsingException e) {
-        builder.add(new TargetPatternSkyKeyException(e, absoluteValueOfPattern));
-        continue;
-      }
-      TargetPatternKey targetPatternKey =
-          new TargetPatternKey(
-              targetPattern,
-              positive ? policy : FilteringPolicies.NO_FILTER,
-              /*isNegative=*/ !positive,
-              offset);
-      builder.add(new TargetPatternSkyKeyValue(targetPatternKey));
-    }
-    return builder.build();
+  public static Iterable<TargetPatternKey> keys(
+      List<SignedTargetPattern> patterns, FilteringPolicy policy) {
+    return patterns.stream().map(pattern -> key(pattern, policy)).collect(toImmutableList());
   }
 
   @ThreadSafe
@@ -189,12 +164,7 @@
       policy =
           FilteringPolicies.and(policy, new TargetExcludingFilteringPolicy(excludedSingleTargets));
     }
-    return new TargetPatternKey(
-        original.getParsedPattern(),
-        policy,
-        original.isNegative(),
-        original.getOffset(),
-        excludedSubdirectories);
+    return new TargetPatternKey(original.getSignedParsedPattern(), policy, excludedSubdirectories);
   }
 
   private static class TargetPatternKeyWithExclusionsResult {
@@ -281,36 +251,26 @@
    */
   @ThreadSafe
   public static class TargetPatternKey implements SkyKey, Serializable {
-    private final TargetPattern parsedPattern;
+    private final SignedTargetPattern signedParsedPattern;
     private final FilteringPolicy policy;
-    private final boolean isNegative;
 
-    private final PathFragment offset;
     /**
-     * Must be "compatible" with {@link #parsedPattern}: if {@link #parsedPattern} is a {@link
-     * TargetsBelowDirectory} object, then {@link TargetsBelowDirectory#containedIn} is false for
-     * every element of {@code excludedSubdirectories}.
+     * Must be "compatible" with {@link #signedParsedPattern}: if {@link #signedParsedPattern} is a
+     * {@link TargetsBelowDirectory} object, then {@link TargetsBelowDirectory#containedIn} is false
+     * for every element of {@code excludedSubdirectories}.
      */
     private final ImmutableSet<PathFragment> excludedSubdirectories;
 
-    public TargetPatternKey(
-        TargetPattern parsedPattern,
-        FilteringPolicy policy,
-        boolean isNegative,
-        PathFragment offset) {
-      this(parsedPattern, policy, isNegative, offset, ImmutableSet.of());
+    public TargetPatternKey(SignedTargetPattern signedParsedPattern, FilteringPolicy policy) {
+      this(signedParsedPattern, policy, ImmutableSet.of());
     }
 
     private TargetPatternKey(
-        TargetPattern parsedPattern,
+        SignedTargetPattern signedParsedPattern,
         FilteringPolicy policy,
-        boolean isNegative,
-        PathFragment offset,
         ImmutableSet<PathFragment> excludedSubdirectories) {
-      this.parsedPattern = Preconditions.checkNotNull(parsedPattern);
+      this.signedParsedPattern = Preconditions.checkNotNull(signedParsedPattern);
       this.policy = Preconditions.checkNotNull(policy);
-      this.isNegative = isNegative;
-      this.offset = offset;
       this.excludedSubdirectories = Preconditions.checkNotNull(excludedSubdirectories);
     }
 
@@ -320,25 +280,25 @@
     }
 
     public String getPattern() {
-      return parsedPattern.getOriginalPattern();
+      return signedParsedPattern.pattern().getOriginalPattern();
     }
 
     public TargetPattern getParsedPattern() {
-      return parsedPattern;
+      return signedParsedPattern.pattern();
+    }
+
+    private SignedTargetPattern getSignedParsedPattern() {
+      return signedParsedPattern;
     }
 
     public boolean isNegative() {
-      return isNegative;
+      return signedParsedPattern.sign() == Sign.NEGATIVE;
     }
 
     public FilteringPolicy getPolicy() {
       return policy;
     }
 
-    public PathFragment getOffset() {
-      return offset;
-    }
-
     public ImmutableSet<PathFragment> getExcludedSubdirectories() {
       return excludedSubdirectories;
     }
@@ -347,15 +307,14 @@
     public String toString() {
       return String.format(
           "%s, excludedSubdirs=%s, filteringPolicy=%s",
-          (isNegative ? "-" : "") + parsedPattern.getOriginalPattern(),
+          (isNegative() ? "-" : "") + signedParsedPattern.pattern().getOriginalPattern(),
           excludedSubdirectories,
           getPolicy());
     }
 
     @Override
     public int hashCode() {
-      return Objects.hash(parsedPattern, isNegative, policy, offset,
-          excludedSubdirectories);
+      return Objects.hash(signedParsedPattern, policy, excludedSubdirectories);
     }
 
     @Override
@@ -365,69 +324,9 @@
       }
       TargetPatternKey other = (TargetPatternKey) obj;
 
-      return other.isNegative == this.isNegative && other.parsedPattern.equals(this.parsedPattern)
-          && other.offset.equals(this.offset) && other.policy.equals(this.policy)
+      return other.signedParsedPattern.equals(this.signedParsedPattern)
+          && other.policy.equals(this.policy)
           && other.excludedSubdirectories.equals(this.excludedSubdirectories);
     }
   }
-
-  /**
-   * Wrapper for a target pattern {@link SkyKey} or the {@link TargetParsingException} thrown when
-   * trying to compute it.
-   */
-  public interface TargetPatternSkyKeyOrException {
-
-    /**
-     * Returns the stored {@link SkyKey} or throws {@link TargetParsingException} if one was thrown
-     * when computing the key.
-     */
-    TargetPatternKey getSkyKey() throws TargetParsingException;
-
-    /**
-     * Returns the pattern that resulted in the stored {@link SkyKey} or {@link
-     * TargetParsingException}.
-     */
-    String getOriginalPattern();
-  }
-
-  private static final class TargetPatternSkyKeyValue implements TargetPatternSkyKeyOrException {
-
-    private final TargetPatternKey value;
-
-    private TargetPatternSkyKeyValue(TargetPatternKey value) {
-      this.value = value;
-    }
-
-    @Override
-    public TargetPatternKey getSkyKey() throws TargetParsingException {
-      return value;
-    }
-
-    @Override
-    public String getOriginalPattern() {
-      return ((TargetPatternKey) value.argument()).getPattern();
-    }
-  }
-
-  private static final class TargetPatternSkyKeyException implements
-      TargetPatternSkyKeyOrException {
-
-    private final TargetParsingException exception;
-    private final String originalPattern;
-
-    private TargetPatternSkyKeyException(TargetParsingException exception, String originalPattern) {
-      this.exception = exception;
-      this.originalPattern = originalPattern;
-    }
-
-    @Override
-    public TargetPatternKey getSkyKey() throws TargetParsingException {
-      throw exception;
-    }
-
-    @Override
-    public String getOriginalPattern() {
-      return originalPattern;
-    }
-  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/cmdline/TargetPatternTest.java b/src/test/java/com/google/devtools/build/lib/cmdline/TargetPatternTest.java
index eb24477..0e50105 100644
--- a/src/test/java/com/google/devtools/build/lib/cmdline/TargetPatternTest.java
+++ b/src/test/java/com/google/devtools/build/lib/cmdline/TargetPatternTest.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.cmdline.TargetPattern.TargetsBelowDirectory;
 import com.google.devtools.build.lib.cmdline.TargetPattern.TargetsBelowDirectory.ContainsResult;
+import com.google.devtools.build.lib.vfs.PathFragment;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -171,35 +172,26 @@
             ImmutableMap.of(
                 RepositoryName.create("@foo"), RepositoryName.create("@bar"),
                 RepositoryName.create("@myworkspace"), RepositoryName.create("@")));
+    TargetPattern.Parser parser =
+        new TargetPattern.Parser(
+            PathFragment.EMPTY_FRAGMENT,
+            RepositoryName.createFromValidStrippedName("myrepo"),
+            renaming);
 
     // Expecting renaming
-    assertThat(TargetPattern.renameRepository("@foo//package:target", renaming))
-        .isEqualTo("@bar//package:target");
-    assertThat(TargetPattern.renameRepository("@myworkspace//package:target", renaming))
-        .isEqualTo("@//package:target");
-    assertThat(TargetPattern.renameRepository("@foo//foo/...", renaming))
-        .isEqualTo("@bar//foo/...");
-    assertThat(TargetPattern.renameRepository("@myworkspace//foo/...", renaming))
-        .isEqualTo("@//foo/...");
-
-    // Expecting renaming with the prefix negation operator -
-    assertThat(TargetPattern.renameRepository("-@foo//package:target", renaming))
-        .isEqualTo("-@bar//package:target");
-    assertThat(TargetPattern.renameRepository("-@myworkspace//package:target", renaming))
-        .isEqualTo("-@//package:target");
-    assertThat(TargetPattern.renameRepository("-@foo//foo/...", renaming))
-        .isEqualTo("-@bar//foo/...");
-    assertThat(TargetPattern.renameRepository("-@myworkspace//foo/...", renaming))
-        .isEqualTo("-@//foo/...");
+    assertThat(parser.parse("@foo//package:target").getRepository().strippedName())
+        .isEqualTo("bar");
+    assertThat(parser.parse("@myworkspace//package:target").getRepository().isMain()).isTrue();
+    assertThat(parser.parse("@foo//foo/...").getRepository().strippedName()).isEqualTo("bar");
+    assertThat(parser.parse("@myworkspace//foo/...").getRepository().isMain()).isTrue();
 
     // No renaming should occur
-    assertThat(TargetPattern.renameRepository("@//package:target", renaming))
-        .isEqualTo("@//package:target");
-    assertThat(TargetPattern.renameRepository("@unrelated//package:target", renaming))
-        .isEqualTo("@unrelated//package:target");
-    assertThat(TargetPattern.renameRepository("foo/package:target", renaming))
-        .isEqualTo("foo/package:target");
-    assertThat(TargetPattern.renameRepository("foo/...", renaming)).isEqualTo("foo/...");
+    assertThat(parser.parse("@//package:target").getRepository().isMain()).isTrue();
+    assertThat(parser.parse("@unrelated//package:target").getRepository().strippedName())
+        .isEqualTo("unrelated");
+    assertThat(parser.parse("foo/package:target").getRepository().strippedName())
+        .isEqualTo("myrepo");
+    assertThat(parser.parse("foo/...").getRepository().strippedName()).isEqualTo("myrepo");
   }
 
   private static TargetPattern parse(String pattern) throws TargetParsingException {
diff --git a/src/test/java/com/google/devtools/build/lib/packages/WorkspaceFactoryTest.java b/src/test/java/com/google/devtools/build/lib/packages/WorkspaceFactoryTest.java
index c153fd4..e48acfc 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/WorkspaceFactoryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/WorkspaceFactoryTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.devtools.build.lib.cmdline.RepositoryName;
+import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.testutil.Scratch;
 import com.google.devtools.build.lib.vfs.Root;
 import org.junit.Before;
@@ -76,14 +77,16 @@
   public void testRegisterExecutionPlatforms() throws Exception {
     helper.parse("register_execution_platforms('//platform:ep1')");
     assertThat(helper.getPackage().getRegisteredExecutionPlatforms())
-        .containsExactly("//platform:ep1");
+        .containsExactly(TargetPattern.defaultParser().parse("//platform:ep1"));
   }
 
   @Test
   public void testRegisterExecutionPlatforms_multipleLabels() throws Exception {
-    helper.parse("register_execution_platforms(", "  '//platform:ep1',", "  '//platform:ep2')");
+    helper.parse("register_execution_platforms('//platform:ep1', '//platform:ep2')");
     assertThat(helper.getPackage().getRegisteredExecutionPlatforms())
-        .containsExactly("//platform:ep1", "//platform:ep2");
+        .containsExactly(
+            TargetPattern.defaultParser().parse("//platform:ep1"),
+            TargetPattern.defaultParser().parse("//platform:ep2"));
   }
 
   @Test
@@ -93,20 +96,26 @@
         "register_execution_platforms('//platform:ep2')",
         "register_execution_platforms('//platform/...')");
     assertThat(helper.getPackage().getRegisteredExecutionPlatforms())
-        .containsExactly("//platform:ep1", "//platform:ep2", "//platform/...");
+        .containsExactly(
+            TargetPattern.defaultParser().parse("//platform:ep1"),
+            TargetPattern.defaultParser().parse("//platform:ep2"),
+            TargetPattern.defaultParser().parse("//platform/..."));
   }
 
   @Test
   public void testRegisterToolchains() throws Exception {
     helper.parse("register_toolchains('//toolchain:tc1')");
-    assertThat(helper.getPackage().getRegisteredToolchains()).containsExactly("//toolchain:tc1");
+    assertThat(helper.getPackage().getRegisteredToolchains())
+        .containsExactly(TargetPattern.defaultParser().parse("//toolchain:tc1"));
   }
 
   @Test
   public void testRegisterToolchains_multipleLabels() throws Exception {
-    helper.parse("register_toolchains(", "  '//toolchain:tc1',", "  '//toolchain:tc2')");
+    helper.parse("register_toolchains('//toolchain:tc1', '//toolchain:tc2')");
     assertThat(helper.getPackage().getRegisteredToolchains())
-        .containsExactly("//toolchain:tc1", "//toolchain:tc2");
+        .containsExactly(
+            TargetPattern.defaultParser().parse("//toolchain:tc1"),
+            TargetPattern.defaultParser().parse("//toolchain:tc2"));
   }
 
   @Test
@@ -116,7 +125,10 @@
         "register_toolchains('//toolchain:tc2')",
         "register_toolchains('//toolchain/...')");
     assertThat(helper.getPackage().getRegisteredToolchains())
-        .containsExactly("//toolchain:tc1", "//toolchain:tc2", "//toolchain/...");
+        .containsExactly(
+            TargetPattern.defaultParser().parse("//toolchain:tc1"),
+            TargetPattern.defaultParser().parse("//toolchain:tc2"),
+            TargetPattern.defaultParser().parse("//toolchain/..."));
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/repository/ExternalPackageHelperTest.java b/src/test/java/com/google/devtools/build/lib/repository/ExternalPackageHelperTest.java
index 3ecf4a8..59fa1cb 100644
--- a/src/test/java/com/google/devtools/build/lib/repository/ExternalPackageHelperTest.java
+++ b/src/test/java/com/google/devtools/build/lib/repository/ExternalPackageHelperTest.java
@@ -14,6 +14,7 @@
 
 package com.google.devtools.build.lib.repository;
 
+import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.devtools.build.skyframe.EvaluationResultSubjectFactory.assertThatEvaluationResult;
 import static org.mockito.Mockito.mock;
@@ -30,6 +31,7 @@
 import com.google.devtools.build.lib.analysis.util.AnalysisMock;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
+import com.google.devtools.build.lib.cmdline.TargetPattern;
 import com.google.devtools.build.lib.events.NullEventHandler;
 import com.google.devtools.build.lib.packages.BuildFileName;
 import com.google.devtools.build.lib.packages.Rule;
@@ -413,11 +415,15 @@
     @Override
     public SkyValue compute(SkyKey skyKey, Environment env)
         throws SkyFunctionException, InterruptedException {
-      List<String> registeredToolchains = RegisteredToolchainsFunction.getWorkspaceToolchains(env);
+      List<TargetPattern> registeredToolchains =
+          RegisteredToolchainsFunction.getWorkspaceToolchains(env);
       if (registeredToolchains == null) {
         return null;
       }
-      return GetRegisteredToolchainsValue.create(registeredToolchains);
+      return GetRegisteredToolchainsValue.create(
+          registeredToolchains.stream()
+              .map(TargetPattern::getOriginalPattern)
+              .collect(toImmutableList()));
     }
 
     @Nullable
@@ -447,12 +453,15 @@
     @Override
     public SkyValue compute(SkyKey skyKey, Environment env)
         throws SkyFunctionException, InterruptedException {
-      List<String> registeredExecutionPlatforms =
+      List<TargetPattern> registeredExecutionPlatforms =
           RegisteredExecutionPlatformsFunction.getWorkspaceExecutionPlatforms(env);
       if (registeredExecutionPlatforms == null) {
         return null;
       }
-      return GetRegisteredExecutionPlatformsValue.create(registeredExecutionPlatforms);
+      return GetRegisteredExecutionPlatformsValue.create(
+          registeredExecutionPlatforms.stream()
+              .map(TargetPattern::getOriginalPattern)
+              .collect(toImmutableList()));
     }
 
     @Nullable
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/RegisteredExecutionPlatformsFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/RegisteredExecutionPlatformsFunctionTest.java
index e83d783..89b96b5 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/RegisteredExecutionPlatformsFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/RegisteredExecutionPlatformsFunctionTest.java
@@ -193,12 +193,8 @@
 
     assertExecutionPlatformLabels(result.get(executionPlatformsKey))
         .containsAtLeast(
-            // We would expect:
-            // Label.parseAbsoluteUnchecked("@myrepo//platforms:execution_platform_1"),
-            // Label.parseAbsoluteUnchecked("@myrepo//platforms:execution_platform_2"))
-            // but this is actually:
-            Label.parseAbsoluteUnchecked("//platforms:linux"),
-            Label.parseAbsoluteUnchecked("//platforms:mac"))
+            Label.parseAbsoluteUnchecked("@myrepo//platforms:execution_platform_1"),
+            Label.parseAbsoluteUnchecked("@myrepo//platforms:execution_platform_2"))
         .inOrder();
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/RegisteredToolchainsFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/RegisteredToolchainsFunctionTest.java
index b2adebe..71d59c7 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/RegisteredToolchainsFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/RegisteredToolchainsFunctionTest.java
@@ -158,23 +158,6 @@
   }
 
   @Test
-  public void testRegisteredToolchains_invalidPattern() throws Exception {
-    rewriteWorkspace("register_toolchains('/:invalid:label:syntax')");
-
-    // Request the toolchains.
-    SkyKey toolchainsKey = RegisteredToolchainsValue.key(targetConfigKey);
-    EvaluationResult<RegisteredToolchainsValue> result =
-        requestToolchainsFromSkyframe(toolchainsKey);
-    assertThatEvaluationResult(result)
-        .hasErrorEntryForKeyThat(toolchainsKey)
-        .hasExceptionThat()
-        .hasMessageThat()
-        .contains(
-            "invalid registered toolchain '/:invalid:label:syntax': "
-                + "not a valid absolute pattern");
-  }
-
-  @Test
   public void testRegisteredToolchains_notToolchain() throws Exception {
     rewriteWorkspace("register_toolchains('//error:not_a_toolchain')");
     scratch.file("error/BUILD", "filegroup(name = 'not_a_toolchain')");
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunctionTest.java
index 534ff72..0cd0436 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunctionTest.java
@@ -430,6 +430,22 @@
   }
 
   @Test
+  public void testRegisterToolchainsInvalidPattern() throws Exception {
+    // Test intentionally introduces errors.
+    reporter.removeHandler(failFastHandler);
+
+    // //external:bar:baz is not a legal package.
+    String[] lines = {"register_toolchains('/:invalid:label:syntax')"};
+    createWorkspaceFile(lines);
+
+    SkyKey key = ExternalPackageFunction.key();
+    EvaluationResult<PackageValue> evaluationResult = eval(key);
+    Package pkg = evaluationResult.get(key).getPackage();
+    assertThat(pkg.containsErrors()).isTrue();
+    assertContainsEvent("not a valid absolute pattern");
+  }
+
+  @Test
   public void testNoWorkspaceFile() throws Exception {
     // Create and immediately delete to make sure we got the right file.
     RootedPath workspacePath = createWorkspaceFile();
diff --git a/src/test/shell/bazel/toolchain_test.sh b/src/test/shell/bazel/toolchain_test.sh
index 37e1327..3fce211 100755
--- a/src/test/shell/bazel/toolchain_test.sh
+++ b/src/test/shell/bazel/toolchain_test.sh
@@ -747,7 +747,7 @@
 EOF
 
   bazel build //demo:use &> $TEST_log && fail "Build failure expected"
-  expect_log "invalid registered toolchain '/:invalid:label:syntax': not a valid absolute pattern"
+  expect_log "error parsing target pattern \"/:invalid:label:syntax\": not a valid absolute pattern"
 }
 
 function test_register_toolchain_error_invalid_target() {