| #!/usr/bin/env python3 |
| # Copyright (c) 2023-2024, ARM Limited. |
| # SPDX-License-Identifier: Apache-2.0 |
| import os |
| import re |
| from functools import cmp_to_key |
| |
| import tosa |
| |
| |
| def compare_profiles(a, b): |
| if a.profiles[0] == b.profiles[0]: |
| return 1 if a.mode > b.mode else -1 |
| if "EXT-" in a.profiles[0]: |
| if "EXT-" in b.profiles[0]: |
| return 1 if a.profiles[0] > b.profiles[0] else -1 |
| else: |
| return 1 |
| if "EXT-" in b.profiles[0]: |
| return -1 |
| return 1 if a.profiles[0] > b.profiles[0] else -1 |
| |
| |
| class TOSASpecAsciidocGenerator: |
| def __init__(self, spec): |
| self.spec = spec |
| |
| def generate_enum(self, enum, file): |
| file.write(f"\n=== {enum.name}\n") |
| file.write(f"{enum.description}\n") |
| file.write("|===\n") |
| file.write("|Name|Value|Description\n\n") |
| for val in enum.values: |
| file.write(f"|{val[0]}|{val[1]}|{val[2]}\n") |
| file.write("|===\n") |
| |
| def generate_operator(self, op, file): |
| file.write("\n*Arguments:*\n") |
| file.write("[cols='3,3,2,2,4,8']") |
| file.write("\n|===\n") |
| file.write("|Argument|Type|Name|Shape|Rank|Description\n\n") |
| for arg in op.arguments: |
| # Argument |
| cats = arg.categories |
| if len(cats) > 1: |
| cattext = "" |
| sep = "" |
| for cat in cats: |
| proflist = "/".join(cat.profiles) |
| profcaption = "profiles" if len(cat.profiles) > 1 else "profile" |
| cattext += sep + cat.name.title() + f" ({proflist} {profcaption})" |
| sep = " " |
| else: |
| cattext = cats[0].name.title() |
| |
| # Type |
| if arg.type == "tensor_t": |
| argtype = f"T<{arg.tensor_element_type}>" |
| elif arg.type == "tensor_list_t": |
| if arg.tensor_element_type == "-": |
| argtype = "tensor_list_t" |
| else: |
| argtype = f"tensor_list_t<T<{arg.tensor_element_type}>>" |
| elif arg.type == "shape_t": |
| if arg.shape != "-": |
| argtype = f"shape_t<{arg.shape}>" |
| else: |
| argtype = "shape_t<>" |
| else: |
| argtype = arg.type |
| |
| # Rank |
| if len(arg.rank) > 0: |
| if arg.rank[0] == arg.rank[1]: |
| rank = f"{arg.rank[0]}" |
| else: |
| rank = f"{arg.rank[0]} to {arg.rank[1]}" |
| else: |
| rank = "" |
| |
| # Format and write line |
| file.write( |
| f"|{cattext}|{argtype}|{arg.name}|{arg.shape}" |
| f"|{rank}|{arg.description}\n" |
| ) |
| |
| file.write("|===\n") |
| if op.typesupports: |
| file.write("\n*Supported Data Types:*\n\n") |
| file.write("|===\n") |
| header = "|Profile/Extension|Mode" |
| for ty in op.types: |
| header += f"|{ty}" |
| file.write(header) |
| file.write("\n\n") |
| for tysup in sorted(op.typesupports, key=cmp_to_key(compare_profiles)): |
| profile = " or ".join(tysup.profiles) if tysup.profiles else "Any" |
| entry = f"|{profile}|{tysup.mode}" |
| for ty in op.types: |
| entry += f"|{tysup.tymap[ty]}" |
| entry += "\n" |
| file.write(entry) |
| file.write("|===\n") |
| file.write("\n*Operation Function:*\n\n") |
| leveltext = "" |
| for arg in op.arguments: |
| if len(arg.levellimits) > 0: |
| for limit in arg.levellimits: |
| leveltext += "LEVEL_CHECK(" + limit[0] + " <= " + limit[1] + ");\n" |
| if len(leveltext) > 0: |
| file.write(f"[source,c++]\n----\n{leveltext}\n----\n") |
| |
| def generate(self, outdir): |
| os.makedirs(outdir, exist_ok=True) |
| |
| # Generate version information |
| major = self.spec.version_major |
| minor = self.spec.version_minor |
| patch = self.spec.version_patch |
| with open(os.path.join(outdir, "version.adoc"), "w") as f: |
| f.write(":tosa-version-string: {}.{}.{}".format(major, minor, patch)) |
| if self.spec.version_is_draft: |
| f.write(" draft") |
| f.write("\n") |
| |
| # Generate profile table |
| with open(os.path.join(outdir, "profiles.adoc"), "w") as f: |
| f.write("|===\n") |
| f.write("|Profile|Name|Description|Specification Status\n\n") |
| for profile in self.spec.profiles: |
| f.write( |
| f"|{profile.profile}|{profile.name}|" |
| f"{profile.description}|{profile.status}\n" |
| ) |
| f.write("|===\n") |
| |
| # Generate profile table |
| with open(os.path.join(outdir, "profile_extensions.adoc"), "w") as f: |
| f.write("|===\n") |
| f.write("|Name|Description|Allowed profiles|Specification Status\n\n") |
| for profile_extension in self.spec.profile_extensions: |
| f.write( |
| f"|{profile_extension.name}|{profile_extension.description}" |
| f"|{','.join(profile_extension.profiles)}" |
| f"|{profile_extension.status}\n" |
| ) |
| f.write("|===\n") |
| |
| # Generate level maximums table |
| with open(os.path.join(outdir, "levels.adoc"), "w") as f: |
| f.write("|===\n") |
| f.write("|tosa_level_t") |
| for level in self.spec.levels: |
| f.write("|tosa_level_{}".format(level.name)) |
| f.write("\n") |
| f.write("|Description") |
| for level in self.spec.levels: |
| f.write("|{}".format(level.desc)) |
| f.write("\n") |
| for param in self.spec.levels[0].maximums: |
| f.write("|{}".format(param)) |
| for level in self.spec.levels: |
| f.write("|{}".format(level.maximums[param])) |
| f.write("\n") |
| f.write("|===\n") |
| |
| # Generator operators |
| opdir = os.path.join(outdir, "operators") |
| os.makedirs(opdir, exist_ok=True) |
| for group in self.spec.operatorgroups: |
| for op in group.operators: |
| with open(os.path.join(opdir, op.name + ".adoc"), "w") as f: |
| self.generate_operator(op, f) |
| with open(os.path.join(outdir, "enums.adoc"), "w") as f: |
| for enum in self.spec.enums: |
| self.generate_enum(enum, f) |
| |
| all_operators = [] |
| for group in self.spec.operatorgroups: |
| for op in group.operators: |
| all_operators.append(op) |
| |
| # Generate profile operator appendix |
| with open(os.path.join(outdir, "profile_ops.adoc"), "w") as f: |
| f.write("=== Profiles\n") |
| for profile in self.spec.profiles: |
| f.write(f"==== {profile.profile}\n") |
| f.write(f"{profile.description}\n\n") |
| f.write(f"Status: {profile.status}\n") |
| f.write("|===\n") |
| f.write("|Operator|Mode|Version Added\n\n") |
| for op in sorted(all_operators, key=lambda o: o.name): |
| if op.typesupports: |
| for tysup in op.typesupports: |
| if profile.name in tysup.profiles: |
| f.write( |
| f"|{op.name}|{tysup.mode}|{tysup.version_added}\n" |
| ) |
| f.write("|===\n") |
| |
| f.write("=== Profile Extensions\n") |
| for pext in self.spec.profile_extensions: |
| f.write(f"==== {pext.name} extension\n") |
| f.write(f"{pext.description}\n\n") |
| f.write(f"Status: {pext.status}\n\n") |
| f.write(f"Compatible profiles: {', '.join(pext.profiles)}\n\n") |
| f.write("|===\n") |
| f.write("|Operator|Mode|Version Added|Note\n\n") |
| for op in sorted(all_operators, key=lambda o: o.name): |
| if op.typesupports: |
| for tysup in op.typesupports: |
| for profile in tysup.profiles: |
| if profile.find(pext.name) != -1: |
| note = "" |
| m = re.match(r"(.*) and (.*)", profile) |
| if m: |
| if m[1] == pext.name: |
| note = f"If {m[2]} is also supported" |
| else: |
| note = f"If {m[1]} is also supported" |
| f.write( |
| f"|{op.name}|{tysup.mode}|" |
| f"{tysup.version_added}|{note}\n" |
| ) |
| f.write("|===\n") |
| |
| |
| if __name__ == "__main__": |
| import argparse |
| |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--xml", required=True, help="Path to specification XML") |
| parser.add_argument("--outdir", required=True, help="Output directory") |
| args = parser.parse_args() |
| |
| try: |
| spec = tosa.TOSASpec(args.xml) |
| except RuntimeError as e: |
| print(f"Failure reading/validating XML spec: {str(e)}") |
| exit(1) |
| |
| generator = TOSASpecAsciidocGenerator(spec) |
| generator.generate(args.outdir) |