blob: 7105afce3f9d4208f0a75480bc92ecb709183359 [file] [log] [blame]
// Copyright 2014 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 static com.google.devtools.build.lib.cmdline.LabelParser.validateAndProcessTargetName;
import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Interner;
import com.google.devtools.build.docgen.annot.DocCategory;
import com.google.devtools.build.lib.actions.CommandLineItem;
import com.google.devtools.build.lib.cmdline.LabelParser.Parts;
import com.google.devtools.build.lib.concurrent.BlazeInterners;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.skyframe.SkyFunctionName;
import com.google.devtools.build.skyframe.SkyKey;
import java.util.Arrays;
import javax.annotation.Nullable;
import net.starlark.java.annot.Param;
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.annot.StarlarkMethod;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Module;
import net.starlark.java.eval.Printer;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkSemantics;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.eval.StarlarkValue;
/**
* A class to identify a BUILD target. All targets belong to exactly one package. The name of a
* target is called its label. A typical label looks like this: //dir1/dir2:target_name where
* 'dir1/dir2' identifies the package containing a BUILD file, and 'target_name' identifies the
* target within the package.
*
* <p>Parsing is robust against bad input, for example, from the command line.
*/
@StarlarkBuiltin(name = "Label", category = DocCategory.BUILTIN, doc = "A BUILD target identifier.")
@AutoCodec
@Immutable
@ThreadSafe
public final class Label implements Comparable<Label>, StarlarkValue, SkyKey, CommandLineItem {
/**
* Package names that aren't made relative to the current repository because they mean special
* things to Bazel.
*/
private static final ImmutableSet<String> ABSOLUTE_PACKAGE_NAMES =
ImmutableSet.of(
// Used for select's `//conditions:default` label (not a target)
"conditions",
// Used for the public and private visibility labels (not targets)
"visibility",
// There is only one //external package
LabelConstants.EXTERNAL_PACKAGE_NAME.getPathString());
// Intern "__pkg__" and "__subpackages__" pseudo-targets, which appears in labels used for
// visibility specifications. This saves a couple tenths of a percent of RAM off the loading
// phase. Note that general interning of all values for `name` is *not* beneficial. See
// Google-internal cl/386077913 and cl/185394812 for more context.
private static final String PKG_VISIBILITY_NAME = "__pkg__";
private static final String SUBPACKAGES_VISIBILITY_NAME = "__subpackages__";
public static final SkyFunctionName TRANSITIVE_TRAVERSAL =
SkyFunctionName.createHermetic("TRANSITIVE_TRAVERSAL");
private static final Interner<Label> LABEL_INTERNER = BlazeInterners.newWeakInterner();
/** The context of a current repo, necessary to parse a repo-relative label ("//foo:bar"). */
public interface RepoContext {
static RepoContext of(RepositoryName currentRepo, RepositoryMapping repoMapping) {
return new AutoValue_Label_RepoContextImpl(currentRepo, repoMapping);
}
RepositoryName currentRepo();
RepositoryMapping repoMapping();
}
@AutoValue
abstract static class RepoContextImpl implements RepoContext {}
/** The context of a current package, necessary to parse a package-relative label (":foo"). */
public interface PackageContext extends RepoContext {
static PackageContext of(PackageIdentifier currentPackage, RepositoryMapping repoMapping) {
return new AutoValue_Label_PackageContextImpl(
currentPackage.getRepository(), repoMapping, currentPackage.getPackageFragment());
}
PathFragment packageFragment();
default PackageIdentifier packageIdentifier() {
return PackageIdentifier.create(currentRepo(), packageFragment());
}
}
@AutoValue
abstract static class PackageContextImpl implements PackageContext {}
/**
* Parses a raw label string that contains the canonical form of a label. It must be of the form
* {@code [@repo]//foo/bar[:quux]}. If the {@code @repo} part is present, it must be a canonical
* repo name, otherwise the label will be assumed to be in the main repo.
*/
public static Label parseCanonical(String raw) throws LabelSyntaxException {
Parts parts = Parts.parse(raw);
parts.checkPkgIsAbsolute();
RepositoryName repoName =
parts.repo == null ? RepositoryName.MAIN : RepositoryName.createUnvalidated(parts.repo);
return createUnvalidated(
PackageIdentifier.create(repoName, PathFragment.create(parts.pkg)), parts.target);
}
public static Label parseCanonicalUnchecked(String raw) {
try {
return parseCanonical(raw);
} catch (LabelSyntaxException e) {
throw new IllegalArgumentException(e);
}
}
/** Computes the repo name for the label, within the context of a current repo. */
private static RepositoryName computeRepoNameWithRepoContext(
Parts parts, RepoContext repoContext) {
if (parts.repo == null) {
// Certain package names when used without a "@" part are always absolutely in the main repo,
// disregarding the current repo and repo mappings.
return ABSOLUTE_PACKAGE_NAMES.contains(parts.pkg)
? RepositoryName.MAIN
: repoContext.currentRepo();
}
if (parts.repoIsCanonical) {
// This label uses the canonical label literal syntax starting with two @'s ("@@foo//bar").
return RepositoryName.createUnvalidated(parts.repo);
}
return repoContext.repoMapping().get(parts.repo);
}
/**
* Parses a raw label string within the context of a current repo. It must be of the form {@code
* [@repo]//foo/bar[:quux]}. If the {@code @repo} part is present, it will undergo {@code
* repoContext.repoMapping()}, otherwise the label will be assumed to be in {@code
* repoContext.currentRepo()}.
*/
public static Label parseWithRepoContext(String raw, RepoContext repoContext)
throws LabelSyntaxException {
Parts parts = Parts.parse(raw);
parts.checkPkgIsAbsolute();
RepositoryName repoName = computeRepoNameWithRepoContext(parts, repoContext);
return createUnvalidated(
PackageIdentifier.create(repoName, PathFragment.create(parts.pkg)), parts.target);
}
/**
* Parses a raw label string within the context of a current package. It can be of a
* package-relative form ({@code :quux}). Otherwise, it must be of the form {@code
* [@repo]//foo/bar[:quux]}. If the {@code @repo} part is present, it will undergo {@code
* packageContext.repoMapping()}, otherwise the label will be assumed to be in the repo of {@code
* packageContext.currentRepo()}.
*/
public static Label parseWithPackageContext(String raw, PackageContext packageContext)
throws LabelSyntaxException {
Parts parts = Parts.parse(raw);
// pkg is either absolute or empty
if (!parts.pkg.isEmpty()) {
parts.checkPkgIsAbsolute();
}
RepositoryName repoName = computeRepoNameWithRepoContext(parts, packageContext);
PathFragment pkgFragment =
parts.pkgIsAbsolute ? PathFragment.create(parts.pkg) : packageContext.packageFragment();
return createUnvalidated(PackageIdentifier.create(repoName, pkgFragment), parts.target);
}
/**
* Factory for Labels from absolute string form. e.g.
*
* <pre>
* //foo/bar
* //foo/bar:quux
* {@literal @}foo
* {@literal @}foo//bar
* {@literal @}foo//bar:baz
* </pre>
*
* <p>Labels that don't begin with a repository name are considered to be in the main repository,
* so for instance {@code //foo/bar} will turn into {@code @//foo/bar}.
*
* <p>Labels that begin with a repository name will undergo {@code repositoryMapping}.
*
* @param absName label-like string to be parsed
* @param repositoryMapping map of repository names from the local name found in the current
* repository to the global name declared in the main repository
*/
// TODO(b/200024947): Remove this.
public static Label parseAbsolute(String absName, RepositoryMapping repositoryMapping)
throws LabelSyntaxException {
Preconditions.checkNotNull(repositoryMapping);
return parseWithRepoContext(absName, RepoContext.of(RepositoryName.MAIN, repositoryMapping));
}
// TODO(b/200024947): Remove this.
public static Label parseAbsolute(
String absName, ImmutableMap<String, RepositoryName> repositoryMapping)
throws LabelSyntaxException {
return parseAbsolute(absName, RepositoryMapping.createAllowingFallback(repositoryMapping));
}
/**
* Alternate factory method for Labels from absolute strings. This is a convenience method for
* cases when a Label needs to be initialized statically, so the declared exception is
* inconvenient.
*
* <p>Do not use this when the argument is not hard-wired.
*/
// TODO(b/200024947): Remove this.
public static Label parseAbsoluteUnchecked(String absName) {
try {
return parseCanonical(absName);
} catch (LabelSyntaxException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Factory for Labels from separate components.
*
* @param packageName The name of the package. The package name does <b>not</b> include {@code
* //}. Must be valid according to {@link LabelValidator#validatePackageName}.
* @param targetName The name of the target within the package. Must be valid according to {@link
* LabelValidator#validateTargetName}.
* @throws LabelSyntaxException if either of the arguments was invalid.
*/
// TODO(b/200024947): Remove this...?
public static Label create(String packageName, String targetName) throws LabelSyntaxException {
return createUnvalidated(
PackageIdentifier.parse(packageName),
validateAndProcessTargetName(packageName, targetName));
}
/**
* Similar factory to above, but takes a package identifier to allow external repository labels to
* be created.
*/
// TODO(b/200024947): Remove this...?
public static Label create(PackageIdentifier packageId, String targetName)
throws LabelSyntaxException {
return createUnvalidated(
packageId,
validateAndProcessTargetName(packageId.getPackageFragment().getPathString(), targetName));
}
/**
* Similar factory to above, but does not perform target name validation.
*
* <p>Only call this method if you know what you're doing; in particular, don't call it on
* arbitrary {@code name} inputs
*/
@AutoCodec.Instantiator
public static Label createUnvalidated(PackageIdentifier packageIdentifier, String name) {
String internedName = name;
if (internedName.equals(PKG_VISIBILITY_NAME)) {
internedName = PKG_VISIBILITY_NAME;
} else if (internedName.equals(SUBPACKAGES_VISIBILITY_NAME)) {
internedName = SUBPACKAGES_VISIBILITY_NAME;
}
return LABEL_INTERNER.intern(new Label(packageIdentifier, internedName));
}
/** The name and repository of the package. */
private final PackageIdentifier packageIdentifier;
/** The name of the target within the package. Canonical. */
private final String name;
private Label(PackageIdentifier packageIdentifier, String name) {
Preconditions.checkNotNull(packageIdentifier);
Preconditions.checkNotNull(name);
this.packageIdentifier = packageIdentifier;
this.name = name;
}
public PackageIdentifier getPackageIdentifier() {
return packageIdentifier;
}
public RepositoryName getRepository() {
return packageIdentifier.getRepository();
}
/**
* Returns the name of the package in which this rule was declared (e.g. {@code
* //file/base:fileutils_test} returns {@code file/base}).
*/
@StarlarkMethod(
name = "package",
structField = true,
doc =
"The package part of this label. "
+ "For instance:<br>"
+ "<pre class=language-python>Label(\"//pkg/foo:abc\").package == \"pkg/foo\"</pre>")
public String getPackageName() {
return packageIdentifier.getPackageFragment().getPathString();
}
/**
* Returns the execution root for the workspace, relative to the execroot (e.g., for label
* {@code @repo//pkg:b}, it will returns {@code external/repo/pkg} and for label {@code //pkg:a},
* it will returns an empty string.
*
* @deprecated The sole purpose of this method is to implement the workspace_root method. For
* other purposes, use {@link RepositoryName#getExecPath} instead.
*/
@StarlarkMethod(
name = "workspace_root",
structField = true,
doc =
"Returns the execution root for the workspace of this label, relative to the execroot. "
+ "For instance:<br>"
+ "<pre class=language-python>Label(\"@repo//pkg/foo:abc\").workspace_root =="
+ " \"external/repo\"</pre>",
useStarlarkSemantics = true)
@Deprecated
public String getWorkspaceRootForStarlarkOnly(StarlarkSemantics semantics) {
return packageIdentifier
.getRepository()
.getExecPath(semantics.getBool(BuildLanguageOptions.EXPERIMENTAL_SIBLING_REPOSITORY_LAYOUT))
.toString();
}
/**
* Returns the path fragment of the package in which this rule was declared (e.g. {@code
* //file/base:fileutils_test} returns {@code file/base}).
*
* <p>This is <b>not</b> suitable for inferring a path under which files related to a rule with
* this label will be under the exec root, in particular, it won't work for rules in external
* repositories.
*/
public PathFragment getPackageFragment() {
return packageIdentifier.getPackageFragment();
}
/**
* Returns the label as a path fragment, using the package and the label name.
*
* <p>Make sure that the label refers to a file. Non-file labels do not necessarily have
* PathFragment representations.
*
* <p>The package's repository is not included in the returned fragment. To account for it,
* compose this with {@code #getRepository()#getExecPath}.
*/
public PathFragment toPathFragment() {
// PathFragments are normalized, so if we do this on a non-file target named '.'
// then the package would be returned. Detect this and throw.
// A target named '.' can never refer to a file.
Preconditions.checkArgument(!name.equals("."));
return packageIdentifier.getPackageFragment().getRelative(name);
}
/**
* Returns the name by which this rule was declared (e.g. {@code //foo/bar:baz} returns {@code
* baz}).
*/
@StarlarkMethod(
name = "name",
structField = true,
doc =
"The name of this label within the package. "
+ "For instance:<br>"
+ "<pre class=language-python>Label(\"//pkg/foo:abc\").name == \"abc\"</pre>")
public String getName() {
return name;
}
/**
* Renders this label in canonical form.
*
* <p>invariant: {@code parseCanonical(x.toString()).equals(x)}. Note that using {@link
* #parseWithPackageContext} or {@link #parseWithRepoContext} on the returned string might not
* yield the same label! For that, use {@link #getUnambiguousCanonicalForm()}.
*/
@Override
public String toString() {
return getCanonicalForm();
}
/**
* Renders this label in canonical form.
*
* <p>invariant: {@code parseCanonical(x.getCanonicalForm()).equals(x)}. Note that using {@link
* #parseWithPackageContext} or {@link #parseWithRepoContext} on the returned string might not
* yield the same label! For that, use {@link #getUnambiguousCanonicalForm()}.
*/
public String getCanonicalForm() {
return packageIdentifier.getCanonicalForm() + ":" + name;
}
/**
* Returns an absolutely unambiguous canonical form for this label. Parsing this string in any
* environment should yield the same label (as in {@code
* Label.parse*(x.getUnambiguousCanonicalForm(), ...).equals(x)}).
*/
public String getUnambiguousCanonicalForm() {
return String.format(
"@@%s//%s:%s",
packageIdentifier.getRepository().getName(), packageIdentifier.getPackageFragment(), name);
}
/** Return the name of the repository label refers to without the leading `at` symbol. */
@StarlarkMethod(
name = "workspace_name",
structField = true,
doc =
"The repository part of this label. For instance, "
+ "<pre class=language-python>Label(\"@foo//bar:baz\").workspace_name"
+ " == \"foo\"</pre>")
public String getWorkspaceName() {
return packageIdentifier.getRepository().getName();
}
/**
* Renders this label in shorthand form.
*
* <p>Labels with canonical form {@code //foo/bar:bar} have the shorthand form {@code //foo/bar}.
* All other labels have identical shorthand and canonical forms.
*/
public String toShorthandString() {
if (!getPackageFragment().getBaseName().equals(name)) {
return toString();
}
String repository;
if (packageIdentifier.getRepository().isMain()) {
repository = "";
} else {
repository = packageIdentifier.getRepository().getNameWithAt();
}
return repository + "//" + getPackageFragment();
}
/**
* Returns a label in the same package as this label with the given target name.
*
* @throws LabelSyntaxException if {@code targetName} is not a valid target name
*/
public Label getLocalTargetLabel(String targetName) throws LabelSyntaxException {
return create(packageIdentifier, targetName);
}
/**
* Resolves a relative or absolute label name. If given name is absolute, then this method calls
* {@link #parseAbsolute}. Otherwise, it calls {@link #getLocalTargetLabel}.
*
* <p>For example: {@code :quux} relative to {@code //foo/bar:baz} is {@code //foo/bar:quux};
* {@code //wiz:quux} relative to {@code //foo/bar:baz} is {@code //wiz:quux}.
*
* @param relName the relative label name; must be non-empty.
* @param thread the Starlark thread.
*/
@StarlarkMethod(
name = "relative",
doc =
// TODO(#14503): Fix the documentation.
"Resolves a label that is either absolute (starts with <code>//</code>) or relative to "
+ "the current package. If this label is in a remote repository, the argument will "
+ "be resolved relative to that repository. If the argument contains a repository "
+ "name, the current label is ignored and the argument is returned as-is, except "
+ "that the repository name is rewritten if it is in the current repository mapping. "
+ "Reserved labels will also be returned as-is.<br>"
+ "For example:<br>"
+ "<pre class=language-python>\n"
+ "Label(\"//foo/bar:baz\").relative(\":quux\") == Label(\"//foo/bar:quux\")\n"
+ "Label(\"//foo/bar:baz\").relative(\"//wiz:quux\") == Label(\"//wiz:quux\")\n"
+ "Label(\"@repo//foo/bar:baz\").relative(\"//wiz:quux\") == "
+ "Label(\"@repo//wiz:quux\")\n"
+ "Label(\"@repo//foo/bar:baz\").relative(\"//visibility:public\") == "
+ "Label(\"//visibility:public\")\n"
+ "Label(\"@repo//foo/bar:baz\").relative(\"@other//wiz:quux\") == "
+ "Label(\"@other//wiz:quux\")\n"
+ "</pre>"
+ "<p>If the repository mapping passed in is <code>{'@other' : '@remapped'}</code>, "
+ "then the following remapping will take place:<br>"
+ "<pre class=language-python>\n"
+ "Label(\"@repo//foo/bar:baz\").relative(\"@other//wiz:quux\") == "
+ "Label(\"@remapped//wiz:quux\")\n"
+ "</pre>",
parameters = {
@Param(name = "relName", doc = "The label that will be resolved relative to this one.")
},
useStarlarkThread = true)
public Label getRelative(String relName, StarlarkThread thread)
throws LabelSyntaxException, EvalException {
Label label =
getRelativeWithRemapping(
relName,
BazelModuleContext.of(Module.ofInnermostEnclosingStarlarkFunction(thread))
.repoMapping());
if (!label.getRepository().isVisible()) {
throw Starlark.errorf(
"Invalid label string '%s': no repository visible as '@%s' from %s",
relName,
label.getRepository().getName(),
label.getRepository().getOwnerRepoDisplayString());
}
return label;
}
/**
* Resolves a relative or absolute label name. If given name is absolute, then this method calls
* {@link #parseAbsolute}. Otherwise, it calls {@link #getLocalTargetLabel}.
*
* <p>For example: {@code :quux} relative to {@code //foo/bar:baz} is {@code //foo/bar:quux};
* {@code //wiz:quux} relative to {@code //foo/bar:baz} is {@code //wiz:quux};
* {@code @repo//foo:bar} relative to anything will be {@code @repo//foo:bar} if {@code @repo} is
* not in {@code repositoryMapping} but will be {@code @other_repo//foo:bar} if there is an entry
* {@code @repo -> @other_repo} in {@code repositoryMapping}.
*
* @param relName the relative label name; must be non-empty
* @param repositoryMapping the map of local repository names in external repository to global
* repository names in main repo; can be empty, but not null
*/
// TODO(b/200024947): Remove this.
public Label getRelativeWithRemapping(String relName, RepositoryMapping repositoryMapping)
throws LabelSyntaxException {
Preconditions.checkNotNull(repositoryMapping);
if (relName.isEmpty()) {
throw new LabelSyntaxException("empty package-relative label");
}
return parseWithPackageContext(
relName, PackageContext.of(packageIdentifier, repositoryMapping));
}
// TODO(b/200024947): Remove this.
public Label getRelativeWithRemapping(
String relName, ImmutableMap<String, RepositoryName> repositoryMapping)
throws LabelSyntaxException {
return getRelativeWithRemapping(
relName, RepositoryMapping.createAllowingFallback(repositoryMapping));
}
@Override
public SkyFunctionName functionName() {
return TRANSITIVE_TRAVERSAL;
}
@Override
public int hashCode() {
return hashCode(name, packageIdentifier);
}
/**
* Specialization of {@link Arrays#hashCode()} that does not require constructing a 2-element
* array.
*/
private static int hashCode(Object obj1, Object obj2) {
int result = 31 + (obj1 == null ? 0 : obj1.hashCode());
return 31 * result + (obj2 == null ? 0 : obj2.hashCode());
}
/** Two labels are equal iff both their name and their package name are equal. */
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Label)) {
return false;
}
Label otherLabel = (Label) other;
// Package identifiers are (weakly) interned so we compare them first.
return packageIdentifier.equals(otherLabel.packageIdentifier) && name.equals(otherLabel.name);
}
/**
* Defines the order between labels.
*
* <p>Labels are ordered primarily by package name and secondarily by target name. Both components
* are ordered lexicographically. Thus {@code //a:b/c} comes before {@code //a/b:a}, i.e. the
* position of the colon is significant to the order.
*/
@Override
public int compareTo(Label other) {
if (this == other) {
return 0;
}
return ComparisonChain.start()
.compare(packageIdentifier, other.packageIdentifier)
.compare(name, other.name)
.result();
}
/**
* Returns a suitable string for the user-friendly representation of the Label. Works even if the
* argument is null.
*/
public static String print(@Nullable Label label) {
return label == null ? "(unknown)" : label.toString();
}
/**
* Returns a {@link PathFragment} corresponding to the directory in which {@code label} would
* reside, if it were interpreted to be a path.
*/
public static PathFragment getContainingDirectory(Label label) {
PathFragment pkg = label.getPackageFragment();
String name = label.name;
if (name.equals(".")) {
return pkg;
}
if (PathFragment.isNormalizedRelativePath(name) && !PathFragment.containsSeparator(name)) {
// Optimize for the common case of a label like '//pkg:target'.
return pkg;
}
return pkg.getRelative(name).getParentDirectory();
}
@Override
public boolean isImmutable() {
return true;
}
@Override
public void repr(Printer printer) {
// TODO(wyv): Consider using StarlarkSemantics here too for optional unambiguity.
printer.append("Label(");
printer.repr(getCanonicalForm());
printer.append(")");
}
@Override
public void str(Printer printer, StarlarkSemantics semantics) {
if (getRepository().isMain()
&& !semantics.getBool(
BuildLanguageOptions.INCOMPATIBLE_UNAMBIGUOUS_LABEL_STRINGIFICATION)) {
// If this label is in the main repo and we're not using unambiguous label stringification,
// the result should always be "//foo:bar".
printer.append(getCanonicalForm());
return;
}
if (semantics.getBool(BuildLanguageOptions.ENABLE_BZLMOD)) {
// If Bzlmod is enabled, we use canonical label literal syntax here and prepend an extra '@'.
// So the result looks like "@@//foo:bar" for the main repo and "@@foo~1.0//bar:quux" for
// other repos.
printer.append(getUnambiguousCanonicalForm());
return;
}
// If Bzlmod is not enabled, we just use a single '@'.
// So the result looks like "@//foo:bar" for the main repo and "@foo//bar:quux" for other repos.
printer.append(
String.format(
"@%s//%s:%s",
packageIdentifier.getRepository().getName(),
packageIdentifier.getPackageFragment(),
name));
}
@Override
public String expandToCommandLine() {
// TODO(wyv): Consider using StarlarkSemantics here too for optional unambiguity.
return getCanonicalForm();
}
}