From 1ec7c0e418070ba48849da822dadb80e1c79713b Mon Sep 17 00:00:00 2001 From: sp00ktober <79335616+sp00ktober@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:34:31 +0200 Subject: [PATCH] fix(mhfu): rabbitizer enums hash by value; 1.16's hash leaks an object a call Co-Authored-By: Claude Opus 5.5 --- packages/mhfu/src/mhfu/mips.py | 10 ++++++++ packages/mhfu/tests/test_mips.py | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/packages/mhfu/src/mhfu/mips.py b/packages/mhfu/src/mhfu/mips.py index 7a1c8fec..32c1a32d 100644 --- a/packages/mhfu/src/mhfu/mips.py +++ b/packages/mhfu/src/mhfu/mips.py @@ -36,6 +36,16 @@ Register = rabbitizer.Enum """A GPR (`Gpr.a1`) or an FPU register (`Fpr.fa0`, which is $f12).""" + +def _value_hash(self: rabbitizer.Enum) -> int: + return hash(self.value) + + +# rabbitizer 1.16's Enum.__hash__ leaks an object a call, and the walkers hash registers and +# instruction ids in every dict and set they touch (10+ GB in one port's derivation). Equal enums +# have equal values, so this hash agrees with ==. +rabbitizer.Enum.__hash__ = _value_hash # type: ignore[method-assign] + _MASK = 0xFFFFFFFF CALL_CLOBBERS = frozenset( [Gpr.at, Gpr.v0, Gpr.v1, Gpr.a0, Gpr.a1, Gpr.a2, Gpr.a3, Gpr.t8, Gpr.t9, Gpr.ra] diff --git a/packages/mhfu/tests/test_mips.py b/packages/mhfu/tests/test_mips.py index 84abc70d..5a013e53 100644 --- a/packages/mhfu/tests/test_mips.py +++ b/packages/mhfu/tests/test_mips.py @@ -1,12 +1,18 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2026 sp00ktober +import gc +import re import struct +import subprocess +import sys +from pathlib import Path import pytest from mhfu import files from mhfu.memory import Image, Unmapped from mhfu.mips import Code, Fpr, Gpr, decode, is_prologue, move_source, writes from modkit_testing import mips as asm +from rabbitizer import InstrId, Instruction BASE = 0x0010_0000 PROLOGUE = asm.addiu("sp", "sp", -0x10) @@ -232,3 +238,37 @@ def test_game_code(game): } for s in code.switches.values(): assert s.targets and all(t in code.function(s.jr) for t in s.targets) + + +PACKAGES = Path(__file__).resolve().parents[2] + + +def test_hashing_an_enum_keeps_no_memory(): + keys = {Gpr.a0: 1} + gc.collect() + before = sys.getallocatedblocks() + for _ in range(10_000): + keys.get(Gpr.a1) + hash(InstrId.cpu_sb) + gc.collect() + assert sys.getallocatedblocks() - before < 100 + + +def test_an_operand_finds_its_register_key(): + ins = Instruction(0x8FA40010) # lw a0, 0x10(sp) + assert {Gpr.a0: 1}[ins.rt] == 1 and ins.uniqueId in {InstrId.cpu_lw} + + +def test_every_module_that_imports_rabbitizer_loads_the_hash(): + names = [] + for f in sorted(PACKAGES.glob("*/src/**/*.py")): + if re.search(r"^(import|from) rabbitizer", f.read_text(encoding="utf-8"), re.M): + src = next(p for p in f.parents if p.name == "src") + names.append( + ".".join(x for x in f.relative_to(src).with_suffix("").parts if x != "__init__") + ) + assert names + for n in names: # each alone: once any has loaded mhfu.mips the rest would pass for free + check = f"import importlib, sys; importlib.import_module({n!r}); " + check += "assert 'mhfu.mips' in sys.modules" + subprocess.run([sys.executable, "-c", check], check=True)