std-traits 0.8.1

Traits for types in the standard library.
Documentation
import json
import re
import shutil
from dataclasses import dataclass
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import IO, Any


@dataclass
class FunctionSpec:
    must_use: bool | str | None
    unsafe: bool
    name: str
    definition: str
    call: str


def stringify_type(d: dict[str, Any]) -> str:
    [(typ, inner)] = d.items()
    match typ:
        case "primitive":
            return inner
        case "generic":
            return inner
        case "slice":
            return f"[{stringify_type(inner)}]"
        case "array":
            return f"[{stringify_type(inner['type'])}; {inner['len']}]"
        case "borrowed_ref":
            assert inner["lifetime"] is None
            s = "&"
            if inner["is_mutable"]:
                s += "mut "
            return s + stringify_type(inner["type"])
        case "tuple":
            return f"({', '.join(stringify_type(v) for v in inner)})"
        case "resolved_path":
            s = inner["path"]
            args = inner["args"]
            if args:
                [(arg_typ, arg_inner)] = args.items()
                assert not arg_inner["constraints"]
                assert arg_typ == "angle_bracketed"
                s += (
                    "<"
                    + ", ".join(stringify_generic(t) for t in arg_inner["args"])
                    + ">"
                )

            return s
        case _:
            assert False, (typ, inner)


def stringify_generic(d: dict[str, Any]) -> str:
    [(typ, inner)] = d.items()
    match typ:
        case "type":
            return stringify_type(inner)
        case _:
            assert False, (typ, inner)


def stringify_arg(name: str, typ_s: str) -> str:
    if name == "self":
        match typ_s:
            case "Self":
                return "self"
            case "&Self":
                return "&self"
            case _:
                assert False, typ_s
    return f"{name}: {typ_s}"


def parse_fn(d: dict[str, Any]) -> FunctionSpec | None:
    inner = d["inner"]
    [(typ, spec)] = inner.items()
    if typ != "function":
        return None

    generics = spec["generics"]
    if generics["params"] or generics["where_predicates"]:
        return None

    if d["deprecation"] is not None:
        return None

    stability = d["stability"]
    if stability is None or stability["level"] == "unstable":
        return None

    must_use = None
    for attr in d["attrs"]:
        [(typ, inner)] = attr.items()
        match typ:
            case "must_use":
                [(reason_key, reason)] = inner.items()
                assert reason_key == "reason"
                assert must_use is None
                if reason is None:
                    must_use = True
                elif isinstance(reason, str):
                    must_use = reason
                else:
                    assert False, (typ, inner)
            case "other":
                pass
            case _:
                assert False, (typ, inner)

    name = d["name"]
    header = spec["header"]
    is_unsafe = header["is_unsafe"]

    sig = spec["sig"]

    args = []
    arg_names = []
    for arg_name, typ in sig["inputs"]:
        typ_s = stringify_type(typ)
        arg_names.append(arg_name)
        args.append(stringify_arg(arg_name, typ_s))

    definition = ""
    if is_unsafe:
        definition += "unsafe "

    definition += f"fn {name}("
    definition += ", ".join(args)
    definition += ")"
    definition += f" -> {stringify_type(sig['output'])}"

    call = f"{name}({', '.join(arg_names)})"

    return FunctionSpec(
        must_use=must_use,
        unsafe=is_unsafe,
        name=name,
        definition=definition,
        call=call,
    )


def parse_specs(crate: str, typ: str) -> list[FunctionSpec]:
    print(f"Processing {crate}::{typ}")

    path = (
        Path.home()
        / f".rustup/toolchains/nightly-x86_64-unknown-linux-gnu/share/doc/rust/json/{crate}.json"
    )
    with path.open("rb") as f:
        data = json.load(f)

    res = []
    for d in data["index"].values():
        inner = d["inner"]
        if isinstance(inner, str):
            continue
        impl = inner.get("impl", {})

        if impl.get("for", {}).get("primitive") != typ:
            continue

        if impl["trait"] is not None:
            continue

        for item_id in impl["items"]:
            item_d = data["index"][str(item_id)]
            if fn := parse_fn(item_d):
                res.append(fn)

    return res


@dataclass
class Trait:
    super_trait: str | None
    example_implementor: str
    core_fns: list[FunctionSpec]
    std_fns: list[FunctionSpec]
    ignores: set[str]
    replacements: dict[str, str]


i32_core = parse_specs("core", "i32")
u32_core = parse_specs("core", "u32")
f32_core = parse_specs("core", "f32")
f32_std = parse_specs("std", "f32")

i32_fns = {s.name for s in i32_core}
u32_fns = {s.name for s in u32_core}
f32_fns = {s.name for s in f32_core}

number_fns = i32_fns & u32_fns & f32_fns

int_fns = (i32_fns & u32_fns) - f32_fns
float_fns = f32_fns - int_fns
signed_fns = i32_fns - u32_fns
unsigned_fns = u32_fns - i32_fns

number_core = [s for s in i32_core if s.name in number_fns]
float_core = [s for s in f32_core if s.name in float_fns]
float_std = [s for s in f32_std if s.name not in float_fns and s.name not in int_fns]
int_core = [s for s in i32_core if s.name in int_fns]
signed_core = [s for s in i32_core if s.name in signed_fns]
unsigned_core = [s for s in u32_core if s.name in unsigned_fns]


TRAITS = {
    "NUMBER": Trait(
        super_trait=None,
        example_implementor="i32",
        core_fns=number_core,
        std_fns=[],
        ignores={
            # Implemented on NumberLike trait
            "to_be_bytes",
            "to_le_bytes",
            "to_ne_bytes",
        },
        replacements={
            "[u8; 4]": "Self::ByteArray",
        },
    ),
    "FLOAT": Trait(
        super_trait="NUMBER",
        example_implementor="f32",
        core_fns=float_core,
        std_fns=float_std,
        ignores={
            # Implemented on NumberLike trait
            "to_be_bytes",
            "to_le_bytes",
            "to_ne_bytes",
            # Implemented on Number trait
            "from_be_bytes",
            "from_le_bytes",
            "from_ne_bytes",
            "abs",
            "signum",
        },
        replacements={
            "u32": "Self::Bits",
            "[u8; 4]": "Self::Bytes",
        },
    ),
    "INTEGER": Trait(
        super_trait="NUMBER",
        example_implementor="i32",
        core_fns=int_core,
        std_fns=[],
        ignores={
            # Implemented on Number
            "div_euclid",
            "rem_euclid",
            # NumBufferTrait is not stabilized
            "format_into",
        },
        replacements={
            "abs_diff(self, other: Self) -> u32": "abs_diff(self, other: Self) -> Self::Unsigned",
        },
    ),
    "SIGNED": Trait(
        super_trait="INTEGER",
        example_implementor="i32",
        core_fns=signed_core,
        std_fns=[],
        ignores=set(),
        replacements={
            "_unsigned(self) -> u32": "_unsigned(self) -> Self::Unsigned",
            "_unsigned(self, rhs: u32)": "_unsigned(self, rhs: Self::Unsigned)",
            "unsigned_abs(self) -> u32": "unsigned_abs(self) -> Self::Unsigned",
        },
    ),
    "UNSIGNED": Trait(
        super_trait="INTEGER",
        example_implementor="u32",
        core_fns=unsigned_core,
        std_fns=[],
        ignores=set(),
        replacements={
            "bit_width(self) -> Self": "bit_width(self) -> u32",
            "_signed(self, rhs: i32)": "_signed(self, rhs: Self::Signed)",
            "_signed(self) -> i32": "_signed(self) -> Self::Signed",
            "_signed_diff(self, rhs: Self) -> Option<i32>": "_signed_diff(self, rhs: Self) -> Option<Self::Signed>",
        },
    ),
}


def print_decl(dst: IO[str], indent: str, trait: Trait, impl: bool) -> None:
    print(f"{indent}// Generated by generate_delegates.py", file=dst)
    print(file=dst)

    ignores_not_found = set(trait.ignores)

    ignores = set(trait.ignores)
    t = trait
    while t.super_trait is not None:
        t = TRAITS[t.super_trait]
        ignores |= t.ignores

    core_fns = {s.name for s in trait.core_fns}
    std_fns = [s for s in trait.std_fns if s.name not in core_fns]

    for is_std, fns in enumerate([trait.core_fns, std_fns]):
        if is_std:
            crate_name = "std"
            cfg = f'{indent}#[cfg(feature = "std")]\n'
        else:
            crate_name = "core"
            cfg = ""
        for fn in fns:
            if fn.name in trait.ignores:
                ignores_not_found.remove(fn.name)
                continue

            replacements = {
                trait.example_implementor: "Self",
                "crate::": f"{crate_name}::",
            } | trait.replacements

            definition = fn.definition
            call = fn.call
            for k, v in replacements.items():
                definition = definition.replace(k, v)
                call = call.replace(k, v)

            if impl:
                call = f"Self::{call}"
                if fn.unsafe:
                    call = f"unsafe {{ {call} }}"
                print(
                    f"""{cfg}{indent}{definition} {{
{indent}    {call}
{indent}}}
""",
                    file=dst,
                )
            else:
                ref = f"[`{trait.example_implementor}::{fn.name}`]"
                docs = f"{indent}/// See {ref}.\n"
                if fn.unsafe:
                    docs += f"""{indent}///
{indent}/// # Safety
{indent}///
{indent}/// See {ref}.
"""
                must_use = ""
                if fn.must_use is True:
                    must_use = f"{indent}#[must_use]\n"
                elif isinstance(fn.must_use, str):
                    must_use = f'{indent}#[must_use = "{fn.must_use}"]\n'
                print(
                    f"""{docs}{cfg}{must_use}{indent}{definition};
""",
                    file=dst,
                )

    for f in ignores_not_found:
        print(f"  Warning: ignored function {f} not found.")


START_RE = re.compile(r"^(?P<indent>\s*)// @START@ (?P<type>\S+) (?P<name>\S+)")

with NamedTemporaryFile("w") as dst:
    with open("src/num.rs", "r") as src:
        copy_lines = True
        for l in src:
            if copy_lines:
                print(l, end="", file=dst)
            if "@START@" in l:
                m = START_RE.search(l)
                assert m is not None
                group = m.groupdict()
                indent = group["indent"]
                trait = TRAITS[group["name"]]
                impl = group["type"] == "IMPL"
                print(f"Generating {group['type']} for {group['name']}")
                print_decl(dst, indent, trait, impl)
                copy_lines = False

            if "@END@" in l:
                print(l, end="", file=dst)
                copy_lines = True

    dst.flush()
    shutil.move(dst.name, "src/num.rs")