diff --git a/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java b/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java index 5fd5860919167e..39eadbb4ac81d1 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleClassFunctions.java @@ -75,6 +75,8 @@ import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.StarlarkImplicitOutputsFunctionWithCallback; import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.StarlarkImplicitOutputsFunctionWithMap; import com.google.devtools.build.lib.packages.LabelConverter; +import com.google.devtools.build.lib.packages.MacroClass; +import com.google.devtools.build.lib.packages.MacroInstance; import com.google.devtools.build.lib.packages.Package; import com.google.devtools.build.lib.packages.Package.NameConflictException; import com.google.devtools.build.lib.packages.PredicateWithMessage; @@ -311,6 +313,22 @@ private static void failIf(boolean condition, String message, Object... args) } } + @Override + public StarlarkCallable macro(StarlarkFunction implementation, StarlarkThread thread) + throws EvalException { + // Ordinarily we would use StarlarkMethod#enableOnlyWithFlag, but this doesn't work for + // top-level symbols (due to StarlarkGlobalsImpl relying on the Starlark#addMethods overload + // that uses default StarlarkSemantics), so enforce it here instead. + if (!thread + .getSemantics() + .getBool(BuildLanguageOptions.EXPERIMENTAL_ENABLE_FIRST_CLASS_MACROS)) { + throw Starlark.errorf("Use of `macro()` requires --experimental_enable_first_class_macros"); + } + + MacroClass.Builder builder = new MacroClass.Builder(implementation); + return new MacroFunction(builder); + } + // TODO(bazel-team): implement attribute copy and other rule properties @Override public StarlarkRuleFunction rule( @@ -434,7 +452,7 @@ public StarlarkRuleFunction rule( } /** - * Returns a new function representing a Starlark-defined rule. + * Returns a new callable representing a Starlark-defined rule. * *

This is public for the benefit of {@link StarlarkTestingModule}, which has the unusual use * case of creating new rule types to house analysis-time test assertions ({@code analysis_test}). @@ -974,26 +992,143 @@ private static ImmutableSet getLegacyAnyTypeAttrs(RuleClass ruleClass) { } /** - * The implementation for the magic function "rule" that creates Starlark rule classes. + * A callable Starlark object representing a symbolic macro, which may be invoked during package + * construction time to instantiate the macro. + * + *

Instantiating the macro does not necessarily imply that the macro's implementation function + * will run synchronously with the call to this object. Just like a rule, a macro's implementation + * function is evaluated in its own context separate from the caller. + * + *

This object is not usable until it has been {@link #export exported}. Calling an unexported + * macro function results in an {@link EvalException}. + */ + public static final class MacroFunction implements StarlarkExportable, StarlarkCallable { + + // Initially non-null, then null once exported. + @Nullable private MacroClass.Builder builder; + + // Initially null, then non-null once exported. + @Nullable private MacroClass macroClass = null; + + public MacroFunction(MacroClass.Builder builder) { + this.builder = builder; + } + + @Override + public String getName() { + return macroClass != null ? macroClass.getName() : "unexported macro"; + } + + // TODO(#19922): Define getDocumentation() and interaction with ModuleInfoExtractor, analogous + // to StarlarkRuleFunction. + + @Override + public Object call(StarlarkThread thread, Tuple args, Dict kwargs) + throws EvalException, InterruptedException { + BazelStarlarkContext.checkLoadingPhase(thread, getName()); + Package.Builder pkgBuilder = thread.getThreadLocal(Package.Builder.class); + if (pkgBuilder == null) { + throw new EvalException( + "Cannot instantiate a macro when loading a .bzl file. " + + "Macros may only be instantiated while evaluating a BUILD file."); + } + + if (macroClass == null) { + throw Starlark.errorf( + "Cannot instantiate a macro that has not been exported (assign it to a global variable" + + " in the .bzl where it's defined)"); + } + + if (!args.isEmpty()) { + throw Starlark.errorf("unexpected positional arguments"); + } + + Object nameUnchecked = kwargs.get("name"); + if (nameUnchecked == null) { + throw Starlark.errorf("macro requires a `name` attribute"); + } + if (!(nameUnchecked instanceof String)) { + throw Starlark.errorf( + "Expected a String for attribute 'name'; got %s", + nameUnchecked.getClass().getSimpleName()); + } + String instanceName = (String) nameUnchecked; + + MacroInstance macroInstance = new MacroInstance(macroClass, instanceName); + try { + pkgBuilder.addMacro(macroInstance); + } catch (NameConflictException e) { + throw new EvalException(e); + } + return Starlark.NONE; + } + + @Override + public void export(EventHandler handler, Label label, String exportedName) { + checkState(builder != null && macroClass == null); + builder.setName(exportedName); + this.macroClass = builder.build(); + this.builder = null; + } + + @Override + public boolean isExported() { + return macroClass != null; + } + + @Override + public void repr(Printer printer) { + if (isExported()) { + printer.append(""); + } else { + printer.append(""); + } + } + + @Override + public String toString() { + return "macro(...)"; + } + + @Override + public boolean isImmutable() { + // TODO(bazel-team): This seems technically wrong, analogous to + // StarlarkRuleFunction#isImmutable. + return true; + } + } + + /** + * A callable Starlark object representing a Starlark-defined rule, which may be invoked during + * package construction time to instantiate the rule. * - *

Exactly one of {@link #builder} or {@link #ruleClass} is null except inside {@link #export}. + *

This is the object returned by calling {@code rule()}, e.g. the value that is bound in + * {@code my_rule = rule(...)}}. */ public static final class StarlarkRuleFunction implements StarlarkExportable, RuleFunction { - private RuleClass.Builder builder; + // Initially non-null, then null once exported. + @Nullable private RuleClass.Builder builder; + + // Initially null, then non-null once exported. + @Nullable private RuleClass ruleClass; - private RuleClass ruleClass; private final Location definitionLocation; @Nullable private final String documentation; - private Label starlarkLabel; + + // Set upon export. + @Nullable private Label starlarkLabel; // TODO(adonovan): merge {Starlark,Builtin}RuleFunction and RuleClass, // making the latter a callable, StarlarkExportable value. // (Making RuleClasses first-class values will help us to build a // rich query output mode that includes values from loaded .bzl files.) + // [Note from brandjon: Even if we merge RuleFunction and RuleClass, it may still be useful to + // carry a distinction between loading-time vs analysis-time information about a rule type, + // particularly when it comes to the possibility of lazy .bzl loading. For example, you can in + // principle evaluate a BUILD file without loading and digesting .bzls that are only used by the + // implementation function.] public StarlarkRuleFunction( - RuleClass.Builder builder, - Location definitionLocation, - Optional documentation) { + RuleClass.Builder builder, Location definitionLocation, Optional documentation) { this.builder = builder; this.definitionLocation = definitionLocation; this.documentation = documentation.orElse(null); @@ -1245,6 +1380,7 @@ public String toString() { @Override public boolean isImmutable() { + // TODO(bazel-team): It shouldn't be immutable until it's exported, no? return true; } } diff --git a/src/main/java/com/google/devtools/build/lib/packages/MacroClass.java b/src/main/java/com/google/devtools/build/lib/packages/MacroClass.java new file mode 100644 index 00000000000000..f8866bf1926451 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/packages/MacroClass.java @@ -0,0 +1,68 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.devtools.build.lib.packages; + +import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import javax.annotation.Nullable; +import net.starlark.java.eval.StarlarkFunction; + +/** + * Represents a symbolic macro, defined in a .bzl file, that may be instantiated during Package + * evaluation. + * + *

This is analogous to {@link RuleClass}. In essence, a {@code MacroClass} consists of the + * macro's schema and its implementation function. + */ +public final class MacroClass { + + private final String name; + private final StarlarkFunction implementation; + + public MacroClass(String name, StarlarkFunction implementation) { + this.name = name; + this.implementation = implementation; + } + + /** Returns the macro's exported name. */ + public String getName() { + return name; + } + + public StarlarkFunction getImplementation() { + return implementation; + } + + /** Builder for {@link MacroClass}. */ + public static final class Builder { + private final StarlarkFunction implementation; + @Nullable private String name = null; + + public Builder(StarlarkFunction implementation) { + this.implementation = implementation; + } + + @CanIgnoreReturnValue + public Builder setName(String name) { + this.name = name; + return this; + } + + public MacroClass build() { + Preconditions.checkNotNull(name); + return new MacroClass(name, implementation); + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/packages/MacroInstance.java b/src/main/java/com/google/devtools/build/lib/packages/MacroInstance.java new file mode 100644 index 00000000000000..ab92f6c401abce --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/packages/MacroInstance.java @@ -0,0 +1,47 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.devtools.build.lib.packages; + +/** + * Represents a use of a symbolic macro in a package. + * + *

There is one {@code MacroInstance} for each call to a {@link + * StarlarkRuleClassFunctions#MacroFunction} that is executed during a package's evaluation. Just as + * a {@link MacroClass} is analogous to a {@link RuleClass}, {@code MacroInstance} is analogous to a + * {@link Rule} (i.e. a rule target). + */ +public final class MacroInstance { + + private final MacroClass macroClass; + private final String name; + + public MacroInstance(MacroClass macroClass, String name) { + this.macroClass = macroClass; + this.name = name; + } + + /** Returns the {@link MacroClass} (i.e. schema info) that this instance parameterizes. */ + public MacroClass getMacroClass() { + return macroClass; + } + + /** + * Returns the name of this instance, as given in the {@code name = ...} attribute in the calling + * BUILD file or macro. + */ + public String getName() { + return name; + } +} 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 2ef9ce0c445c9b..2b0d617bb56c05 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 @@ -64,6 +64,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -126,9 +127,23 @@ private NameConflictException(String message) { } } - /** The collection of all targets defined in this package, indexed by name. */ + /** + * The collection of all targets defined in this package, indexed by name. + * + *

Invariant: This is disjoint with the set of keys in {@link #macros}. + */ private ImmutableSortedMap targets; + /** + * The collection of all symbolic macro instances defined in this package, indexed by name. + * + *

Invariant: This is disjoint with the set of keys in {@link #targets}. + */ + // TODO(#19922): We'll have to strengthen this invariant. It's not just that nothing should share + // the same name as a macro, but also that nothing should be inside a macro's namespace (meaning, + // in the current design, having the macro as a prefix) unless it was defined by that macro. + private ImmutableSortedMap macros; + public PackageArgs getPackageArgs() { return metadata.packageArgs; } @@ -336,6 +351,7 @@ private void finishInit(Builder builder) { this.metadata.makeEnv = ImmutableMap.copyOf(builder.makeEnv); this.targets = ImmutableSortedMap.copyOf(builder.targets); + this.macros = ImmutableSortedMap.copyOf(builder.macros); this.failureDetail = builder.getFailureDetail(); this.registeredExecutionPlatforms = ImmutableList.copyOf(builder.registeredExecutionPlatforms); this.registeredToolchains = ImmutableList.copyOf(builder.registeredToolchains); @@ -655,6 +671,14 @@ private String getAlternateTargetSuggestion(String targetName) { } } + /** Returns all symbolic macros defined in the package. */ + // TODO(#19922): Clarify this comment to indicate whether the macros have already been expanded + // by the point the Package has been built. The answer's probably "yes". In that case, this + // accessor is still useful for introspecting e.g. by `bazel query`. + public ImmutableMap getMacros() { + return macros; + } + /** * How to enforce visibility on config_setting See {@link * ConfigSettingVisibilityPolicy} for details. @@ -886,11 +910,15 @@ default boolean precomputeTransitiveLoads() { // some tests. @Nullable private final Globber globber; + private final Map environmentGroups = new HashMap<>(); + // All targets added to the package. We use SnapshottableBiMap to help track insertion order of // Rule targets, for use by native.existing_rules(). private BiMap targets = new SnapshottableBiMap<>(target -> target instanceof Rule); - private final Map environmentGroups = new HashMap<>(); + + // All instances of symbolic macros created during package construction. + private final Map macros = new LinkedHashMap<>(); private enum NameConflictCheckingPolicy { UNKNOWN, @@ -1626,6 +1654,13 @@ void addRule(Rule rule) throws NameConflictException { ruleLabels.put(rule, labels); } + public void addMacro(MacroInstance macro) throws NameConflictException { + // TODO(#19922): Add name conflict checking! + macros.put(macro.getName(), macro); + // TODO(#19922): Push to a queue of unexpanded macros, read those when expanding macros as + // part of monolithic package evaluation (but not lazy macro evaluation). + } + void addRegisteredExecutionPlatforms(List platforms) { this.registeredExecutionPlatforms.addAll(platforms); } diff --git a/src/main/java/com/google/devtools/build/lib/packages/semantics/BuildLanguageOptions.java b/src/main/java/com/google/devtools/build/lib/packages/semantics/BuildLanguageOptions.java index 99eecda05ec574..658c5f5ec6e58d 100644 --- a/src/main/java/com/google/devtools/build/lib/packages/semantics/BuildLanguageOptions.java +++ b/src/main/java/com/google/devtools/build/lib/packages/semantics/BuildLanguageOptions.java @@ -185,6 +185,14 @@ public final class BuildLanguageOptions extends OptionsBase { help = "If set to true, enables the APIs required to support the Android Starlark migration.") public boolean experimentalEnableAndroidMigrationApis; + @Option( + name = "experimental_enable_first_class_macros", + defaultValue = "false", + documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS, + effectTags = OptionEffectTag.BUILD_FILE_SEMANTICS, + help = "If set to true, enables the `macro()` construct for defining first-class macros.") + public boolean experimentalEnableFirstClassMacros; + @Option( name = "experimental_enable_scl_dialect", defaultValue = "false", @@ -763,6 +771,7 @@ public StarlarkSemantics toStarlarkSemantics() { .setBool(CHECK_BZL_VISIBILITY, checkBzlVisibility) .setBool( EXPERIMENTAL_ENABLE_ANDROID_MIGRATION_APIS, experimentalEnableAndroidMigrationApis) + .setBool(EXPERIMENTAL_ENABLE_FIRST_CLASS_MACROS, experimentalEnableFirstClassMacros) .setBool(EXPERIMENTAL_ENABLE_SCL_DIALECT, experimentalEnableSclDialect) .setBool(ENABLE_BZLMOD, enableBzlmod) .setBool(ENABLE_WORKSPACE, enableWorkspace) @@ -872,6 +881,8 @@ public StarlarkSemantics toStarlarkSemantics() { "-experimental_disable_external_package"; public static final String EXPERIMENTAL_ENABLE_ANDROID_MIGRATION_APIS = "-experimental_enable_android_migration_apis"; + public static final String EXPERIMENTAL_ENABLE_FIRST_CLASS_MACROS = + "-experimental_enable_first_class_macros"; public static final String EXPERIMENTAL_ENABLE_SCL_DIALECT = "-experimental_enable_scl_dialect"; public static final String ENABLE_BZLMOD = "+enable_bzlmod"; public static final String ENABLE_WORKSPACE = "+enable_workspace"; diff --git a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/StarlarkRuleFunctionsApi.java b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/StarlarkRuleFunctionsApi.java index 1e402377349296..efad37499e5696 100644 --- a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/StarlarkRuleFunctionsApi.java +++ b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/StarlarkRuleFunctionsApi.java @@ -191,6 +191,22 @@ public interface StarlarkRuleFunctionsApi { Object provider(Object doc, Object fields, Object init, StarlarkThread thread) throws EvalException; + @StarlarkMethod( + name = "macro", + documented = false, // TODO(#19922): Document + parameters = { + @Param( + name = "implementation", + positional = false, + named = true, + documented = false // TODO(#19922): Document + ) + // TODO(#19922): Take attrs dict + }, + useStarlarkThread = true) + StarlarkCallable macro(StarlarkFunction implementation, StarlarkThread thread) + throws EvalException; + @StarlarkMethod( name = "rule", doc = diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStarlarkRuleFunctionsApi.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStarlarkRuleFunctionsApi.java index 074fc5dc8abaf4..acd62a02e23106 100644 --- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStarlarkRuleFunctionsApi.java +++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStarlarkRuleFunctionsApi.java @@ -128,6 +128,18 @@ public ProviderInfoWrapper forProviderInfo( return new ProviderInfoWrapper(identifier, docString, fieldInfos); } + @Override + public StarlarkCallable macro(StarlarkFunction implementation, StarlarkThread thread) { + // We don't support documenting symbolic macros -- at least not in legacy Stardoc. + // Return a dummy. + return new StarlarkCallable() { + @Override + public String getName() { + return "UNNAMED"; + } + }; + } + @Override public StarlarkCallable rule( StarlarkFunction implementation, diff --git a/src/test/java/com/google/devtools/build/docgen/StarlarkDocumentationTest.java b/src/test/java/com/google/devtools/build/docgen/StarlarkDocumentationTest.java index da9d5da88d56b3..cc1e1ade1ccf04 100644 --- a/src/test/java/com/google/devtools/build/docgen/StarlarkDocumentationTest.java +++ b/src/test/java/com/google/devtools/build/docgen/StarlarkDocumentationTest.java @@ -53,8 +53,9 @@ @RunWith(JUnit4.class) public class StarlarkDocumentationTest { - private static final ImmutableList DEPRECATED_UNDOCUMENTED_TOP_LEVEL_SYMBOLS = - ImmutableList.of("Actions"); + private static final ImmutableList + DEPRECATED_OR_EXPERIMENTAL_UNDOCUMENTED_TOP_LEVEL_SYMBOLS = + ImmutableList.of("Actions", "macro"); private static final StarlarkDocExpander expander = new StarlarkDocExpander(null) { @@ -92,7 +93,7 @@ private void checkStarlarkTopLevelEnvItemsAreDocumented(Map glob // If they need documentation, the easiest approach would be // to hard-code it in StarlarkDocumentationCollector. ImmutableSet.of("True", "False", "None"))) - .containsExactlyElementsIn(DEPRECATED_UNDOCUMENTED_TOP_LEVEL_SYMBOLS); + .containsExactlyElementsIn(DEPRECATED_OR_EXPERIMENTAL_UNDOCUMENTED_TOP_LEVEL_SYMBOLS); } // TODO(bazel-team): come up with better Starlark specific tests. diff --git a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleClassFunctionsTest.java b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleClassFunctionsTest.java index 5289051bc3a02b..27cb5d0d5dab0f 100644 --- a/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleClassFunctionsTest.java +++ b/src/test/java/com/google/devtools/build/lib/starlark/StarlarkRuleClassFunctionsTest.java @@ -36,6 +36,7 @@ import com.google.devtools.build.lib.analysis.starlark.StarlarkAttrModule; import com.google.devtools.build.lib.analysis.starlark.StarlarkConfig; import com.google.devtools.build.lib.analysis.starlark.StarlarkGlobalsImpl; +import com.google.devtools.build.lib.analysis.starlark.StarlarkRuleClassFunctions.MacroFunction; import com.google.devtools.build.lib.analysis.starlark.StarlarkRuleClassFunctions.StarlarkRuleFunction; import com.google.devtools.build.lib.analysis.starlark.StarlarkRuleContext; import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; @@ -57,6 +58,8 @@ import com.google.devtools.build.lib.packages.BuildType; import com.google.devtools.build.lib.packages.ExecGroup; import com.google.devtools.build.lib.packages.ImplicitOutputsFunction; +import com.google.devtools.build.lib.packages.NoSuchPackageException; +import com.google.devtools.build.lib.packages.Package; import com.google.devtools.build.lib.packages.PredicateWithMessage; import com.google.devtools.build.lib.packages.Provider; import com.google.devtools.build.lib.packages.RequiredProviders; @@ -77,6 +80,7 @@ import com.google.devtools.build.lib.testutil.TestRuleClassProvider; import com.google.devtools.build.lib.util.FileTypeSet; import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import java.io.IOException; @@ -103,6 +107,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; + /** Tests for StarlarkRuleClassFunctions. */ @RunWith(TestParameterInjector.class) public final class StarlarkRuleClassFunctionsTest extends BuildViewTestCase { @@ -206,6 +211,173 @@ public void testImplicitArgsAttribute() throws Exception { assertThat(getRuleClass("non_exec_rule").hasAttr("args", Type.STRING_LIST)).isFalse(); } + /** Returns a package by the given name (no leading "//"), or null if it was in error. */ + @CanIgnoreReturnValue + @Nullable + private Package getPackage(String pkgName) throws InterruptedException { + try { + return getPackageManager().getPackage(reporter, PackageIdentifier.createInMainRepo(pkgName)); + } catch (NoSuchPackageException unused) { + return null; + } + } + + @Test + public void testSymbolicMacro_failsWithoutFlag() throws Exception { + setBuildLanguageOptions("--experimental_enable_first_class_macros=false"); + + scratch.file( + "pkg/foo.bzl", // + "def _impl(name):", + " pass", + "my_macro = macro(implementation=_impl)"); + scratch.file( + "pkg/BUILD", // + "load(':foo.bzl', 'my_macro')"); + + reporter.removeHandler(failFastHandler); + Package pkg = getPackage("pkg"); + assertThat(pkg).isNull(); + assertContainsEvent("requires --experimental_enable_first_class_macros"); + } + + @Test + public void testSymbolicMacro_instantiationRegistersOnPackage() throws Exception { + setBuildLanguageOptions("--experimental_enable_first_class_macros"); + + scratch.file( + "pkg/foo.bzl", // + "def _impl(name):", + " pass", + "my_macro = macro(implementation=_impl)"); + scratch.file( + "pkg/BUILD", // + "load(':foo.bzl', 'my_macro')", + "my_macro(name='ghi')", // alphabetized when read back + "my_macro(name='abc')", + "my_macro(name='def')"); + + Package pkg = getPackage("pkg"); + assertThat(pkg.getMacros().keySet()).containsExactly("abc", "def", "ghi").inOrder(); + assertThat(pkg.getMacros().get("abc").getMacroClass().getName()).isEqualTo("my_macro"); + } + + @Test + public void testSymbolicMacro_instantiationRequiresExport() throws Exception { + setBuildLanguageOptions("--experimental_enable_first_class_macros"); + + scratch.file( + "pkg/foo.bzl", // + "def _impl(name):", + " pass", + "s = struct(m = macro(implementation=_impl))"); + scratch.file( + "pkg/BUILD", // + "load(':foo.bzl', 's')", + "s.m(name='abc')"); + + reporter.removeHandler(failFastHandler); + Package pkg = getPackage("pkg"); + assertThat(pkg).isNotNull(); + assertThat(pkg.containsErrors()).isTrue(); + assertContainsEvent("Cannot instantiate a macro that has not been exported"); + } + + @Test + public void testSymbolicMacro_cannotInstantiateInBzlThread() throws Exception { + setBuildLanguageOptions("--experimental_enable_first_class_macros"); + + scratch.file( + "pkg/foo.bzl", + "def _impl(name):", + " pass", + "my_macro = macro(implementation=_impl)", + "", + // Calling it from a function during .bzl load time is a little more interesting than + // calling it directly at the top level, since it forces us to check thread state rather + // than call stack state. + "def some_func():", + " my_macro(name='nope')", + "some_func()"); + scratch.file( + "pkg/BUILD", // + "load(':foo.bzl', 'my_macro')"); + + reporter.removeHandler(failFastHandler); + Package pkg = getPackage("pkg"); + assertThat(pkg).isNull(); + assertContainsEvent("Cannot instantiate a macro when loading a .bzl file"); + } + + @Test + public void testSymbolicMacro_requiresNameAttribute() throws Exception { + setBuildLanguageOptions("--experimental_enable_first_class_macros"); + + scratch.file( + "pkg/foo.bzl", // + "def _impl(name):", + " pass", + "my_macro = macro(implementation=_impl)"); + scratch.file( + "pkg/BUILD", // + "load(':foo.bzl', 'my_macro')", + "my_macro()"); + + reporter.removeHandler(failFastHandler); + Package pkg = getPackage("pkg"); + assertThat(pkg).isNotNull(); + assertThat(pkg.containsErrors()).isTrue(); + assertContainsEvent("macro requires a `name` attribute"); + } + + @Test + public void testSymbolicMacro_prohibitsPositionalArgs() throws Exception { + setBuildLanguageOptions("--experimental_enable_first_class_macros"); + + scratch.file( + "pkg/foo.bzl", // + "def _impl(name):", + " pass", + "my_macro = macro(implementation=_impl)"); + scratch.file( + "pkg/BUILD", // + "load(':foo.bzl', 'my_macro')", + "my_macro('a positional arg', name = 'abc')"); + + reporter.removeHandler(failFastHandler); + Package pkg = getPackage("pkg"); + assertThat(pkg).isNotNull(); + assertThat(pkg.containsErrors()).isTrue(); + assertContainsEvent("unexpected positional arguments"); + } + + @Test + public void testSymbolicMacro_macroFunctionApi() throws Exception { + ev.setSemantics("--experimental_enable_first_class_macros"); + + evalAndExport( + ev, // + "def _impl(name):", + " pass", + "exported = macro(implementation=_impl)", + "s = struct(unexported = macro(implementation=_impl))"); + + MacroFunction exported = (MacroFunction) ev.lookup("exported"); + MacroFunction unexported = (MacroFunction) ev.eval("s.unexported"); + + assertThat(exported.getName()).isEqualTo("exported"); + assertThat(unexported.getName()).isEqualTo("unexported macro"); + + assertThat(exported.isExported()).isTrue(); + assertThat(unexported.isExported()).isFalse(); + + assertThat(ev.eval("repr(exported)")).isEqualTo(""); + assertThat(ev.eval("repr(s.unexported)")).isEqualTo(""); + } + + // TODO(#19922): Add assertions for calling convention and execution of macro implementation + // function. + private RuleClass getRuleClass(String name) throws Exception { return ((StarlarkRuleFunction) ev.lookup(name)).getRuleClass(); }