# build-aux/measurestack/app_plugins.py - Application-specific plugins for analyze.py
#
# Copyright (C) 2024-2025  Luke T. Shumaker <lukeshu@lukeshu.com>
# SPDX-License-Identifier: AGPL-3.0-or-later

import re
import typing

from . import analyze, util
from .analyze import BaseName, Node, QName
from .util import synthetic_node

# pylint: disable=unused-variable
__all__ = [
    "CmdPlugin",
    "LibObjPlugin",
    "LibHWPlugin",
    "LibCRPlugin",
    "LibCRIPCPlugin",
    "Lib9PPlugin",
    "LibMiscPlugin",
    "PicoFmtPlugin",
    "PicoSDKPlugin",
    "TinyUSBDevicePlugin",
    "NewlibPlugin",
    "LibGCCPlugin",
]


class CmdPlugin:
    def is_intrhandler(self, name: QName) -> bool:
        return False

    def init_array(self) -> typing.Collection[QName]:
        return []

    def extra_includes(self) -> typing.Collection[BaseName]:
        return []

    def extra_nodes(self) -> typing.Collection[Node]:
        return []

    def indirect_callees(
        self, loc: str, line: str
    ) -> tuple[typing.Collection[QName], bool] | None:
        if "/3rd-party/" in loc:
            return None
        if "srv->auth" in line:
            return [], False
        if "srv->rootdir" in line:
            return [QName("get_root")], False
        return None

    def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
        return {}


re_comment = re.compile(r"/\*.*?\*/")
re_ws = re.compile(r"\s+")
re_lo_iface = re.compile(r"^\s*#\s*define\s+(?P<name>\S+)_LO_IFACE")
re_lo_func = re.compile(r"LO_FUNC *\([^,]*, *(?P<name>[^,) ]+) *[,)]")
re_lo_implementation = re.compile(
    r"^LO_IMPLEMENTATION_[HC]\s*\(\s*(?P<iface>[^, ]+)\s*,\s*(?P<impl_typ>[^,]+)\s*,\s*(?P<impl_name>[^, ]+)\s*[,)].*"
)
re_call_objcall = re.compile(r"LO_CALL\((?P<obj>[^,]+), (?P<meth>[^,)]+)[,)].*")


class LibObjPlugin:
    objcalls: dict[str, set[QName]]  # method_name => {method_impls}

    def __init__(self, arg_c_fnames: typing.Collection[str]) -> None:
        ifaces: dict[str, set[str]] = {}  # iface_name => {method_names}
        for fname in arg_c_fnames:
            with open(fname, "r", encoding="utf-8") as fh:
                while line := fh.readline():
                    if m := re_lo_iface.match(line):
                        iface_name = m.group("name")
                        if iface_name not in ifaces:
                            ifaces[iface_name] = set()
                        while line.endswith("\\\n"):
                            line += fh.readline()
                        line = line.replace("\\\n", " ")
                        line = re_comment.sub(" ", line)
                        line = re_ws.sub(" ", line)
                        for m2 in re_lo_func.finditer(line):
                            ifaces[iface_name].add(m2.group("name"))

        implementations: dict[str, set[str]] = {}  # iface_name => {impl_names}
        for iface_name in ifaces:
            implementations[iface_name] = set()
        for fname in arg_c_fnames:
            with open(fname, "r", encoding="utf-8") as fh:
                for line in fh:
                    line = line.strip()
                    if m := re_lo_implementation.match(line):
                        implementations[m.group("iface")].add(m.group("impl_name"))

        objcalls: dict[str, set[QName]] = {}  # method_name => {method_impls}
        for iface_name, iface in ifaces.items():
            for method_name in iface:
                if method_name not in objcalls:
                    objcalls[method_name] = set()
                for impl_name in implementations[iface_name]:
                    objcalls[method_name].add(QName(impl_name + "_" + method_name))
        self.objcalls = objcalls

    def is_intrhandler(self, name: QName) -> bool:
        return False

    def init_array(self) -> typing.Collection[QName]:
        return []

    def extra_includes(self) -> typing.Collection[BaseName]:
        return []

    def extra_nodes(self) -> typing.Collection[Node]:
        return []

    def indirect_callees(
        self, loc: str, line: str
    ) -> tuple[typing.Collection[QName], bool] | None:

        if "/3rd-party/" in loc:
            return None
        if m := re_call_objcall.fullmatch(line):
            if m.group("meth") in self.objcalls:
                return self.objcalls[m.group("meth")], False
            return [
                QName(f"__indirect_call:{m.group('obj')}.vtable->{m.group('meth')}")
            ], False
        return None

    def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
        return {}


class LibHWPlugin:
    pico_platform: str
    libobj: LibObjPlugin

    def __init__(self, arg_pico_platform: str, libobj: LibObjPlugin) -> None:
        self.pico_platform = arg_pico_platform
        self.libobj = libobj

    def is_intrhandler(self, name: QName) -> bool:
        return name.base() in [
            BaseName("rp2040_hwtimer_intrhandler"),
            BaseName("hostclock_handle_sig_alarm"),
            BaseName("hostnet_handle_sig_io"),
            BaseName("gpioirq_handler"),
            BaseName("dmairq_handler"),
        ]

    def init_array(self) -> typing.Collection[QName]:
        return []

    def extra_includes(self) -> typing.Collection[BaseName]:
        return []

    def extra_nodes(self) -> typing.Collection[Node]:
        return []

    def indirect_callees(
        self, loc: str, line: str
    ) -> tuple[typing.Collection[QName], bool] | None:
        if "/3rd-party/" in loc:
            return None
        for fn in [
            "io_readv",
            "io_writev",
            "io_close",
            "io_close_read",
            "io_close_write",
            "io_readwritev",
        ]:
            if f"{fn}(" in line:
                return self.libobj.indirect_callees(loc, f"LO_CALL(x, {fn[3:]})")
        if "io_read(" in line:
            return self.libobj.indirect_callees(loc, "LO_CALL(x, readv)")
        if "io_writev(" in line:
            return self.libobj.indirect_callees(loc, "LO_CALL(x, writev)")
        if "trigger->cb(trigger->cb_arg)" in line:
            ret = [
                QName("alarmclock_sleep_intrhandler"),
            ]
            if self.pico_platform == "rp2040":
                ret += [
                    QName("w5500_tcp_alarm_handler"),
                    QName("w5500_udp_alarm_handler"),
                ]
            return ret, False
        if "/rp2040_gpioirq.c:" in loc and "handler->fn" in line:
            return [
                QName("w5500_intrhandler"),
            ], False
        if "/rp2040_dma.c:" in loc and "handler->fn" in line:
            return [
                QName("rp2040_hwspi_intrhandler"),
            ], False
        return None

    def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
        return {}


class LibCRPlugin:
    def is_intrhandler(self, name: QName) -> bool:
        return name.base() in [
            BaseName("_cr_gdb_intrhandler"),
        ]

    def init_array(self) -> typing.Collection[QName]:
        return []

    def extra_includes(self) -> typing.Collection[BaseName]:
        return []

    def extra_nodes(self) -> typing.Collection[Node]:
        return []

    def indirect_callees(
        self, loc: str, line: str
    ) -> tuple[typing.Collection[QName], bool] | None:
        return None

    def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
        return {}


class LibCRIPCPlugin:
    def is_intrhandler(self, name: QName) -> bool:
        return False

    def init_array(self) -> typing.Collection[QName]:
        return []

    def extra_includes(self) -> typing.Collection[BaseName]:
        return []

    def extra_nodes(self) -> typing.Collection[Node]:
        return []

    def indirect_callees(
        self, loc: str, line: str
    ) -> tuple[typing.Collection[QName], bool] | None:
        if "/3rd-party/" in loc:
            return None
        if "/chan.c:" in loc and "front->dequeue(" in line:
            return [
                QName("_cr_chan_dequeue"),
                QName("_cr_select_dequeue"),
            ], False
        return None

    def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
        return {}


re_tmessage_handler = re.compile(
    r"^\s*\[LIB9P_TYP_T[^]]+\]\s*=\s*\(tmessage_handler\)\s*(?P<handler>\S+),\s*$"
)
re_lib9p_msg_entry = re.compile(r"^\s*_MSG_(?:[A-Z]+)\((?P<typ>\S+)\),$")
re_lib9p_caller = re.compile(
    r"^lib9p_(?P<grp>[TR])msg_(?P<meth>validate|unmarshal|marshal)$"
)
re_lib9p_callee = re.compile(
    r"^(?P<meth>validate|unmarshal|marshal)_(?P<msg>(?P<grp>[TR]).*)$"
)


class Lib9PPlugin:
    tmessage_handlers: set[QName] | None
    lib9p_msgs: set[str]
    _CONFIG_9P_NUM_SOCKS: int | None
    CONFIG_9P_SRV_MAX_REQS: int | None
    CONFIG_9P_SRV_MAX_DEPTH: int | None

    def __init__(self, arg_base_dir: str, arg_c_fnames: typing.Collection[str]) -> None:
        # Find filenames #######################################################

        def _is_config_h(fname: str) -> bool:
            if not fname.startswith(arg_base_dir + "/"):
                return False
            suffix = fname[len(arg_base_dir) + 1 :]
            if suffix.startswith("3rd-party/"):
                return False
            return suffix.endswith("/config.h")

        config_h_fname = util.get_zero_or_one(_is_config_h, arg_c_fnames)

        lib9p_srv_c_fname = util.get_zero_or_one(
            lambda fname: fname.endswith("lib9p/srv.c"), arg_c_fnames
        )

        lib9p_generated_c_fname = util.get_zero_or_one(
            lambda fname: fname.endswith("lib9p/9p.generated.c"), arg_c_fnames
        )

        # Read config ##########################################################

        def config_h_get(varname: str) -> int | None:
            if config_h_fname:
                with open(config_h_fname, "r", encoding="utf-8") as fh:
                    for line in fh:
                        line = line.rstrip()
                        if line.startswith("#define"):
                            parts = line.split()
                            if parts[1] == varname:
                                return int(parts[2])
            return None

        self._CONFIG_9P_NUM_SOCKS = config_h_get("_CONFIG_9P_NUM_SOCKS")
        self.CONFIG_9P_SRV_MAX_REQS = config_h_get("CONFIG_9P_SRV_MAX_REQS")
        self.CONFIG_9P_SRV_MAX_DEPTH = config_h_get("CONFIG_9P_SRV_MAX_DEPTH")

        # Read sources #########################################################

        tmessage_handlers: set[QName] | None = None
        if lib9p_srv_c_fname:
            tmessage_handlers = set()
            with open(lib9p_srv_c_fname, "r", encoding="utf-8") as fh:
                for line in fh:
                    line = line.rstrip()
                    if m := re_tmessage_handler.fullmatch(line):
                        tmessage_handlers.add(QName(m.group("handler")))
        self.tmessage_handlers = tmessage_handlers

        lib9p_msgs: set[str] = set()
        if lib9p_generated_c_fname:
            with open(lib9p_generated_c_fname, "r", encoding="utf-8") as fh:
                for line in fh:
                    line = line.rstrip()
                    if m := re_lib9p_msg_entry.fullmatch(line):
                        typ = m.group("typ")
                        lib9p_msgs.add(typ)
        self.lib9p_msgs = lib9p_msgs

    def thread_count(self, name: QName) -> int:
        assert self._CONFIG_9P_NUM_SOCKS
        assert self.CONFIG_9P_SRV_MAX_REQS
        if "read" in str(name.base()):
            return self._CONFIG_9P_NUM_SOCKS
        if "write" in str(name.base()):
            return self._CONFIG_9P_NUM_SOCKS * self.CONFIG_9P_SRV_MAX_REQS
        return 1

    def is_intrhandler(self, name: QName) -> bool:
        return False

    def init_array(self) -> typing.Collection[QName]:
        return []

    def extra_includes(self) -> typing.Collection[BaseName]:
        return []

    def extra_nodes(self) -> typing.Collection[Node]:
        return []

    def indirect_callees(
        self, loc: str, line: str
    ) -> tuple[typing.Collection[QName], bool] | None:
        if "/3rd-party/" in loc:
            return None
        if (
            self.tmessage_handlers
            and "/srv.c:" in loc
            and "tmessage_handlers[typ](" in line
        ):
            # Functions for disabled protocol extensions will be missing.
            return self.tmessage_handlers, True
        if self.lib9p_msgs and "/9p.c:" in loc:
            for meth in ["validate", "unmarshal", "marshal"]:
                if line.startswith(f"tentry.{meth}("):
                    # Functions for disabled protocol extensions will be missing.
                    return [QName(f"{meth}_{msg}") for msg in self.lib9p_msgs], True
        return None

    def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
        ret: dict[BaseName, analyze.SkipModel] = {
            BaseName("_lib9p_validate"): analyze.SkipModel(
                2,
                self._skipmodel__lib9p_validate_unmarshal_marshal,
            ),
            BaseName("_lib9p_unmarshal"): analyze.SkipModel(
                2,
                self._skipmodel__lib9p_validate_unmarshal_marshal,
            ),
            BaseName("_lib9p_marshal"): analyze.SkipModel(
                2,
                self._skipmodel__lib9p_validate_unmarshal_marshal,
            ),
        }
        if isinstance(self.CONFIG_9P_SRV_MAX_DEPTH, int):
            ret[BaseName("srv_util_pathfree")] = analyze.SkipModel(
                self.CONFIG_9P_SRV_MAX_DEPTH,
                self._skipmodel_srv_util_pathfree,
            )
        return ret

    def _skipmodel__lib9p_validate_unmarshal_marshal(
        self, chain: typing.Sequence[QName], call: QName
    ) -> bool:
        m_caller = re_lib9p_caller.fullmatch(str(chain[-2].base()))
        assert m_caller

        m_callee = re_lib9p_callee.fullmatch(str(call.base()))
        if not m_callee:
            return False
        return m_caller.group("grp") != m_callee.group("grp")

    def _skipmodel_srv_util_pathfree(
        self, chain: typing.Sequence[QName], call: QName
    ) -> bool:
        assert isinstance(self.CONFIG_9P_SRV_MAX_DEPTH, int)
        if call.base() == BaseName("srv_util_pathfree"):
            return len(chain) >= self.CONFIG_9P_SRV_MAX_DEPTH and all(
                c.base() == BaseName("srv_util_pathfree")
                for c in chain[-self.CONFIG_9P_SRV_MAX_DEPTH :]
            )
        return False


class LibMiscPlugin:
    def is_intrhandler(self, name: QName) -> bool:
        return False

    def init_array(self) -> typing.Collection[QName]:
        return []

    def extra_includes(self) -> typing.Collection[BaseName]:
        return []

    def extra_nodes(self) -> typing.Collection[Node]:
        return []

    def indirect_callees(
        self, loc: str, line: str
    ) -> tuple[typing.Collection[QName], bool] | None:
        return None

    def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
        return {
            BaseName("__assert_msg_fail"): analyze.SkipModel(
                {BaseName("__assert_msg_fail")}, self._skipmodel___assert_msg_fail
            ),
        }

    def _skipmodel___assert_msg_fail(
        self, chain: typing.Sequence[QName], call: QName
    ) -> bool:
        if call.base() in [BaseName("__lm_printf"), BaseName("__lm_light_printf")]:
            return any(
                c.base() == BaseName("__assert_msg_fail") for c in reversed(chain[:-1])
            )
        return False


class PicoFmtPlugin:
    known_fct: dict[BaseName, BaseName]

    def __init__(self, arg_pico_platform: str) -> None:
        self.known_fct = {
            # pico_fmt
            BaseName("fmt_vsnprintf"): BaseName("_out_buffer"),
        }
        match arg_pico_platform:
            case "rp2040":
                self.known_fct.update(
                    {
                        # pico_stdio
                        BaseName("__wrap_vprintf"): BaseName("stdio_buffered_printer"),
                        BaseName("stdio_vprintf"): BaseName("stdio_buffered_printer"),
                        # libfmt
                        BaseName("__lm_light_printf"): BaseName("libfmt_light_fct"),
                    }
                )
            case "host":
                self.known_fct.update(
                    {
                        # libfmt
                        BaseName("__lm_printf"): BaseName("libfmt_libc_fct"),
                        BaseName("__lm_light_printf"): BaseName("libfmt_libc_fct"),
                    }
                )

    def is_intrhandler(self, name: QName) -> bool:
        return False

    def init_array(self) -> typing.Collection[QName]:
        return []

    def extra_includes(self) -> typing.Collection[BaseName]:
        return []

    def extra_nodes(self) -> typing.Collection[Node]:
        return []

    def indirect_callees(
        self, loc: str, line: str
    ) -> tuple[typing.Collection[QName], bool] | None:
        if "/3rd-party/pico-fmt/" not in loc:
            return None
        if "/printf.c:" in loc:
            m = util.re_call_other.fullmatch(line)
            call: str | None = m.group("func") if m else None
            if "->fct" in line:
                return [x.as_qname() for x in self.known_fct.values()], False
            if "specifier_table" in line:
                return [
                    # pico-fmt
                    QName("conv_sint"),
                    QName("conv_uint"),
                    # QName("conv_double"),
                    QName("conv_char"),
                    QName("conv_str"),
                    QName("conv_ptr"),
                    QName("conv_pct"),
                    # libfmt
                    QName("libfmt_conv_formatter"),
                    QName("libfmt_conv_quote"),
                ], False
        return None

    def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
        ret: dict[BaseName, analyze.SkipModel] = {
            BaseName("fmt_state_putchar"): analyze.SkipModel(
                self.known_fct.keys(), self._skipmodel_fmt_state_putchar
            ),
        }
        return ret

    def _skipmodel_fmt_state_putchar(
        self, chain: typing.Sequence[QName], call: QName
    ) -> bool:
        if call.base() in self.known_fct.values():
            fct: BaseName | None = None
            for pcall in reversed(chain):
                if pcall.base() in self.known_fct:
                    fct = self.known_fct[pcall.base()]
                    return call.base() != fct
            return True
        return False


class PicoSDKPlugin:
    get_init_array: typing.Callable[[], typing.Collection[QName]]
    app_init_array: typing.Collection[QName] | None
    app_preinit_array: typing.Collection[QName]

    def __init__(
        self,
        *,
        get_init_array: typing.Callable[[], typing.Collection[QName]],
    ) -> None:
        # grep for '__attribute__((constructor))' / '[[gnu::constructor]]'.
        self.get_init_array = get_init_array
        self.app_init_array = None

        # git grep '^PICO_RUNTIME_INIT_FUNC\S*('
        self.app_preinit_array = [
            # QName("runtime_init_mutex"),  # pico_mutex
            # QName("runtime_init_default_alarm_pool"),  # pico_time
            # QName("runtime_init_boot_locks_reset"),  # hardware_boot_lock
            QName("runtime_init_per_core_irq_priorities"),  # hardware_irq
            # QName("spinlock_set_extexclall"),  # hardware_sync_spin_lock
            QName("__aeabi_bits_init"),  # pico_bit_ops
            # QName("runtime_init_bootrom_locking_enable"),  # pico_bootrom, rp2350-only
            # QName("runtime_init_pre_core_tls_setup"), # pico_clib_interface, picolibc-only
            # QName("__aeabi_double_init"),  # pico_double
            # QName("__aeabi_float_init"),  # pico_float
            QName("__aeabi_mem_init"),  # pico_mem_ops
            QName("first_per_core_initializer"),  # pico_runtime
            # pico_runtime_init
            # QName("runtime_init_bootrom_reset"),  # rp2350-only
            # QName("runtime_init_per_core_bootrom_reset"),  # rp2350-only
            # QName("runtime_init_per_core_h3_irq_registers"),  # rp2350-only
            QName("runtime_init_early_resets"),
            QName("runtime_init_usb_power_down"),
            # QName("runtime_init_per_core_enable_coprocessors"), # PICO_RUNTIME_SKIP_INIT_PER_CORE_ENABLE_COPROCESSORS
            QName("runtime_init_clocks"),
            QName("runtime_init_post_clock_resets"),
            QName("runtime_init_rp2040_gpio_ie_disable"),
            QName("runtime_init_spin_locks_reset"),
            QName("runtime_init_install_ram_vector_table"),
        ]

    def is_intrhandler(self, name: QName) -> bool:
        return name.base() in [
            BaseName("isr_invalid"),
            BaseName("isr_nmi"),
            BaseName("isr_hardfault"),
            BaseName("isr_svcall"),
            BaseName("isr_pendsv"),
            BaseName("isr_systick"),
            *[BaseName(f"isr_irq{n}") for n in range(32)],
        ]

    def init_array(self) -> typing.Collection[QName]:
        return []

    def extra_includes(self) -> typing.Collection[BaseName]:
        return []

    def indirect_callees(
        self, loc: str, line: str
    ) -> tuple[typing.Collection[QName], bool] | None:
        if "/3rd-party/pico-sdk/" not in loc or "/3rd-party/pico-sdk/lib/" in loc:
            return None
        m = util.re_call_other.fullmatch(line)
        call: str | None = m.group("func") if m else None

        match call:
            case "connect_internal_flash_func":
                return [
                    QName("rom_func_lookup(ROM_FUNC_CONNECT_INTERNAL_FLASH)")
                ], False
            case "flash_exit_xip_func":
                return [QName("rom_func_lookup(ROM_FUNC_FLASH_EXIT_XIP)")], False
            case "flash_range_erase_func":
                return [QName("rom_func_lookup(ROM_FUNC_FLASH_RANGE_ERASE)")], False
            case "flash_flush_cache_func":
                return [QName("rom_func_lookup(ROM_FUNC_FLASH_FLUSH_CACHE)")], False
            case "rom_table_lookup":
                return [QName("rom_hword_as_ptr(BOOTROM_TABLE_LOOKUP_OFFSET)")], False
        if "/flash.c:" in loc and "boot2_copyout" in line:
            return [QName("_stage2_boot")], False
        if "/stdio.c:" in loc:
            if call == "out_func":
                return [
                    QName("stdio_out_chars_crlf"),
                    QName("stdio_out_chars_no_crlf"),
                ], False
            if call and (call.startswith("d->") or call.startswith("driver->")):
                _, meth = call.split("->", 1)
                match meth:
                    case "out_chars":
                        return [QName("stdio_uart_out_chars")], False
                    case "out_flush":
                        return [QName("stdio_uart_out_flush")], False
                    case "in_chars":
                        return [QName("stdio_uart_in_chars")], False
        if "/newlib_interface.c:" in loc:
            if line == "*p)();":
                if self.app_init_array is None:
                    self.app_init_array = self.get_init_array()
                return self.app_init_array, False
        if "/pico_runtime/runtime.c:" in loc:
            return self.app_preinit_array, False
        return None

    def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
        return {}

    def extra_nodes(self) -> typing.Collection[Node]:
        ret = []

        # src/rp2_common/hardware_divider/include/hardware/divider_helper.S
        save_div_state_and_lr = 5 * 4
        # src/rp2_common/pico_divider/divider_hardware.S
        save_div_state_and_lr_64 = 5 * 4

        # src/src/rp2_common/pico_crt0/crt0.S
        for n in range(32):
            ret += [synthetic_node(f"isr_irq{n}", 0, {"__unhandled_user_irq"})]
        ret += [
            synthetic_node("isr_invalid", 0, {"__unhandled_user_irq"}),
            synthetic_node("isr_nmi", 0, {"__unhandled_user_irq"}),
            synthetic_node("isr_hardfault", 0, {"__unhandled_user_irq"}),
            synthetic_node("isr_svcall", 0, {"__unhandled_user_irq"}),
            synthetic_node("isr_pendsv", 0, {"__unhandled_user_irq"}),
            synthetic_node("isr_systick", 0, {"__unhandled_user_irq"}),
            synthetic_node("__unhandled_user_irq", 0),
            synthetic_node("_entry_point", 0, {"_reset_handler"}),
            synthetic_node("_reset_handler", 0, {"runtime_init", "main", "exit"}),
        ]

        ret += [
            # src/rp2_common/pico_int64_ops/pico_int64_ops_aeabi.S
            synthetic_node("__wrap___aeabi_lmul", 4),
            # src/rp2_common/pico_divider/divider_hardware.S
            # s32 aliases
            synthetic_node("div_s32s32", 0, {"divmod_s32s32"}),
            synthetic_node("__wrap___aeabi_idiv", 0, {"divmod_s32s32"}),
            synthetic_node("__wrap___aeabi_idivmod", 0, {"divmod_s32s32"}),
            # s32 impl
            synthetic_node("divmod_s32s32", 0, {"divmod_s32s32_savestate"}),
            synthetic_node(
                "divmod_s32s32_savestate",
                save_div_state_and_lr,
                {"divmod_s32s32_unsafe"},
            ),
            synthetic_node("divmod_s32s32_unsafe", 2 * 4, {"__aeabi_idiv0"}),
            # u32 aliases
            synthetic_node("div_u32u32", 0, {"divmod_u32u32"}),
            synthetic_node("__wrap___aeabi_uidiv", 0, {"divmod_u32u32"}),
            synthetic_node("__wrap___aeabi_uidivmod", 0, {"divmod_u32u32"}),
            # u32 impl
            synthetic_node("divmod_u32u32", 0, {"divmod_u32u32_savestate"}),
            synthetic_node(
                "divmod_u32u32_savestate",
                save_div_state_and_lr,
                {"divmod_u32u32_unsafe"},
            ),
            synthetic_node("divmod_u32u32_unsafe", 2 * 4, {"__aeabi_idiv0"}),
            # s64 aliases
            synthetic_node("div_s64s64", 0, {"divmod_s64s64"}),
            synthetic_node("__wrap___aeabi_ldiv", 0, {"divmod_s64s64"}),
            synthetic_node("__wrap___aeabi_ldivmod", 0, {"divmod_s64s64"}),
            # s64 impl
            synthetic_node("divmod_s64s64", 0, {"divmod_s64s64_savestate"}),
            synthetic_node(
                "divmod_s64s64_savestate",
                save_div_state_and_lr_64 + (2 * 4),
                {"divmod_s64s64_unsafe"},
            ),
            synthetic_node(
                "divmod_s64s64_unsafe", 4, {"divmod_u64u64_unsafe", "__aeabi_ldiv0"}
            ),
            # u64 aliases
            synthetic_node("div_u64u64", 0, {"divmod_u64u64"}),
            synthetic_node("__wrap___aeabi_uldivmod", 0, {"divmod_u64u64"}),
            # u64 impl
            synthetic_node("divmod_u64u64", 0, {"divmod_u64u64_savestate"}),
            synthetic_node(
                "divmod_u64u64_savestate",
                save_div_state_and_lr_64 + (2 * 4),
                {"divmod_u64u64_unsafe"},
            ),
            synthetic_node(
                "divmod_u64u64_unsafe", (1 + 1 + 2 + 5 + 5 + 2) * 4, {"__aeabi_ldiv0"}
            ),
            # *_rem
            synthetic_node("divod_s64s64_rem", 2 * 4, {"divmod_s64s64"}),
            synthetic_node("divod_u64u64_rem", 2 * 4, {"divmod_u64u64"}),
            # src/rp2_common/pico_mem_ops/mem_ops_aeabi.S
            synthetic_node("__aeabi_mem_init", 0, {"rom_funcs_lookup"}),
            synthetic_node(
                "__wrap___aeabi_memset", 0, {"rom_func_lookup(ROM_FUNC_MEMSET)"}
            ),
            synthetic_node("__wrap___aeabi_memset4", 0, {"__wrap___aeabi_memset8"}),
            synthetic_node(
                "__wrap___aeabi_memset8", 0, {"rom_func_lookup(ROM_FUNC_MEMSET4)"}
            ),
            synthetic_node("__wrap___aeabi_memcpy4", 0, {"__wrap___aeabi_memcpy8"}),
            synthetic_node(
                "__wrap___aeabi_memcpy7", 0, {"rom_func_lookup(ROM_FUNC_MEMCPY4)"}
            ),
            synthetic_node("__wrap_memset", 0, {"rom_func_lookup(ROM_FUNC_MEMSET)"}),
            synthetic_node("__wrap___aeabi_memcpy", 0, {"__wrap_memcpy"}),
            synthetic_node("__wrap_memcpy", 0, {"rom_func_lookup(ROM_FUNC_MEMCPY)"}),
            # src/rp2_common/pico_bit_ops/bit_ops_aeabi.S
            synthetic_node("__aeabi_bits_init", 0, {"rom_funcs_lookup"}),
            synthetic_node("__wrap___clz", 0, {"__wrap___clzsi2"}),
            synthetic_node("__wrap___clzl", 0, {"__wrap___clzsi2"}),
            synthetic_node("__wrap___clzsi2", 0, {"rom_func_lookup(ROM_FUNC_CLZ32)"}),
            synthetic_node("__wrap___ctzsi2", 0, {"rom_func_lookup(ROM_FUNC_CTZ32)"}),
            synthetic_node(
                "__wrap___popcountsi2", 0, {"rom_func_lookup(ROM_FUNC_POPCOUNT32)"}
            ),
            synthetic_node("__wrap___clzll", 0, {"__wrap___clzdi2"}),
            synthetic_node("__wrap___clzdi2", 4, {"rom_func_lookup(ROM_FUNC_CLZ32)"}),
            synthetic_node("__wrap___ctzdi2", 4, {"rom_func_lookup(ROM_FUNC_CTZ32)"}),
            synthetic_node(
                "__wrap___popcountdi2", 3 * 4, {"rom_func_lookup(ROM_FUNC_POPCOUNT32)"}
            ),
            synthetic_node("__rev", 0, {"reverse32"}),
            synthetic_node("__revl", 0, {"reverse32"}),
            synthetic_node("reverse32", 0, {"rom_func_lookup(ROM_FUNC_REVERSE32)"}),
            synthetic_node("__revll", 0, {"reverse64"}),
            synthetic_node("reverse64", 3 * 4, {"rom_func_lookup(ROM_FUNC_REVERSE32)"}),
            # src/rp2040/boot_stage2/boot2_${name,,}.S for name=W25Q080,
            # controlled by `#define PICO_BOOT_STAGE2_{name} 1` in
            # src/boards/include/boards/pico.h
            # synthetic_node("_stage2_boot", 0),  # TODO
            # https://github.com/raspberrypi/pico-bootrom-rp2040
            # synthetic_node("rom_func_lookup(ROM_FUNC_CONNECT_INTERNAL_FLASH)", 0),  # TODO
            # synthetic_node("rom_func_lookup(ROM_FUNC_FLASH_EXIT_XIP)", 0),  # TODO
            # synthetic_node("rom_func_lookup(ROM_FUNC_FLASH_FLUSH_CACHE)", 0),  # TODO
            # synthetic_node("rom_hword_as_ptr(BOOTROM_TABLE_LOOKUP_OFFSET)", 0),  # TODO
        ]
        return ret


re_tud_class = re.compile(
    r"^\s*#\s*define\s+(?P<k>CFG_TUD_(?:\S{3}|AUDIO|VIDEO|MIDI|VENDOR|USBTMC|DFU_RUNTIME|ECM_RNDIS))\s+(?P<v>\S+).*"
)
re_tud_entry = re.compile(r"^\s+\.(?P<meth>\S+)\s*=\s*(?P<impl>[a-zA-Z0-9_]+)(?:,.*)?")
re_tud_if1 = re.compile(r"^\s*#\s*if (\S+)\s*")
re_tud_if2 = re.compile(r"^\s*#\s*if (\S+)\s*\|\|\s*(\S+)\s*")
re_tud_endif = re.compile(r"^\s*#\s*endif\s*")


class TinyUSBDevicePlugin:
    tud_drivers: dict[str, set[QName]]  # method_name => {method_impls}

    def __init__(self, arg_c_fnames: typing.Collection[str]) -> None:
        usbd_c_fname = util.get_zero_or_one(
            lambda fname: fname.endswith("/tinyusb/src/device/usbd.c"), arg_c_fnames
        )

        tusb_config_h_fname = util.get_zero_or_one(
            lambda fname: fname.endswith("/tusb_config.h"), arg_c_fnames
        )

        if not usbd_c_fname:
            self.tud_drivers = {}
            return

        assert tusb_config_h_fname
        tusb_config: dict[str, bool] = {}
        with open(tusb_config_h_fname, "r", encoding="utf-8") as fh:
            in_table = False
            for line in fh:
                line = line.rstrip()
                if m := re_tud_class.fullmatch(line):
                    k = m.group("k")
                    v = m.group("v")
                    tusb_config[k] = bool(int(v))

        tud_drivers: dict[str, set[QName]] = {}
        with open(usbd_c_fname, "r", encoding="utf-8") as fh:
            in_table = False
            enabled = True
            for line in fh:
                line = line.rstrip()
                if in_table:
                    if m := re_tud_if1.fullmatch(line):
                        enabled = tusb_config[m.group(1)]
                    elif m := re_tud_if2.fullmatch(line):
                        enabled = tusb_config[m.group(1)] or tusb_config[m.group(2)]
                    elif re_tud_endif.fullmatch(line):
                        enabled = True
                    if m := re_tud_entry.fullmatch(line):
                        meth = m.group("meth")
                        impl = m.group("impl")
                        if meth == "name" or not enabled:
                            continue
                        if meth not in tud_drivers:
                            tud_drivers[meth] = set()
                        if impl != "NULL":
                            tud_drivers[meth].add(QName(impl))
                    if line.startswith("}"):
                        in_table = False
                elif " _usbd_driver[] = {" in line:
                    in_table = True
        self.tud_drivers = tud_drivers

    def is_intrhandler(self, name: QName) -> bool:
        return False

    def init_array(self) -> typing.Collection[QName]:
        return []

    def extra_includes(self) -> typing.Collection[BaseName]:
        return []

    def extra_nodes(self) -> typing.Collection[Node]:
        return []

    def indirect_callees(
        self, loc: str, line: str
    ) -> tuple[typing.Collection[QName], bool] | None:
        if "/tinyusb/" not in loc or "/tinyusb/src/host/" in loc or "_host.c:" in loc:
            return None
        m = util.re_call_other.fullmatch(line)
        assert m
        call = m.group("func")
        if call == "_ctrl_xfer.complete_cb":
            ret = {
                # QName("process_test_mode_cb"),
                QName("tud_vendor_control_xfer_cb"),
            }
            ret.update(self.tud_drivers["control_xfer_cb"])
            return ret, False
        if call.startswith("driver->"):
            return self.tud_drivers[call[len("driver->") :]], False
        if call == "event.func_call.func":
            # callback from usb_defer_func()
            return [], False

        return None

    def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
        return {}


class NewlibPlugin:
    def is_intrhandler(self, name: QName) -> bool:
        return False

    def init_array(self) -> typing.Collection[QName]:
        return [QName("register_fini")]

    def extra_includes(self) -> typing.Collection[BaseName]:
        return [
            # register_fini() calls atexit(__libc_fini_array)
            BaseName("__libc_fini_array"),
        ]

    def extra_nodes(self) -> typing.Collection[Node]:
        # This is accurate to
        # /usr/arm-none-eabi/lib/thumb/v6-m/nofp/libg.a as of
        # Parabola's arm-none-eabi-newlib 4.5.0.20241231-1.
        return [
            # malloc
            synthetic_node("free", 8, {"_free_r"}),
            synthetic_node("malloc", 8, {"_malloc_r"}),
            synthetic_node("realloc", 8, {"_realloc_r"}),
            synthetic_node("aligned_alloc", 8, {"_memalign_r"}),
            synthetic_node("reallocarray", 24, {"realloc", "__errno"}),
            # synthetic_node("_free_r", 0),  # TODO
            # synthetic_node("_malloc_r", 0),  # TODO
            # synthetic_node("_realloc_r", 0),  # TODO
            # synthetic_node("_memalign_r", 0),  # TODO
            # execution
            synthetic_node("raise", 16, {"_getpid_r"}),
            synthetic_node("abort", 8, {"raise", "_exit"}),
            synthetic_node("longjmp", 0),
            synthetic_node("setjmp", 0),
            # <strings.h>
            synthetic_node("memcmp", 12),
            synthetic_node("memcpy", 28),
            synthetic_node("memset", 20),
            synthetic_node("strcmp", 16),
            synthetic_node("strlen", 8),
            synthetic_node("strncpy", 16),
            synthetic_node("strnlen", 8),
            # other
            synthetic_node("__errno", 0),
            synthetic_node("_getpid_r", 8, {"_getpid"}),
            synthetic_node("random", 8),
            synthetic_node("register_fini", 8, {"atexit"}),
            synthetic_node("atexit", 8, {"__register_exitproc"}),
            synthetic_node(
                "__register_exitproc",
                32,
                {
                    "__retarget_lock_acquire_recursive",
                    "__retarget_lock_release_recursive",
                },
            ),
            synthetic_node("__libc_fini_array", 16, {"_fini"}),
        ]

    def indirect_callees(
        self, loc: str, line: str
    ) -> tuple[typing.Collection[QName], bool] | None:
        return None

    def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
        return {}


class LibGCCPlugin:
    def is_intrhandler(self, name: QName) -> bool:
        return False

    def init_array(self) -> typing.Collection[QName]:
        return [
            QName("libfmt_install_formatter"),
            QName("libfmt_install_quote"),
        ]

    def extra_includes(self) -> typing.Collection[BaseName]:
        return []

    def extra_nodes(self) -> typing.Collection[Node]:
        # This is accurate to Parabola's arm-none-eabi-gcc 14.2.0-1.
        return [
            # /usr/lib/gcc/arm-none-eabi/14.2.0/thumb/v6-m/nofp/libgcc.a
            synthetic_node("__aeabi_idiv0", 0),
            synthetic_node("__aeabi_ldiv0", 0),
            synthetic_node("__aeabi_llsr", 0),
            # /usr/lib/gcc/arm-none-eabi/14.2.0/thumb/v6-m/nofp/crti.o
            synthetic_node("_fini", 24),
        ]

    def indirect_callees(
        self, loc: str, line: str
    ) -> tuple[typing.Collection[QName], bool] | None:
        return None

    def skipmodels(self) -> dict[BaseName, analyze.SkipModel]:
        return {}