Update tblgen.bzl
diff --git a/build_tools/bazel/tblgen.bzl b/build_tools/bazel/tblgen.bzl
index df15b02..75a5cec 100644
--- a/build_tools/bazel/tblgen.bzl
+++ b/build_tools/bazel/tblgen.bzl
@@ -14,88 +14,485 @@
 
 """BUILD extensions for MLIR table generation."""
 
-def gentbl(name, tblgen, td_file, tbl_outs, td_srcs = [], td_includes = [], strip_include_prefix = None, test = False):
-    """gentbl() generates tabular code from a table definition file.
+TdInfo = provider(
+    "Holds TableGen files and the dependencies and include paths necessary to" +
+    " build them.",
+    fields = {
+        "transitive_sources": "td files transitively used by this rule.",
+        "transitive_includes": (
+            "include arguments to add to the final TableGen invocation. These" +
+            " are the absolute directory paths that will be added with '-I'."
+        ),
+    },
+)
+
+# For now we allow anything that provides DefaultInfo to just forward its files.
+# In particular, this allows filegroups to be used. This is mostly to ease
+# transition. In the future, the TdInfo provider will be required.
+# TODO(gcmn): Switch to enforcing TdInfo provider.
+def _get_dep_transitive_srcs(dep):
+    """Extract TdInfo.transitive_sources, falling back to DefaultInfo.files."""
+    if TdInfo in dep:
+        return dep[TdInfo].transitive_sources
+    return dep[DefaultInfo].files
+
+def _get_dep_transitive_includes(dep):
+    """Extract TdInfo.transitive_includes, falling back to an empty depset()."""
+    if TdInfo in dep:
+        return dep[TdInfo].transitive_includes
+    return depset()
+
+def _get_transitive_srcs(srcs, deps):
+    """Obtain the source files for a target and its transitive dependencies.
 
     Args:
-      name: The name of the build rule for use in dependencies.
+      srcs: a list of source files
+      deps: a list of targets that are direct dependencies
+    Returns:
+      a collection of the transitive sources
+    """
+    return depset(
+        direct = srcs,
+        transitive = [_get_dep_transitive_srcs(dep) for dep in deps],
+    )
+
+def _get_transitive_includes(includes, deps):
+    """Obtain the includes paths for a target and its transitive dependencies.
+
+    Args:
+      includes: a list of include paths
+      deps: a list of targets that are direct dependencies
+    Returns:
+      a collection of the transitive include paths
+    """
+    return depset(
+        direct = includes,
+        transitive = [_get_dep_transitive_includes(dep) for dep in deps],
+    )
+
+def _prefix_roots(ctx, includes):
+    """Map the given includes to be relative to all root directories.
+
+    This will expand them to be relative to all the root directories available
+    in the execution environment for ctx.run (bin and genfiles in addition to
+    the normal source root)
+    """
+    prefixed_includes = []
+    for include in includes:
+        prefixed_includes.append(include)
+        prefixed_includes.append(ctx.genfiles_dir.path + "/" + include)
+        prefixed_includes.append(ctx.bin_dir.path + "/" + include)
+    return prefixed_includes
+
+def _resolve_includes(ctx, includes):
+    """Resolves include paths to paths relative to the execution root.
+
+    Relative paths are interpreted as relative to the current label's package.
+    Absolute paths are interpreted as relative to the current label's workspace
+    root."""
+    package = ctx.label.package
+    workspace_root = ctx.label.workspace_root
+    workspace_root = workspace_root if workspace_root else "."
+    resolved_includes = []
+    for include in includes:
+        if not include.startswith("/"):
+            include = "/" + package + "/" + include
+        include = workspace_root + include
+        resolved_includes.extend(_prefix_roots(ctx, [include]))
+    return resolved_includes
+
+def _td_library_impl(ctx):
+    trans_srcs = _get_transitive_srcs(ctx.files.srcs, ctx.attr.deps)
+    trans_includes = _get_transitive_includes(
+        _resolve_includes(ctx, ctx.attr.includes),
+        ctx.attr.deps,
+    )
+    return [
+        DefaultInfo(files = trans_srcs),
+        TdInfo(
+            transitive_sources = trans_srcs,
+            transitive_includes = trans_includes,
+        ),
+    ]
+
+td_library = rule(
+    _td_library_impl,
+    attrs = {
+        "srcs": attr.label_list(allow_files = True),
+        "includes": attr.string_list(
+            doc = "Include paths to be added to the final TableGen tool" +
+                  " invocation. Relative paths are interpreted as relative to" +
+                  " the current label's package. Absolute paths are" +
+                  " interpreted as relative to the current label's workspace",
+        ),
+        # TODO(gcmn): limit to TdInfo providers.
+        "deps": attr.label_list(
+            doc = "Dependencies providing TableGen source files and include" +
+                  " paths.",
+        ),
+    },
+)
+
+def _gentbl_rule_impl(ctx):
+    td_file = ctx.file.td_file
+
+    trans_srcs = _get_transitive_srcs(
+        ctx.files.td_srcs + [td_file],
+        ctx.attr.deps,
+    )
+
+    # Note that we have two types of includes here. The deprecated ones expanded
+    # only by "_prefix_roots" are already relative to the execution root, i.e.
+    # may contain an `external/<workspace_name>` prefix if the current workspace
+    # is not the main workspace (where workspace_name is something configured
+    # per-project and therefore generally not known). Note that dirname also
+    # already includes this prefix. The new style of includes have it prepended
+    # automatically by `_resolve_includes` to avoid BUILD files having to depend
+    # on project specific configurations and Bazel implementation details.
+    trans_includes = _get_transitive_includes(
+        _resolve_includes(ctx, ctx.attr.includes + ["/"]) +
+        _prefix_roots(ctx, ctx.attr.td_includes + [td_file.dirname]),
+        ctx.attr.deps,
+    )
+
+    args = ctx.actions.args()
+    args.add_all(ctx.attr.opts)
+    args.add(td_file)
+    args.add_all(trans_includes, before_each = "-I")
+
+    args.add("-o", ctx.outputs.out.path)
+
+    ctx.actions.run(
+        outputs = [ctx.outputs.out],
+        inputs = trans_srcs,
+        executable = ctx.executable.tblgen,
+        arguments = [args],
+        mnemonic = "TdGenerate",
+    )
+
+    return [DefaultInfo()]
+
+gentbl_rule = rule(
+    _gentbl_rule_impl,
+    doc = "Generates tabular code from a table definition file.",
+    # Match genrule behavior
+    output_to_genfiles = True,
+    attrs = {
+        "tblgen": attr.label(
+            doc = "The TableGen executable with which to generate `out`.",
+            executable = True,
+            cfg = "exec",
+        ),
+        "td_file": attr.label(
+            doc = "The TableGen file to run through `tblgen`.",
+            allow_single_file = True,
+            mandatory = True,
+        ),
+        "td_srcs": attr.label_list(
+            doc = "Additional TableGen files included by `td_file`. It is not" +
+                  " necessary to list td_file here (though not an error).",
+            allow_files = True,
+        ),
+        # TODO(gcmn): limit to TdInfo providers.
+        "deps": attr.label_list(
+            doc = "Dependencies providing TableGen source files and include" +
+                  " paths.",
+        ),
+        "out": attr.output(
+            doc = "The output file for the TableGen invocation.",
+            mandatory = True,
+        ),
+        "opts": attr.string_list(
+            doc = "Additional command line options to add to the TableGen" +
+                  " invocation. For include arguments, prefer to use" +
+                  " `includes`.",
+        ),
+        "includes": attr.string_list(
+            doc = "Include paths to be added to the final TableGen tool" +
+                  " invocation. Relative paths are interpreted as relative to" +
+                  " the current label's package. Absolute paths are" +
+                  " interpreted as relative to the current label's workspace." +
+                  " Includes are applied from all roots available in the" +
+                  " execution environment (source, genfiles, and bin" +
+                  " directories). The execution roots themselves and the " +
+                  " directory of td_file are always added.",
+        ),
+        "td_includes": attr.string_list(
+            doc = "Include paths to add to the TableGen invocation. Paths are" +
+                  " interpreted as relative to the current label's workspace" +
+                  " root and applied from all roots available in the" +
+                  " execution environment (source, genfiles, and bin" +
+                  " directories). Deprecated. Use `includes` instead.",
+        ),
+    },
+)
+
+# TODO(gcmn): Figure out how to reduce duplication with _gentbl_rule_impl
+def _gentbl_test_impl(ctx):
+    td_file = ctx.file.td_file
+
+    trans_srcs = _get_transitive_srcs(
+        ctx.files.td_srcs + [td_file],
+        ctx.attr.deps,
+    )
+
+    # Note that we have two types of includes here. The deprecated ones expanded
+    # only by "_prefix_roots" are already relative to the execution root, i.e.
+    # may contain an `external/<workspace_name>` prefix if the current workspace
+    # is not the main workspace (where workspace_name is something configured
+    # per-project and therefore generally not known). Note that dirname also
+    # already includes this prefix. The new style of includes have it prepended
+    # automatically by `_resolve_includes` to avoid BUILD files having to depend
+    # on project specific configurations and Bazel implementation details.
+    trans_includes = _get_transitive_includes(
+        _resolve_includes(ctx, ctx.attr.includes + ["/"]) +
+        _prefix_roots(ctx, ctx.attr.td_includes + [td_file.dirname]),
+        ctx.attr.deps,
+    )
+
+    test_args = [ctx.executable.tblgen.short_path]
+    test_args.extend(ctx.attr.opts)
+    test_args.append(td_file.path)
+    test_args.extend(["-I " + include for include in trans_includes.to_list()])
+
+    test_args.extend(["-o", "/dev/null"])
+
+    ctx.actions.write(
+        ctx.outputs.executable,
+        content = " ".join(test_args),
+        is_executable = True,
+    )
+
+    return [DefaultInfo(
+        runfiles = ctx.runfiles(
+            [ctx.executable.tblgen],
+            transitive_files = trans_srcs,
+        ),
+    )]
+
+gentbl_test = rule(
+    _gentbl_test_impl,
+    test = True,
+    doc = "A shell test that tests the given TablegGen invocation. Note" +
+          " that unlike gentbl_rule, this builds and invokes `tblgen` in the" +
+          " target configuration. Takes all the same arguments as gentbl_rule" +
+          " except for `out` (as it does not generate any output)",
+    # Match genrule behavior
+    output_to_genfiles = True,
+    attrs = {
+        "tblgen": attr.label(
+            doc = "The TableGen executable run in the shell command. Note" +
+                  " that this is built in the target configuration.",
+            executable = True,
+            cfg = "target",
+        ),
+        "td_file": attr.label(
+            doc = "See gentbl_rule.td_file",
+            allow_single_file = True,
+            mandatory = True,
+        ),
+        "td_srcs": attr.label_list(
+            doc = "See gentbl_rule.td_srcs",
+            allow_files = True,
+        ),
+        "deps": attr.label_list(doc = "See gentbl_rule.deps"),
+        "opts": attr.string_list(doc = "See gentbl_rule.opts"),
+        "includes": attr.string_list(doc = "See gentbl_rule.includes"),
+        "td_includes": attr.string_list(doc = "See gentbl_rule.td_includes"),
+    },
+)
+
+def gentbl_filegroup(
+        name,
+        tblgen,
+        td_file,
+        tbl_outs,
+        td_srcs = [],
+        td_includes = [],
+        includes = [],
+        deps = [],
+        test = False,
+        skip_opts = [],
+        **kwargs):
+    """Create multiple TableGen generated files using the same tool and input.
+
+    All generated outputs are bundled in a file group with the given name.
+
+    Args:
+      name: The name of the generated filegroup rule for use in dependencies.
       tblgen: The binary used to produce the output.
       td_file: The primary table definitions file.
-      tbl_outs: A list of tuples (opts, out), where each opts is a string of
-        options passed to tblgen, and the out is the corresponding output file
-        produced.
-      td_srcs: A list of table definition files included transitively.
-      td_includes: A list of include paths for relative includes.
-      strip_include_prefix: attribute to pass through to cc_library.
-      test: whether to create a test to invoke the tool too.
+      tbl_outs: A list of tuples ([opts], out), where each 'opts' is a list of
+        options passed to tblgen, each option being a string, and 'out' is the
+        corresponding output file produced.
+      td_srcs: See gentbl_rule.td_srcs
+      includes: See gentbl_rule.includes
+      td_includes: See gentbl_rule.td_includes
+      deps: See gentbl_rule.deps
+      test: Whether to create a shell test that invokes the tool too.
+      skip_opts: Files generated using these opts in tbl_outs will be excluded
+        from the generated filegroup.
+      **kwargs: Extra keyword arguments to pass to all generated rules.
     """
-    srcs = []
-    srcs += td_srcs
-    if td_file not in td_srcs:
-        srcs.append(td_file)
 
-    # TODO(gcmn): Every use of "external" in tblgen rules is a hack that depends
-    # on Bazel implementation details and the specific names of Bazel repository
-    # aliases.
-    td_includes_cmd = [
-        # Allow including MLIR td files, including generated ones
-        "-I external/llvm-project/mlir/include",
-        "-I $(GENDIR)/external/llvm-project/mlir/include",
-        # Allow IREE to be used as an external repository, but *only* if the
-        # Bazel repository alias is literally "iree".
-        "-I external/iree",
+    # TODO(gcmn): Transition to td_library, fix TF, and drop all hardcoded
+    # includes.
+    llvm_project_execroot_path = Label("@llvm-project//:fake", relative_to_caller_repository = False).workspace_root
+    tensorflow_project_execroot_path = Label("@llvm-project//:fake", relative_to_caller_repository = False).workspace_root
+    # Allow IREE to be used as an external repository.
+    iree_project_execroot_path = Label("//:fake", relative_to_caller_repository = False).workspace_root
+
+    hardcoded_includes = [
+        "%s/mlir/include" % llvm_project_execroot_path,
+        tensorflow_project_execroot_path,
+        iree_project_execroot_path,
     ]
-    for td_include in td_includes:
-        td_includes_cmd.append("-I%s" % td_include)
-    local_inc = "-I $$(dirname $(location %s))" % td_file
-
-    if test:
-        # Rule to generate shell script to invoke tblgen. This generates a very
-        # bare shell file which the sh_test uses.
-        native.genrule(
-            name = "%s_genrule_sh" % name,
-            srcs = srcs,
-            outs = ["%s.gen.sh" % name],
-            cmd = ("echo \"\\$$1\" %s \\$${@:2} -o /dev/null > $@" % local_inc),
-            executable = 1,
-        )
 
     for (opts, out) in tbl_outs:
-        # All arguments to generate the output except output destination.
-        base_args = [
-            "$(location %s)" % tblgen,
-            "%s" % opts,
-            "$(location %s)" % td_file,
-            "-I$(GENDIR)",
-        ] + td_includes_cmd
-        rule_suffix = "_".join(opts.replace("-", "_").replace("=", "_").split(" "))
-
-        # Rule to generate code using generated shell script.
-        native.genrule(
-            name = "%s_%s_genrule" % (name, rule_suffix),
-            srcs = srcs,
-            outs = [out],
-            tools = [tblgen],
-            message = "Generating code from table: %s" % td_file,
-            cmd = (" ".join(base_args) + " %s -o $@" % local_inc),
+        first_opt = opts[0] if opts else ""
+        rule_suffix = "_{}_{}".format(
+            first_opt.replace("-", "_").replace("=", "_"),
+            str(hash(" ".join(opts))),
+        )
+        gentbl_name = "%s_%s_genrule" % (name, rule_suffix)
+        gentbl_rule(
+            name = gentbl_name,
+            td_file = td_file,
+            tblgen = tblgen,
+            opts = opts,
+            td_srcs = td_srcs,
+            deps = deps,
+            includes = includes,
+            td_includes = td_includes + hardcoded_includes,
+            out = out,
+            **kwargs
         )
 
-        # Optionally generate rule to test tblgen invocation.
         if test:
-            native.sh_test(
-                name = "%s_%s_genrule_test" % (name, rule_suffix),
-                srcs = ["%s.gen.sh" % name],
-                args = base_args,
-                data = srcs + [tblgen],
+            # Also run the generator in the target configuration as a test. This
+            # means it gets run with asserts and sanitizers and such when they
+            # are enabled and is counted in coverage.
+            gentbl_test(
+                name = "%s_test" % (gentbl_name,),
+                td_file = td_file,
+                tblgen = tblgen,
+                opts = opts,
+                td_srcs = td_srcs,
+                deps = deps,
+                includes = includes,
+                td_includes = td_includes + hardcoded_includes,
+                # Shell files not executable on Windows.
+                # TODO(gcmn): Support windows.
+                tags = ["no_windows"],
+                **kwargs
             )
 
-    # List of opts that do not generate cc files.
-    skip_opts = ["-gen-op-doc"]
-    hdrs = [f for (opts, f) in tbl_outs if opts not in skip_opts]
+    included_srcs = [f for (opts, f) in tbl_outs if not any([skip_opt in opts for skip_opt in skip_opts])]
+    native.filegroup(
+        name = name,
+        srcs = included_srcs,
+        **kwargs
+    )
+
+def gentbl_cc_library(
+        name,
+        tblgen,
+        td_file,
+        tbl_outs,
+        td_srcs = [],
+        td_includes = [],
+        includes = [],
+        td_relative_includes = [],
+        deps = [],
+        strip_include_prefix = None,
+        test = False,
+        **kwargs):
+    """Create multiple TableGen generated files using the same tool and input.
+
+    All generated outputs are bundled in a cc_library rule.
+
+    Args:
+      name: The name of the generated cc_library rule for use in dependencies.
+      tblgen: The binary used to produce the output.
+      td_file: The primary table definitions file.
+      tbl_outs: A list of tuples ([opts], out), where each 'opts' is a list of
+        options passed to tblgen, each option being a string, and 'out' is the
+        corresponding output file produced.
+      td_srcs: See gentbl_rule.td_srcs
+      includes: See gentbl_rule.includes
+      td_includes: See gentbl_rule.td_includes
+      td_relative_includes: An alias for "includes". Deprecated. Use includes
+        instead.
+      deps: See gentbl_rule.deps
+      strip_include_prefix: attribute to pass through to cc_library.
+      test: whether to create a shell test that invokes the tool too.
+      **kwargs: Extra keyword arguments to pass to all generated rules.
+    """
+
+    filegroup_name = name + "_filegroup"
+    gentbl_filegroup(
+        name = filegroup_name,
+        tblgen = tblgen,
+        td_file = td_file,
+        tbl_outs = tbl_outs,
+        td_srcs = td_srcs,
+        td_includes = td_includes,
+        includes = includes + td_relative_includes,
+        deps = deps,
+        test = test,
+        skip_opts = ["-gen-op-doc"],
+        **kwargs
+    )
     native.cc_library(
         name = name,
-        # include_prefix does not apply to textual_hdrs.
-        hdrs = hdrs if strip_include_prefix else [],
+        # strip_include_prefix does not apply to textual_hdrs.
+        # https://github.com/bazelbuild/bazel/issues/12424
+        hdrs = [":" + filegroup_name] if strip_include_prefix else [],
         strip_include_prefix = strip_include_prefix,
-        textual_hdrs = hdrs,
+        textual_hdrs = [":" + filegroup_name],
+        **kwargs
+    )
+
+def gentbl(
+        name,
+        tblgen,
+        td_file,
+        tbl_outs,
+        td_srcs = [],
+        td_includes = [],
+        includes = [],
+        td_relative_includes = [],
+        deps = [],
+        test = False,
+        **kwargs):
+    """Deprecated version of gentbl_cc_library.
+
+    Accepts tbl_outs as list of pairs with the first element of the pair being
+    a whitespace-separated string of options rather than a list of options.
+    """
+
+    split_opts = []
+    for (opts_string, out) in tbl_outs:
+        # TODO(gcmn): The API of opts as single string is preserved for backward
+        # compatibility. Change to taking a sequence.
+        opts = opts_string.split(" ") if opts_string else []
+
+        # Filter out empty options
+        opts = [opt for opt in opts if opt]
+
+        split_opts.append((opts, out))
+
+    gentbl_cc_library(
+        name = name,
+        tblgen = tblgen,
+        td_file = td_file,
+        tbl_outs = split_opts,
+        td_srcs = td_srcs,
+        td_includes = td_includes,
+        includes = includes,
+        td_relative_includes = td_relative_includes,
+        deps = deps,
+        test = test,
+        deprecation = "generated by gentbl; use gentbl_cc_library or gentbl_filegroup instead",
+        **kwargs
     )