1# This file is licensed under the Apache License v2.0 with LLVM Exceptions.
2# See https://llvm.org/LICENSE.txt for license information.
3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4"""BUILD extensions for MLIR table generation."""
5
6load("@bazel_skylib//lib:paths.bzl", "paths")
7
8TdInfo = provider(
9    "Holds TableGen files and the dependencies and include paths necessary to" +
10    " build them.",
11    fields = {
12        "transitive_sources": "td files transitively used by this rule.",
13        "transitive_includes": (
14            "include arguments to add to the final TableGen invocation. These" +
15            " are the absolute directory paths that will be added with '-I'."
16        ),
17    },
18)
19
20# For now we allow anything that provides DefaultInfo to just forward its files.
21# In particular, this allows filegroups to be used. This is mostly to ease
22# transition. In the future, the TdInfo provider will be required.
23# TODO(gcmn): Switch to enforcing TdInfo provider.
24def _get_dep_transitive_srcs(dep):
25    """Extract TdInfo.transitive_sources, falling back to DefaultInfo.files."""
26    if TdInfo in dep:
27        return dep[TdInfo].transitive_sources
28    return dep[DefaultInfo].files
29
30def _get_dep_transitive_includes(dep):
31    """Extract TdInfo.transitive_includes, falling back to an empty depset()."""
32    if TdInfo in dep:
33        return dep[TdInfo].transitive_includes
34    return depset()
35
36def _get_transitive_srcs(srcs, deps):
37    """Obtain the source files for a target and its transitive dependencies.
38
39    Args:
40      srcs: a list of source files
41      deps: a list of targets that are direct dependencies
42    Returns:
43      a collection of the transitive sources
44    """
45    return depset(
46        direct = srcs,
47        transitive = [_get_dep_transitive_srcs(dep) for dep in deps],
48    )
49
50def _get_transitive_includes(includes, deps):
51    """Obtain the includes paths for a target and its transitive dependencies.
52
53    Args:
54      includes: a list of include paths
55      deps: a list of targets that are direct dependencies
56    Returns:
57      a collection of the transitive include paths
58    """
59    return depset(
60        direct = includes,
61        transitive = [_get_dep_transitive_includes(dep) for dep in deps],
62    )
63
64def _prefix_roots(ctx, includes):
65    """Map the given includes to be relative to all root directories.
66
67    This will expand them to be relative to all the root directories available
68    in the execution environment for ctx.run (bin and genfiles in addition to
69    the normal source root)
70    """
71    prefixed_includes = []
72    for include in includes:
73        prefixed_includes.append(include)
74        prefixed_includes.append(paths.join(ctx.genfiles_dir.path, include))
75        prefixed_includes.append(paths.join(ctx.bin_dir.path, include))
76    return prefixed_includes
77
78def _resolve_includes(ctx, includes):
79    """Resolves include paths to paths relative to the execution root.
80
81    Relative paths are interpreted as relative to the current label's package.
82    Absolute paths are interpreted as relative to the current label's workspace
83    root."""
84    package = ctx.label.package
85    workspace_root = ctx.label.workspace_root
86    workspace_root = workspace_root if workspace_root else "."
87    resolved_includes = []
88    for include in includes:
89        if paths.is_absolute(include):
90            include = include.lstrip("/")
91        else:
92            include = paths.join(package, include)
93        include = paths.join(workspace_root, include)
94        resolved_includes.extend(_prefix_roots(ctx, [include]))
95    return resolved_includes
96
97def _td_library_impl(ctx):
98    trans_srcs = _get_transitive_srcs(ctx.files.srcs, ctx.attr.deps)
99    trans_includes = _get_transitive_includes(
100        _resolve_includes(ctx, ctx.attr.includes),
101        ctx.attr.deps,
102    )
103
104    # Note that we include srcs in runfiles. A td_library doesn't compile to
105    # produce an output: it's just a depset of source files and include
106    # directories. So if it is needed for execution of some rule (likely
107    # something running tblgen as a test action), the files needed are the same
108    # as the source files.
109    # Note: not using merge_all, as that is not available in Bazel 4.0
110    runfiles = ctx.runfiles(ctx.files.srcs)
111    for src in ctx.attr.srcs:
112        runfiles = runfiles.merge(src[DefaultInfo].default_runfiles)
113    for dep in ctx.attr.deps:
114        runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
115
116    return [
117        DefaultInfo(files = trans_srcs, runfiles = runfiles),
118        TdInfo(
119            transitive_sources = trans_srcs,
120            transitive_includes = trans_includes,
121        ),
122    ]
123
124td_library = rule(
125    _td_library_impl,
126    attrs = {
127        "srcs": attr.label_list(allow_files = True),
128        "includes": attr.string_list(
129            doc = "Include paths to be added to the final TableGen tool" +
130                  " invocation. Relative paths are interpreted as relative to" +
131                  " the current label's package. Absolute paths are" +
132                  " interpreted as relative to the current label's workspace",
133        ),
134        # TODO(gcmn): limit to TdInfo providers.
135        "deps": attr.label_list(
136            doc = "Dependencies providing TableGen source files and include" +
137                  " paths.",
138        ),
139    },
140)
141
142def _gentbl_rule_impl(ctx):
143    td_file = ctx.file.td_file
144
145    trans_srcs = _get_transitive_srcs(
146        ctx.files.td_srcs + [td_file],
147        ctx.attr.deps,
148    )
149
150    # Note that the td_file.dirname is already relative to the execution root,
151    # i.e. may contain an `external/<workspace_name>` prefix if the current
152    # workspace is not the main workspace. Therefore it is not included in the
153    # _resolve_includes call that prepends this prefix.
154    trans_includes = _get_transitive_includes(
155        _resolve_includes(ctx, ctx.attr.includes + ["/"]) +
156        _prefix_roots(ctx, [td_file.dirname]),
157        ctx.attr.deps,
158    )
159
160    args = ctx.actions.args()
161    args.add_all(ctx.attr.opts)
162    args.add(td_file)
163    args.add_all(trans_includes, before_each = "-I")
164
165    args.add("-o", ctx.outputs.out.path)
166
167    ctx.actions.run(
168        outputs = [ctx.outputs.out],
169        inputs = trans_srcs,
170        executable = ctx.executable.tblgen,
171        arguments = [args],
172        # Make sure action_env settings are honored so the env is the same as
173        # when the tool was built. Important for locating shared libraries with
174        # a custom LD_LIBRARY_PATH.
175        use_default_shell_env = True,
176        mnemonic = "TdGenerate",
177    )
178
179    return [DefaultInfo()]
180
181gentbl_rule = rule(
182    _gentbl_rule_impl,
183    doc = "Generates tabular code from a table definition file.",
184    # Match genrule behavior
185    output_to_genfiles = True,
186    attrs = {
187        "tblgen": attr.label(
188            doc = "The TableGen executable with which to generate `out`.",
189            executable = True,
190            cfg = "exec",
191        ),
192        "td_file": attr.label(
193            doc = "The TableGen file to run through `tblgen`.",
194            allow_single_file = True,
195            mandatory = True,
196        ),
197        "td_srcs": attr.label_list(
198            doc = "Additional TableGen files included by `td_file`. It is not" +
199                  " necessary to list td_file here (though not an error).",
200            allow_files = True,
201        ),
202        # TODO(gcmn): limit to TdInfo providers.
203        "deps": attr.label_list(
204            doc = "Dependencies providing TableGen source files and include" +
205                  " paths.",
206        ),
207        "out": attr.output(
208            doc = "The output file for the TableGen invocation.",
209            mandatory = True,
210        ),
211        "opts": attr.string_list(
212            doc = "Additional command line options to add to the TableGen" +
213                  " invocation. For include arguments, prefer to use" +
214                  " `includes`.",
215        ),
216        "includes": attr.string_list(
217            doc = "Include paths to be added to the final TableGen tool" +
218                  " invocation. Relative paths are interpreted as relative to" +
219                  " the current label's package. Absolute paths are" +
220                  " interpreted as relative to the current label's workspace." +
221                  " Includes are applied from all roots available in the" +
222                  " execution environment (source, genfiles, and bin" +
223                  " directories). The execution roots themselves and the " +
224                  " directory of td_file are always added.",
225        ),
226    },
227)
228
229# TODO(gcmn): Figure out how to reduce duplication with _gentbl_rule_impl
230def _gentbl_test_impl(ctx):
231    td_file = ctx.file.td_file
232
233    # Note that the td_file.dirname is already relative to the execution root,
234    # i.e. may contain an `external/<workspace_name>` prefix if the current
235    # workspace is not the main workspace. Therefore it is not included in the
236    # _resolve_includes call that prepends this prefix.
237    trans_includes = _get_transitive_includes(
238        _resolve_includes(ctx, ctx.attr.includes + ["/"]) +
239        _prefix_roots(ctx, [td_file.dirname]),
240        ctx.attr.deps,
241    )
242
243    test_args = [ctx.executable.tblgen.short_path]
244    test_args.extend(ctx.attr.opts)
245    test_args.append(td_file.path)
246    test_args.extend(["-I " + include for include in trans_includes.to_list()])
247
248    test_args.extend(["-o", "/dev/null"])
249
250    ctx.actions.write(
251        ctx.outputs.executable,
252        content = " ".join(test_args),
253        is_executable = True,
254    )
255
256    # Note: not using merge_all, as that is not available in Bazel 4.0
257    runfiles = ctx.runfiles(
258        files = [ctx.executable.tblgen],
259        transitive_files = _get_transitive_srcs(
260            ctx.files.td_srcs + [td_file],
261            ctx.attr.deps,
262        ),
263    )
264    for src in ctx.attr.td_srcs:
265        runfiles = runfiles.merge(src[DefaultInfo].default_runfiles)
266    for dep in ctx.attr.deps:
267        runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
268
269    return [
270        coverage_common.instrumented_files_info(
271            ctx,
272            source_attributes = ["td_file", "td_srcs"],
273            dependency_attributes = ["tblgen", "deps"],
274        ),
275        DefaultInfo(runfiles = runfiles),
276    ]
277
278gentbl_test = rule(
279    _gentbl_test_impl,
280    test = True,
281    doc = "A shell test that tests the given TablegGen invocation. Note" +
282          " that unlike gentbl_rule, this builds and invokes `tblgen` in the" +
283          " target configuration. Takes all the same arguments as gentbl_rule" +
284          " except for `out` (as it does not generate any output)",
285    attrs = {
286        "tblgen": attr.label(
287            doc = "The TableGen executable run in the shell command. Note" +
288                  " that this is built in the target configuration.",
289            executable = True,
290            cfg = "target",
291        ),
292        "td_file": attr.label(
293            doc = "See gentbl_rule.td_file",
294            allow_single_file = True,
295            mandatory = True,
296        ),
297        "td_srcs": attr.label_list(
298            doc = "See gentbl_rule.td_srcs",
299            allow_files = True,
300        ),
301        "deps": attr.label_list(doc = "See gentbl_rule.deps"),
302        "opts": attr.string_list(doc = "See gentbl_rule.opts"),
303        "includes": attr.string_list(doc = "See gentbl_rule.includes"),
304    },
305)
306
307def gentbl_filegroup(
308        name,
309        tblgen,
310        td_file,
311        tbl_outs,
312        td_srcs = [],
313        includes = [],
314        deps = [],
315        test = False,
316        skip_opts = [],
317        **kwargs):
318    """Create multiple TableGen generated files using the same tool and input.
319
320    All generated outputs are bundled in a file group with the given name.
321
322    Args:
323      name: The name of the generated filegroup rule for use in dependencies.
324      tblgen: The binary used to produce the output.
325      td_file: The primary table definitions file.
326      tbl_outs: A list of tuples ([opts], out), where each 'opts' is a list of
327        options passed to tblgen, each option being a string, and 'out' is the
328        corresponding output file produced.
329      td_srcs: See gentbl_rule.td_srcs
330      includes: See gentbl_rule.includes
331      deps: See gentbl_rule.deps
332      test: Whether to create a shell test that invokes the tool too.
333      skip_opts: Files generated using these opts in tbl_outs will be excluded
334        from the generated filegroup.
335      **kwargs: Extra keyword arguments to pass to all generated rules.
336    """
337
338    for (opts, out) in tbl_outs:
339        first_opt = opts[0] if opts else ""
340        rule_suffix = "_{}_{}".format(
341            first_opt.replace("-", "_").replace("=", "_"),
342            str(hash(" ".join(opts))),
343        )
344        gentbl_name = "%s_%s_genrule" % (name, rule_suffix)
345        gentbl_rule(
346            name = gentbl_name,
347            td_file = td_file,
348            tblgen = tblgen,
349            opts = opts,
350            td_srcs = td_srcs,
351            deps = deps,
352            includes = includes,
353            out = out,
354            **kwargs
355        )
356
357        if test:
358            # Also run the generator in the target configuration as a test. This
359            # means it gets run with asserts and sanitizers and such when they
360            # are enabled and is counted in coverage.
361            gentbl_test(
362                name = "%s_test" % (gentbl_name,),
363                td_file = td_file,
364                tblgen = tblgen,
365                opts = opts,
366                td_srcs = td_srcs,
367                deps = deps,
368                includes = includes,
369                # Shell files not executable on Windows.
370                # TODO(gcmn): Support windows.
371                tags = ["no_windows"],
372                **kwargs
373            )
374
375    included_srcs = [f for (opts, f) in tbl_outs if not any([skip_opt in opts for skip_opt in skip_opts])]
376    native.filegroup(
377        name = name,
378        srcs = included_srcs,
379        **kwargs
380    )
381
382def gentbl_cc_library(
383        name,
384        tblgen,
385        td_file,
386        tbl_outs,
387        td_srcs = [],
388        includes = [],
389        deps = [],
390        strip_include_prefix = None,
391        test = False,
392        **kwargs):
393    """Create multiple TableGen generated files using the same tool and input.
394
395    All generated outputs are bundled in a cc_library rule.
396
397    Args:
398      name: The name of the generated cc_library rule for use in dependencies.
399      tblgen: The binary used to produce the output.
400      td_file: The primary table definitions file.
401      tbl_outs: A list of tuples ([opts], out), where each 'opts' is a list of
402        options passed to tblgen, each option being a string, and 'out' is the
403        corresponding output file produced.
404      td_srcs: See gentbl_rule.td_srcs
405      includes: See gentbl_rule.includes
406      deps: See gentbl_rule.deps
407      strip_include_prefix: attribute to pass through to cc_library.
408      test: whether to create a shell test that invokes the tool too.
409      **kwargs: Extra keyword arguments to pass to all generated rules.
410    """
411
412    filegroup_name = name + "_filegroup"
413    gentbl_filegroup(
414        name = filegroup_name,
415        tblgen = tblgen,
416        td_file = td_file,
417        tbl_outs = tbl_outs,
418        td_srcs = td_srcs,
419        includes = includes,
420        deps = deps,
421        test = test,
422        skip_opts = ["-gen-op-doc"],
423        **kwargs
424    )
425    native.cc_library(
426        name = name,
427        # strip_include_prefix does not apply to textual_hdrs.
428        # https://github.com/bazelbuild/bazel/issues/12424
429        hdrs = [":" + filegroup_name] if strip_include_prefix else [],
430        strip_include_prefix = strip_include_prefix,
431        textual_hdrs = [":" + filegroup_name],
432        **kwargs
433    )
434