From 64eb7052534710c07a690bac42bf3ba279368bf3 Mon Sep 17 00:00:00 2001 From: Thomas Goodfellow Date: Wed, 11 Sep 2024 15:42:28 +0200 Subject: [PATCH 1/2] Basic support for generating intrinsic functions Partially satisfies issue #66. Caveats abound: 1. only tested with the example subincacc instruction 2. generated code has yet to be tested in execution (e.g. ETISS) 3. intrinsics described through separate YML file; would be better expressed in CoreDSL alongside the instruction That the intrinsic support is partially implemented through the riscv_instr_info is ungainly but made the dependency between the instruction definition and the intrinsic trivial. The improved intrinsic support in LLVM19 onwards may help to keep the implementation in the riscv_intrinsics module. --- seal5/backends/riscv_instr_info/writer.py | 16 +- seal5/backends/riscv_intrinsics/__init__.py | 0 seal5/backends/riscv_intrinsics/writer.py | 214 ++++++++++++++++++++ seal5/pass_list.py | 64 ++++++ seal5/settings.py | 22 +- 5 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 seal5/backends/riscv_intrinsics/__init__.py create mode 100644 seal5/backends/riscv_intrinsics/writer.py diff --git a/seal5/backends/riscv_instr_info/writer.py b/seal5/backends/riscv_instr_info/writer.py index 0850b09e..765cd8e1 100644 --- a/seal5/backends/riscv_instr_info/writer.py +++ b/seal5/backends/riscv_instr_info/writer.py @@ -20,6 +20,7 @@ from seal5.index import NamedPatch, File, write_index_yaml from seal5.utils import is_power_of_two +from seal5.settings import ExtensionsSettings, IntrinsicDefn # from seal5.settings import ExtensionsSettings @@ -220,6 +221,12 @@ def gen_riscv_instr_info_str(instr, set_def): ) return tablegen_str +def gen_intrinsic_pattern(instr, intrinsic: IntrinsicDefn): + pat = f"""class Pat_{instr.name} +: Pat<(OpNode {instr.llvm_ins_str}), (Inst {instr.llvm_ins_str})>; +def : Pat_{instr.name};""" + return pat + def main(): """Main app entrypoint.""" @@ -234,6 +241,7 @@ def main(): parser.add_argument("--metrics", default=None, help="Output metrics to file") parser.add_argument("--index", default=None, help="Output index to file") parser.add_argument("--ext", type=str, default="td", help="Default file extension (if using --splitted)") + parser.add_argument("--add-intrinsics", type=bool, default=True, action=argparse.BooleanOptionalAction, help="Include patterns for intrinsic functions") args = parser.parse_args() # initialize logging @@ -283,7 +291,7 @@ def main(): } # preprocess model # print("model", model) - # settings = model.get("settings", None) + settings = model.get("settings", None) artifacts = {} artifacts[None] = [] # used for global artifacts if args.splitted: @@ -313,6 +321,11 @@ def main(): output_file = set_dir / out_name content = gen_riscv_instr_info_str(instr_def, set_def) if len(content) > 0: + if args.add_intrinsics and settings.intrinsics.intrinsics: + # TODO: intrinsics should be dict keyed by instr name + for intrinsic in settings.intrinsics.intrinsics: + if intrinsic.instr_name.casefold() == instr_def.mnemonic.casefold(): + content += gen_intrinsic_pattern(instr_def, intrinsic) assert pred is not None predicate_str = f"Predicates = [{pred}, IsRV{xlen}]" content = f"let {predicate_str} in {{\n{content}\n}}" @@ -325,7 +338,6 @@ def main(): artifacts[set_name].append(instr_info_patch) inc = f"seal5/{set_name}/{output_file.name}" includes.append(inc) - includes_str = "\n".join([f'include "{inc}"' for inc in includes]) set_td_includes_patch = NamedPatch( f"llvm/lib/Target/RISCV/seal5/{set_name}.td", diff --git a/seal5/backends/riscv_intrinsics/__init__.py b/seal5/backends/riscv_intrinsics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/seal5/backends/riscv_intrinsics/writer.py b/seal5/backends/riscv_intrinsics/writer.py new file mode 100644 index 00000000..f620307d --- /dev/null +++ b/seal5/backends/riscv_intrinsics/writer.py @@ -0,0 +1,214 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# This file is part of the M2-ISA-R project: https://github.com/tum-ei-eda/M2-ISA-R +# +# Copyright (C) 2022 +# Chair of Electrical Design Automation +# Technical University of Munich + +"""Clean M2-ISA-R/Seal5 metamodel to .core_desc file.""" + +import argparse +import logging +import pathlib +import pickle +import os.path +from typing import Union +from dataclasses import dataclass + +from m2isar.metamodel import arch + +from seal5.index import NamedPatch, write_index_yaml +from seal5.settings import IntrinsicDefn + +logger = logging.getLogger("riscv_intrinsics") + + +def ir_type_to_text(ir_type: str): + # needs fleshing out with all likely types + # probably needs to take into account RISC-V bit width, e.g. does "Li" means 32 bit integer on a 128-bit platform? + if ir_type == 'i32': + return 'Li' + raise NotImplementedError(f'Unhandled ir_type "{ir_type}"') + + +def build_target(arch: str, intrinsic: IntrinsicDefn): + + # Target couples intrinsic name to argument types and function behaviour + # Start with return type if not void + arg_str = '' + if intrinsic.ret_type: + arg_str += ir_type_to_text(intrinsic.ret_type) + for arg in intrinsic.args: + arg_str += ir_type_to_text(arg.arg_type) + + target = f'TARGET_BUILTIN(__builtin_{arch}_{intrinsic.intrinsic_name}, "{arg_str}", "nc", "{arch}")' + return target + +def ir_type_to_pattern(ir_type: str): + # needs fleshing out with all likely types + match ir_type: + case 'i32': + return 'llvm_i32_ty' + raise NotImplementedError(f'Unhandled ir_type "{ir_type}"') + +def build_attr(arch: str, intrinsic: IntrinsicDefn): + uses_mem = False #@todo + ret_types = '(unsupported)' # how to spec void intrinsic? + attr = f' def int_riscv_{intrinsic.intrinsic_name} : Intrinsic<\n [' + if intrinsic.ret_type: + attr += f'{ir_type_to_pattern(intrinsic.ret_type)}' + attr += '],\n [' + for idx, arg in enumerate(intrinsic.args): + if idx: + attr += ', ' + attr += ir_type_to_pattern(arg.arg_type) + attr += '],\n' + attr += ' [IntrNoMem, IntrSpeculatable, IntrWillReturn]>;' + return attr + + +def build_emit(arch: str, intrinsic: IntrinsicDefn): + emit = (f' case RISCV::BI__builtin_{arch}_{intrinsic.intrinsic_name}:\n' + f' ID = Intrinsic::riscv_{intrinsic.intrinsic_name};\n' + f' break;') + return emit + + +@dataclass +class PatchFrag: + patchee: str + tag: str + contents: str = "" + +def main(): + """Main app entrypoint.""" + + # read command line args + parser = argparse.ArgumentParser() + parser.add_argument("top_level", help="A .m2isarmodel or .seal5model file.") + parser.add_argument("--log", default="info", choices=["critical", "error", "warning", "info", "debug"]) + parser.add_argument("--output", "-o", type=str, default=None) + parser.add_argument("--splitted", action="store_true", help="Split per set") + parser.add_argument("--formats", action="store_true", help="Also generate instruction formats") + parser.add_argument("--metrics", default=None, help="Output metrics to file") + parser.add_argument("--index", default=None, help="Output index to file") + parser.add_argument("--ext", type=str, default="td", help="Default file extension (if using --splitted)") + args = parser.parse_args() + + # initialize logging + logging.basicConfig(level=getattr(logging, args.log.upper())) + + # resolve model paths + top_level = pathlib.Path(args.top_level) + + is_seal5_model = False + if top_level.suffix == ".seal5model": + is_seal5_model = True + if args.output is None: + assert top_level.suffix in [".m2isarmodel", ".seal5model"], "Can not infer model type from file extension." + raise NotImplementedError + + # out_path = top_level.parent / (top_level.stem + ".core_desc") + else: + out_path = pathlib.Path(args.output) + + logger.info("loading models") + if not is_seal5_model: + raise NotImplementedError + + # load models + with open(top_level, "rb") as f: + # models: "dict[str, arch.CoreDef]" = pickle.load(f) + if is_seal5_model: + model: "dict[str, Union[arch.InstructionSet, ...]]" = pickle.load(f) + model["cores"] = {} + else: # TODO: core vs. set! + temp: "dict[str, Union[arch.InstructionSet, arch.CoreDef]]" = pickle.load(f) + assert len(temp) > 0, "Empty model!" + if isinstance(list(temp.values())[0], arch.CoreDef): + model = {"cores": temp, "sets": {}} + elif isinstance(list(temp.values())[0], arch.InstructionSet): + model = {"sets": temp, "cores": {}} + else: + assert False + + metrics = { + "n_sets": 0, + "n_skipped": 0, + "n_failed": 0, + "n_success": 0, + } + # preprocess model + # print("model", model) + artifacts = {} + artifacts[None] = [] # used for global artifacts + if args.splitted: + raise NotImplementedError + else: + # errs = [] + settings = model.get("settings", None) + llvm_version = None + if not settings or not settings.intrinsics.intrinsics: + logger.warning("No intrinsics configured; didn't need to invoke intrinsics writer.") + quit() + if settings: + llvm_settings = settings.llvm + if llvm_settings: + llvm_state = llvm_settings.state + if llvm_state: + llvm_version = llvm_state.version + patch_frags = { + 'target': PatchFrag(patchee='clang/include/clang/Basic/BuiltinsRISCV.def', tag='builtins_riscv'), + 'attr': PatchFrag(patchee='llvm/include/llvm/IR/IntrinsicsRISCV.td', tag='intrinsics_riscv'), + 'emit': PatchFrag(patchee='clang/lib/CodeGen/CGBuiltin.cpp', tag='cg_builtin') + } + for set_name, set_def in model["sets"].items(): + artifacts[set_name] = [] + metrics["n_sets"] += 1 + ext_settings = set_def.settings + if ext_settings is None: + metrics["n_skipped"] += 1 + continue + for intrinsic in settings.intrinsics.intrinsics: + metrics["n_success"] += 1 + + patch_frags['target'].contents += build_target(arch=ext_settings.get_arch(), intrinsic=intrinsic) + patch_frags['attr'].contents += build_attr(arch=ext_settings.get_arch(), intrinsic=intrinsic) + patch_frags['emit'].contents += build_emit(arch=ext_settings.get_arch(), intrinsic=intrinsic) + + for id, frag in patch_frags.items(): + contents = frag.contents + if len(contents) > 0: + if id == 'target': + contents = f'// {ext_settings.get_arch()}\n{contents}\n' + elif id == 'attr': + contents = f'let TargetPrefix = "riscv" in {{\n{contents}\n}}' + (root, ext) = os.path.splitext(out_path) + patch_path = root + '_' + id + ext + with open(patch_path, "w") as f: + f.write(contents) + key = frag.tag + if ext_settings.experimental: + key += "_experimental" + patch = NamedPatch(frag.patchee, key=key, src_path=patch_path, content=contents) + artifacts[None].append(patch) + if args.metrics: + metrics_file = args.metrics + with open(metrics_file, "w") as f: + f.write(",".join(metrics.keys())) + f.write("\n") + f.write(",".join(map(str, metrics.values()))) + f.write("\n") + if args.index: + if sum(map(lambda x: len(x), artifacts.values())) > 0: + global_artifacts = artifacts.get(None, []) + set_artifacts = {key: value for key, value in artifacts.items() if key is not None} + index_file = args.index + write_index_yaml(index_file, global_artifacts, set_artifacts, content=True) + else: + logger.warning("No patches generated. No index file will be written.") + + +if __name__ == "__main__": + main() diff --git a/seal5/pass_list.py b/seal5/pass_list.py index 5435b8e2..fbdd3ba0 100644 --- a/seal5/pass_list.py +++ b/seal5/pass_list.py @@ -1076,6 +1076,7 @@ def gen_riscv_isa_info_patch( print_func=logger.info if verbose else logger.debug, live=True, ) +# breakpoint() if gen_index_file: if index_file.is_file(): patch_name = f"riscv_isa_info_{input_file.stem}" @@ -1093,6 +1094,68 @@ def gen_riscv_isa_info_patch( logger.warning("No patches found!") +def gen_riscv_intrinsics( + input_model: str, + settings: Optional[Seal5Settings] = None, + env: Optional[dict] = None, + verbose: bool = False, + split: bool = False, + log_level: str = "debug", + **kwargs, +): + assert not split, "TODO" + # formats = True + gen_metrics_file = True + gen_index_file = True + input_file = settings.models_dir / f"{input_model}.seal5model" + assert input_file.is_file(), f"File not found: {input_file}" + name = input_file.name + new_name = name.replace(".seal5model", "") + logger.info("Writing intrinsics patches patch for %s", name) + out_dir = settings.patches_dir / new_name + out_dir.mkdir(exist_ok=True) + + args = [ + settings.models_dir / name, + "--log", + log_level, + "--output", + out_dir / "riscv_intrinsics_info.patch", + ] + if split: + args.append("--splitted") + if gen_metrics_file: + metrics_file = out_dir / ("riscv_intrinsics_info_metrics.csv") + args.extend(["--metrics", metrics_file]) + if gen_index_file: + index_file = out_dir / ("riscv_intrinsics_index.yml") + args.extend(["--index", index_file]) + utils.python( + "-m", + "seal5.backends.riscv_intrinsics.writer", + *args, + env=env, + print_func=logger.info if verbose else logger.debug, + live=True, + ) +# breakpoint() + if gen_index_file: + if index_file.is_file(): + patch_base = f"riscv_intrinsics_target_{input_file.stem}" + patch_settings = PatchSettings( + name=patch_base, + stage=int(PatchStage.PHASE_1), + comment=f"Generated RISCV Intrinsics patch for {input_file.name}", + index=str(index_file), + generated=True, + target="llvm", + ) + settings.add_patch(patch_settings) + settings.to_yaml_file(settings.settings_file) + else: + logger.warning("No patches found!") + + def gen_riscv_instr_info_patch( input_model: str, settings: Optional[Seal5Settings] = None, @@ -1103,6 +1166,7 @@ def gen_riscv_instr_info_patch( **_kwargs, ): # assert not split, "TODO" +# breakpoint() assert split, "TODO" # formats = True gen_metrics_file = True diff --git a/seal5/settings.py b/seal5/settings.py index 38efe1ef..5135b6ea 100644 --- a/seal5/settings.py +++ b/seal5/settings.py @@ -157,8 +157,10 @@ "clone_depth": 1, "sparse_checkout": False, }, + }, + "intrinsics": { }, -} + } ALLOWED_YAML_TYPES = (int, float, str, bool) @@ -570,6 +572,22 @@ class ToolsSettings(YAMLSettings): pattern_gen: Optional[PatternGenSettings] = None +@dataclass +class IntrinsicArg(YAMLSettings): + arg_name: str + arg_type: str + +@dataclass +class IntrinsicDefn(YAMLSettings): + instr_name: str + intrinsic_name: str + ret_type: Optional[str] = None + args: Optional[List[IntrinsicArg]] = None + +@dataclass +class IntrinsicsSettings(YAMLSettings): + intrinsics: Optional[List[IntrinsicDefn]] = None + @dataclass class Seal5Settings(YAMLSettings): @@ -592,6 +610,7 @@ class Seal5Settings(YAMLSettings): riscv: Optional[RISCVSettings] = None tools: Optional[ToolsSettings] = None metrics: list = field(default_factory=list) + intrinsics: Optional[IntrinsicsSettings] = None def reset(self): """Reset Seal5 seetings.""" @@ -623,6 +642,7 @@ def reset(self): transform_info=None, legalization=None, ) + self.intrinsics = IntrinsicsSettings() def save(self, dest: Optional[Path] = None): """Save Seal5 settings to file.""" From c93d63b388b06a8877e75cc7ff5e087fc5b6616b Mon Sep 17 00:00:00 2001 From: Thomas Goodfellow Date: Wed, 4 Sep 2024 14:57:29 +0200 Subject: [PATCH 2/2] Intrinsic for example subincacc instruction Definition and test case for subincacc. Partially addresses #67 and #68 --- examples/cfg/example/intrinsics.yml | 13 +++++++++++++ examples/demo.py | 1 + examples/tests/example/test_subincacc.c | 10 ++++++++++ seal5/backends/riscv_instr_info/writer.py | 2 +- seal5/backends/riscv_intrinsics/writer.py | 22 ++++++++++++---------- seal5/flow.py | 1 + seal5/pass_list.py | 3 --- seal5/settings.py | 5 ++++- 8 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 examples/cfg/example/intrinsics.yml diff --git a/examples/cfg/example/intrinsics.yml b/examples/cfg/example/intrinsics.yml new file mode 100644 index 00000000..17333aa3 --- /dev/null +++ b/examples/cfg/example/intrinsics.yml @@ -0,0 +1,13 @@ +--- +intrinsics: + intrinsics: + - args: + - arg_name: rd + arg_type: i32 + - arg_name: rs1 + arg_type: i32 + - arg_name: rs2 + arg_type: i32 + instr_name: xexample.subincacc + intrinsic_name: subincacc + ret_type: i32 diff --git a/examples/demo.py b/examples/demo.py index c913a4fb..44ecb476 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -94,6 +94,7 @@ EXAMPLES_DIR / "cfg" / "tests.yml", EXAMPLES_DIR / "cfg" / "passes.yml", EXAMPLES_DIR / "cfg" / "git.yml", + EXAMPLES_DIR / "cfg" / "example/intrinsics.yml", ] seal5_flow.load(cfg_files, verbose=VERBOSE, overwrite=False) diff --git a/examples/tests/example/test_subincacc.c b/examples/tests/example/test_subincacc.c index fc7f699a..063bceb3 100644 --- a/examples/tests/example/test_subincacc.c +++ b/examples/tests/example/test_subincacc.c @@ -23,3 +23,13 @@ __attribute__((naked)) void test_subincacc() { // CHECK: ab ba b5 51 xexample.subincacc x21, x11, x27 asm("xexample.subincacc x21, x11, x27"); } + +void test_intrinsic() { + // CHECK: + int a = 3; + int b = 7; + int c = 4; + // Can't rely upon specific registers being used but at least instruction should have been used + // CHECK: example.subincacc + c = __builtin_xexample_subincacc(a, b, c); +} diff --git a/seal5/backends/riscv_instr_info/writer.py b/seal5/backends/riscv_instr_info/writer.py index 765cd8e1..59941892 100644 --- a/seal5/backends/riscv_instr_info/writer.py +++ b/seal5/backends/riscv_instr_info/writer.py @@ -241,7 +241,7 @@ def main(): parser.add_argument("--metrics", default=None, help="Output metrics to file") parser.add_argument("--index", default=None, help="Output index to file") parser.add_argument("--ext", type=str, default="td", help="Default file extension (if using --splitted)") - parser.add_argument("--add-intrinsics", type=bool, default=True, action=argparse.BooleanOptionalAction, help="Include patterns for intrinsic functions") + parser.add_argument("--no-add-intrinsics", dest='add_intrinsics', default=True, action='store_false', help="Suppress patterns for intrinsic functions") args = parser.parse_args() # initialize logging diff --git a/seal5/backends/riscv_intrinsics/writer.py b/seal5/backends/riscv_intrinsics/writer.py index f620307d..95d8d91d 100644 --- a/seal5/backends/riscv_intrinsics/writer.py +++ b/seal5/backends/riscv_intrinsics/writer.py @@ -45,16 +45,16 @@ def build_target(arch: str, intrinsic: IntrinsicDefn): target = f'TARGET_BUILTIN(__builtin_{arch}_{intrinsic.intrinsic_name}, "{arg_str}", "nc", "{arch}")' return target + def ir_type_to_pattern(ir_type: str): # needs fleshing out with all likely types - match ir_type: - case 'i32': - return 'llvm_i32_ty' + if ir_type == 'i32': + return 'llvm_i32_ty' raise NotImplementedError(f'Unhandled ir_type "{ir_type}"') + def build_attr(arch: str, intrinsic: IntrinsicDefn): - uses_mem = False #@todo - ret_types = '(unsupported)' # how to spec void intrinsic? + uses_mem = False # @todo attr = f' def int_riscv_{intrinsic.intrinsic_name} : Intrinsic<\n [' if intrinsic.ret_type: attr += f'{ir_type_to_pattern(intrinsic.ret_type)}' @@ -77,9 +77,11 @@ def build_emit(arch: str, intrinsic: IntrinsicDefn): @dataclass class PatchFrag: - patchee: str - tag: str - contents: str = "" + """Pairs patch contents to location to apply it""" + patchee: str + tag: str + contents: str = "" + def main(): """Main app entrypoint.""" @@ -113,7 +115,7 @@ def main(): else: out_path = pathlib.Path(args.output) - logger.info("loading models") + logger.info("intrinsics/writer - loading models") if not is_seal5_model: raise NotImplementedError @@ -157,7 +159,7 @@ def main(): if llvm_settings: llvm_state = llvm_settings.state if llvm_state: - llvm_version = llvm_state.version + llvm_version = llvm_state.version # unused today, but needed very soon patch_frags = { 'target': PatchFrag(patchee='clang/include/clang/Basic/BuiltinsRISCV.def', tag='builtins_riscv'), 'attr': PatchFrag(patchee='llvm/include/llvm/IR/IntrinsicsRISCV.td', tag='intrinsics_riscv'), diff --git a/seal5/flow.py b/seal5/flow.py index a4797180..c8919290 100644 --- a/seal5/flow.py +++ b/seal5/flow.py @@ -81,6 +81,7 @@ # ("riscv_instr_formats", passes.gen_riscv_instr_formats_patch, {}), ("riscv_register_info", passes.gen_riscv_register_info_patch, {}), ("riscv_instr_info", passes.gen_riscv_instr_info_patch, {}), + ("riscv_intrinsics", passes.gen_riscv_intrinsics, {}), # subtarget_tests # register_types # operand_types diff --git a/seal5/pass_list.py b/seal5/pass_list.py index fbdd3ba0..3fed3f22 100644 --- a/seal5/pass_list.py +++ b/seal5/pass_list.py @@ -1076,7 +1076,6 @@ def gen_riscv_isa_info_patch( print_func=logger.info if verbose else logger.debug, live=True, ) -# breakpoint() if gen_index_file: if index_file.is_file(): patch_name = f"riscv_isa_info_{input_file.stem}" @@ -1138,7 +1137,6 @@ def gen_riscv_intrinsics( print_func=logger.info if verbose else logger.debug, live=True, ) -# breakpoint() if gen_index_file: if index_file.is_file(): patch_base = f"riscv_intrinsics_target_{input_file.stem}" @@ -1166,7 +1164,6 @@ def gen_riscv_instr_info_patch( **_kwargs, ): # assert not split, "TODO" -# breakpoint() assert split, "TODO" # formats = True gen_metrics_file = True diff --git a/seal5/settings.py b/seal5/settings.py index 5135b6ea..a3a1073e 100644 --- a/seal5/settings.py +++ b/seal5/settings.py @@ -157,7 +157,7 @@ "clone_depth": 1, "sparse_checkout": False, }, - }, + }, "intrinsics": { }, } @@ -572,11 +572,13 @@ class ToolsSettings(YAMLSettings): pattern_gen: Optional[PatternGenSettings] = None + @dataclass class IntrinsicArg(YAMLSettings): arg_name: str arg_type: str + @dataclass class IntrinsicDefn(YAMLSettings): instr_name: str @@ -584,6 +586,7 @@ class IntrinsicDefn(YAMLSettings): ret_type: Optional[str] = None args: Optional[List[IntrinsicArg]] = None + @dataclass class IntrinsicsSettings(YAMLSettings): intrinsics: Optional[List[IntrinsicDefn]] = None