mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Opcode registry: move to generated canonical+alias pipeline
Introduce data-driven opcode registry with canonical and alias sources: - Added Data/opcodes/canonical.json as the single canonical LogicalOpcode set. - Added Data/opcodes/aliases.json for cross-core naming aliases (CMaNGOS/AzerothCore/local legacy). Added generator and generated include fragments: - tools/gen_opcode_registry.py emits include/game/opcode_enum_generated.inc, include/game/opcode_names_generated.inc, and include/game/opcode_aliases_generated.inc. - include/game/opcode_table.hpp now consumes generated enum entries. - src/game/opcode_table.cpp now consumes generated name and alias tables. Loader canonicalization behavior: - OpcodeTable::nameToLogical canonicalizes incoming JSON opcode names via alias table before enum lookup, so implementation code stays stable while expansion maps can use different core spellings. Validation and build integration: - Added tools/validate_opcode_maps.py to validate canonical contract across expansions. - Added CMake targets opcodes-generate and opcodes-validate. - wowee target now depends on opcodes-generate so generated headers stay current. Validation/build run: - cmake -S . -B build - cmake --build build --target opcodes-generate opcodes-validate - cmake --build build -j32
This commit is contained in:
parent
52ac3bcba3
commit
610ed71922
10 changed files with 4572 additions and 2812 deletions
|
|
@ -15,6 +15,23 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
|||
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
|
||||
option(WOWEE_BUILD_TESTS "Build tests" OFF)
|
||||
|
||||
# Opcode registry generation/validation
|
||||
find_package(Python3 COMPONENTS Interpreter QUIET)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_custom_target(opcodes-generate
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/gen_opcode_registry.py
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMENT "Generating opcode registry include fragments"
|
||||
)
|
||||
|
||||
add_custom_target(opcodes-validate
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/validate_opcode_maps.py --root ${CMAKE_SOURCE_DIR}
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
DEPENDS opcodes-generate
|
||||
COMMENT "Validating canonical opcode registry and expansion maps"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Find required packages
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
|
|
@ -322,6 +339,9 @@ endif()
|
|||
|
||||
# Create executable
|
||||
add_executable(wowee ${WOWEE_SOURCES} ${WOWEE_HEADERS} ${WOWEE_PLATFORM_SOURCES})
|
||||
if(TARGET opcodes-generate)
|
||||
add_dependencies(wowee opcodes-generate)
|
||||
endif()
|
||||
|
||||
# Include directories
|
||||
target_include_directories(wowee PRIVATE
|
||||
|
|
|
|||
47
Data/opcodes/aliases.json
Normal file
47
Data/opcodes/aliases.json
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"aliases": {
|
||||
"CMSG_GAMEOBJECT_USE": "CMSG_GAMEOBJ_USE",
|
||||
"CMSG_GUILD_DECLINE_INVITATION": "CMSG_GUILD_DECLINE",
|
||||
"CMSG_GUILD_DEMOTE_MEMBER": "CMSG_GUILD_DEMOTE",
|
||||
"CMSG_GUILD_GET_ROSTER": "CMSG_GUILD_ROSTER",
|
||||
"CMSG_GUILD_PROMOTE_MEMBER": "CMSG_GUILD_PROMOTE",
|
||||
"CMSG_MOVE_FALL_LAND": "MSG_MOVE_FALL_LAND",
|
||||
"CMSG_MOVE_HEARTBEAT": "MSG_MOVE_HEARTBEAT",
|
||||
"CMSG_MOVE_JUMP": "MSG_MOVE_JUMP",
|
||||
"CMSG_MOVE_SET_FACING": "MSG_MOVE_SET_FACING",
|
||||
"CMSG_MOVE_START_BACKWARD": "MSG_MOVE_START_BACKWARD",
|
||||
"CMSG_MOVE_START_FORWARD": "MSG_MOVE_START_FORWARD",
|
||||
"CMSG_MOVE_START_STRAFE_LEFT": "MSG_MOVE_START_STRAFE_LEFT",
|
||||
"CMSG_MOVE_START_STRAFE_RIGHT": "MSG_MOVE_START_STRAFE_RIGHT",
|
||||
"CMSG_MOVE_START_SWIM": "MSG_MOVE_START_SWIM",
|
||||
"CMSG_MOVE_START_TURN_LEFT": "MSG_MOVE_START_TURN_LEFT",
|
||||
"CMSG_MOVE_START_TURN_RIGHT": "MSG_MOVE_START_TURN_RIGHT",
|
||||
"CMSG_MOVE_STOP": "MSG_MOVE_STOP",
|
||||
"CMSG_MOVE_STOP_STRAFE": "MSG_MOVE_STOP_STRAFE",
|
||||
"CMSG_MOVE_STOP_SWIM": "MSG_MOVE_STOP_SWIM",
|
||||
"CMSG_MOVE_STOP_TURN": "MSG_MOVE_STOP_TURN",
|
||||
"CMSG_REQUEST_PLAYED_TIME": "CMSG_PLAYED_TIME",
|
||||
"CMSG_SHOWING_CLOAK": "CMSG_SHOWING_CLOAK",
|
||||
"CMSG_SHOWING_HELM": "CMSG_SHOWING_HELM",
|
||||
"CMSG_STAND_STATE_CHANGE": "CMSG_STANDSTATECHANGE",
|
||||
"CMSG_TOGGLE_CLOAK": "CMSG_SHOWING_CLOAK",
|
||||
"CMSG_TOGGLE_HELM": "CMSG_SHOWING_HELM",
|
||||
"SMSG_BATTLEFIELD_PORT_DENIED": "SMSG_BATTLEFIELD_PORT_DENIED",
|
||||
"SMSG_CAST_FAILED": "SMSG_CAST_FAILED",
|
||||
"SMSG_CAST_RESULT": "SMSG_CAST_FAILED",
|
||||
"SMSG_ENVIRONMENTALDAMAGELOG": "SMSG_ENVIRONMENTAL_DAMAGE_LOG",
|
||||
"SMSG_INIT_EXTRA_AURA_INFO": "SMSG_INIT_EXTRA_AURA_INFO_OBSOLETE",
|
||||
"SMSG_INSPECT": "SMSG_INSPECT_RESULTS_UPDATE",
|
||||
"SMSG_INSPECT_RESULTS": "SMSG_INSPECT_RESULTS_UPDATE",
|
||||
"SMSG_PUREMOUNT_CANCELLED_OBSOLETE": "SMSG_REMOVED_FROM_PVP_QUEUE",
|
||||
"SMSG_QUEST_FORCE_REMOVE": "SMSG_QUEST_FORCE_REMOVE",
|
||||
"SMSG_REMOVED_FROM_PVP_QUEUE": "SMSG_REMOVED_FROM_PVP_QUEUE",
|
||||
"SMSG_SET_EXTRA_AURA_INFO": "SMSG_SET_EXTRA_AURA_INFO_OBSOLETE",
|
||||
"SMSG_SET_REST_START": "SMSG_QUEST_FORCE_REMOVE",
|
||||
"SMSG_SPLINE_MOVE_SET_RUN_BACK_SPEED": "SMSG_SPLINE_SET_RUN_BACK_SPEED",
|
||||
"SMSG_SPLINE_MOVE_SET_RUN_SPEED": "SMSG_SPLINE_SET_RUN_SPEED",
|
||||
"SMSG_SPLINE_MOVE_SET_SWIM_SPEED": "SMSG_SPLINE_SET_SWIM_SPEED",
|
||||
"SMSG_UPDATE_AURA_DURATION": "SMSG_EQUIPMENT_SET_SAVED",
|
||||
"SMSG_VICTIMSTATEUPDATE_OBSOLETE": "SMSG_BATTLEFIELD_PORT_DENIED"
|
||||
}
|
||||
}
|
||||
1391
Data/opcodes/canonical.json
Normal file
1391
Data/opcodes/canonical.json
Normal file
File diff suppressed because it is too large
Load diff
45
include/game/opcode_aliases_generated.inc
Normal file
45
include/game/opcode_aliases_generated.inc
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// GENERATED FILE - DO NOT EDIT
|
||||
|
||||
{"CMSG_GAMEOBJECT_USE", "CMSG_GAMEOBJ_USE"},
|
||||
{"CMSG_GUILD_DECLINE_INVITATION", "CMSG_GUILD_DECLINE"},
|
||||
{"CMSG_GUILD_DEMOTE_MEMBER", "CMSG_GUILD_DEMOTE"},
|
||||
{"CMSG_GUILD_GET_ROSTER", "CMSG_GUILD_ROSTER"},
|
||||
{"CMSG_GUILD_PROMOTE_MEMBER", "CMSG_GUILD_PROMOTE"},
|
||||
{"CMSG_MOVE_FALL_LAND", "MSG_MOVE_FALL_LAND"},
|
||||
{"CMSG_MOVE_HEARTBEAT", "MSG_MOVE_HEARTBEAT"},
|
||||
{"CMSG_MOVE_JUMP", "MSG_MOVE_JUMP"},
|
||||
{"CMSG_MOVE_SET_FACING", "MSG_MOVE_SET_FACING"},
|
||||
{"CMSG_MOVE_START_BACKWARD", "MSG_MOVE_START_BACKWARD"},
|
||||
{"CMSG_MOVE_START_FORWARD", "MSG_MOVE_START_FORWARD"},
|
||||
{"CMSG_MOVE_START_STRAFE_LEFT", "MSG_MOVE_START_STRAFE_LEFT"},
|
||||
{"CMSG_MOVE_START_STRAFE_RIGHT", "MSG_MOVE_START_STRAFE_RIGHT"},
|
||||
{"CMSG_MOVE_START_SWIM", "MSG_MOVE_START_SWIM"},
|
||||
{"CMSG_MOVE_START_TURN_LEFT", "MSG_MOVE_START_TURN_LEFT"},
|
||||
{"CMSG_MOVE_START_TURN_RIGHT", "MSG_MOVE_START_TURN_RIGHT"},
|
||||
{"CMSG_MOVE_STOP", "MSG_MOVE_STOP"},
|
||||
{"CMSG_MOVE_STOP_STRAFE", "MSG_MOVE_STOP_STRAFE"},
|
||||
{"CMSG_MOVE_STOP_SWIM", "MSG_MOVE_STOP_SWIM"},
|
||||
{"CMSG_MOVE_STOP_TURN", "MSG_MOVE_STOP_TURN"},
|
||||
{"CMSG_REQUEST_PLAYED_TIME", "CMSG_PLAYED_TIME"},
|
||||
{"CMSG_SHOWING_CLOAK", "CMSG_SHOWING_CLOAK"},
|
||||
{"CMSG_SHOWING_HELM", "CMSG_SHOWING_HELM"},
|
||||
{"CMSG_STAND_STATE_CHANGE", "CMSG_STANDSTATECHANGE"},
|
||||
{"CMSG_TOGGLE_CLOAK", "CMSG_SHOWING_CLOAK"},
|
||||
{"CMSG_TOGGLE_HELM", "CMSG_SHOWING_HELM"},
|
||||
{"SMSG_BATTLEFIELD_PORT_DENIED", "SMSG_BATTLEFIELD_PORT_DENIED"},
|
||||
{"SMSG_CAST_FAILED", "SMSG_CAST_FAILED"},
|
||||
{"SMSG_CAST_RESULT", "SMSG_CAST_FAILED"},
|
||||
{"SMSG_ENVIRONMENTALDAMAGELOG", "SMSG_ENVIRONMENTAL_DAMAGE_LOG"},
|
||||
{"SMSG_INIT_EXTRA_AURA_INFO", "SMSG_INIT_EXTRA_AURA_INFO_OBSOLETE"},
|
||||
{"SMSG_INSPECT", "SMSG_INSPECT_RESULTS_UPDATE"},
|
||||
{"SMSG_INSPECT_RESULTS", "SMSG_INSPECT_RESULTS_UPDATE"},
|
||||
{"SMSG_PUREMOUNT_CANCELLED_OBSOLETE", "SMSG_REMOVED_FROM_PVP_QUEUE"},
|
||||
{"SMSG_QUEST_FORCE_REMOVE", "SMSG_QUEST_FORCE_REMOVE"},
|
||||
{"SMSG_REMOVED_FROM_PVP_QUEUE", "SMSG_REMOVED_FROM_PVP_QUEUE"},
|
||||
{"SMSG_SET_EXTRA_AURA_INFO", "SMSG_SET_EXTRA_AURA_INFO_OBSOLETE"},
|
||||
{"SMSG_SET_REST_START", "SMSG_QUEST_FORCE_REMOVE"},
|
||||
{"SMSG_SPLINE_MOVE_SET_RUN_BACK_SPEED", "SMSG_SPLINE_SET_RUN_BACK_SPEED"},
|
||||
{"SMSG_SPLINE_MOVE_SET_RUN_SPEED", "SMSG_SPLINE_SET_RUN_SPEED"},
|
||||
{"SMSG_SPLINE_MOVE_SET_SWIM_SPEED", "SMSG_SPLINE_SET_SWIM_SPEED"},
|
||||
{"SMSG_UPDATE_AURA_DURATION", "SMSG_EQUIPMENT_SET_SAVED"},
|
||||
{"SMSG_VICTIMSTATEUPDATE_OBSOLETE", "SMSG_BATTLEFIELD_PORT_DENIED"},
|
||||
1389
include/game/opcode_enum_generated.inc
Normal file
1389
include/game/opcode_enum_generated.inc
Normal file
File diff suppressed because it is too large
Load diff
1389
include/game/opcode_names_generated.inc
Normal file
1389
include/game/opcode_names_generated.inc
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
96
tools/gen_opcode_registry.py
Normal file
96
tools/gen_opcode_registry.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate opcode registry include fragments from data files.
|
||||
|
||||
Inputs:
|
||||
- Data/opcodes/canonical.json
|
||||
- Data/opcodes/aliases.json
|
||||
|
||||
Outputs:
|
||||
- include/game/opcode_enum_generated.inc
|
||||
- include/game/opcode_names_generated.inc
|
||||
- include/game/opcode_aliases_generated.inc
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
RE_NAME = re.compile(r"^(?:CMSG|SMSG|MSG)_[A-Z0-9_]+$")
|
||||
|
||||
|
||||
def load_canonical(path: Path) -> list[str]:
|
||||
data = json.loads(path.read_text())
|
||||
names = data.get("logical_opcodes", [])
|
||||
if not isinstance(names, list):
|
||||
raise ValueError("canonical.json: logical_opcodes must be a list")
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for raw in names:
|
||||
if not isinstance(raw, str) or not RE_NAME.match(raw):
|
||||
raise ValueError(f"Invalid canonical opcode name: {raw!r}")
|
||||
if raw in seen:
|
||||
continue
|
||||
seen.add(raw)
|
||||
out.append(raw)
|
||||
return out
|
||||
|
||||
|
||||
def load_aliases(path: Path, canonical: set[str]) -> dict[str, str]:
|
||||
data = json.loads(path.read_text())
|
||||
aliases = data.get("aliases", {})
|
||||
if not isinstance(aliases, dict):
|
||||
raise ValueError("aliases.json: aliases must be an object")
|
||||
out: dict[str, str] = {}
|
||||
for alias, target in sorted(aliases.items()):
|
||||
if not isinstance(alias, str) or not RE_NAME.match(alias):
|
||||
raise ValueError(f"Invalid alias opcode name: {alias!r}")
|
||||
if not isinstance(target, str) or not RE_NAME.match(target):
|
||||
raise ValueError(f"Invalid alias target opcode name: {target!r}")
|
||||
if target not in canonical:
|
||||
raise ValueError(f"Alias target not in canonical set: {alias} -> {target}")
|
||||
out[alias] = target
|
||||
return out
|
||||
|
||||
|
||||
def write_file(path: Path, content: str) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
data_dir = root / "Data/opcodes"
|
||||
inc_dir = root / "include/game"
|
||||
|
||||
canonical_names = load_canonical(data_dir / "canonical.json")
|
||||
canonical_set = set(canonical_names)
|
||||
aliases = load_aliases(data_dir / "aliases.json", canonical_set)
|
||||
|
||||
enum_lines = ["// GENERATED FILE - DO NOT EDIT", ""]
|
||||
enum_lines += [f" {name}," for name in canonical_names]
|
||||
enum_content = "\n".join(enum_lines) + "\n"
|
||||
|
||||
name_lines = ["// GENERATED FILE - DO NOT EDIT", ""]
|
||||
name_lines += [f' {{"{name}", LogicalOpcode::{name}}},' for name in canonical_names]
|
||||
names_content = "\n".join(name_lines) + "\n"
|
||||
|
||||
alias_lines = ["// GENERATED FILE - DO NOT EDIT", ""]
|
||||
alias_lines += [f' {{"{alias}", "{target}"}},' for alias, target in aliases.items()]
|
||||
aliases_content = "\n".join(alias_lines) + "\n"
|
||||
|
||||
write_file(inc_dir / "opcode_enum_generated.inc", enum_content)
|
||||
write_file(inc_dir / "opcode_names_generated.inc", names_content)
|
||||
write_file(inc_dir / "opcode_aliases_generated.inc", aliases_content)
|
||||
|
||||
print(
|
||||
f"generated: canonical={len(canonical_names)} aliases={len(aliases)} "
|
||||
f"-> include/game/opcode_*_generated.inc"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
171
tools/validate_opcode_maps.py
Normal file
171
tools/validate_opcode_maps.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validate opcode canonicalization and expansion mappings.
|
||||
|
||||
Checks:
|
||||
1. Every enum opcode appears in kOpcodeNames.
|
||||
2. Every expansion JSON key resolves to a canonical opcode name (direct or alias).
|
||||
3. Every opcode referenced as Opcode::<NAME> in implementation code exists in each expansion map
|
||||
after alias canonicalization.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Set
|
||||
|
||||
|
||||
RE_OPCODE_NAME = re.compile(r"^(?:CMSG|SMSG|MSG)_[A-Z0-9_]+$")
|
||||
RE_CODE_REF = re.compile(r"\bOpcode::((?:CMSG|SMSG|MSG)_[A-Z0-9_]+)\b")
|
||||
|
||||
|
||||
def read_canonical_data(path: Path) -> Set[str]:
|
||||
data = json.loads(path.read_text())
|
||||
names = data.get("logical_opcodes", [])
|
||||
return {n for n in names if isinstance(n, str) and RE_OPCODE_NAME.match(n)}
|
||||
|
||||
|
||||
def read_alias_data(path: Path) -> Dict[str, str]:
|
||||
data = json.loads(path.read_text())
|
||||
aliases = data.get("aliases", {})
|
||||
out: Dict[str, str] = {}
|
||||
for k, v in aliases.items():
|
||||
if isinstance(k, str) and isinstance(v, str) and RE_OPCODE_NAME.match(k) and RE_OPCODE_NAME.match(v):
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def canonicalize(name: str, aliases: Dict[str, str]) -> str:
|
||||
seen: Set[str] = set()
|
||||
current = name
|
||||
while current in aliases and current not in seen:
|
||||
seen.add(current)
|
||||
current = aliases[current]
|
||||
return current
|
||||
|
||||
|
||||
def iter_expansion_files(expansions_dir: Path) -> Iterable[Path]:
|
||||
for p in sorted(expansions_dir.glob("*/opcodes.json")):
|
||||
yield p
|
||||
|
||||
|
||||
def load_expansion_names(path: Path) -> Dict[str, str]:
|
||||
data = json.loads(path.read_text())
|
||||
out: Dict[str, str] = {}
|
||||
for k, v in data.items():
|
||||
if RE_OPCODE_NAME.match(k):
|
||||
out[k] = str(v)
|
||||
return out
|
||||
|
||||
|
||||
def collect_code_refs(root: Path) -> Set[str]:
|
||||
refs: Set[str] = set()
|
||||
skip_suffixes = {
|
||||
"include/game/opcode_table.hpp",
|
||||
"src/game/opcode_table.cpp",
|
||||
}
|
||||
for p in list(root.glob("src/**/*.cpp")) + list(root.glob("include/**/*.hpp")):
|
||||
rel = p.as_posix()
|
||||
if rel in skip_suffixes:
|
||||
continue
|
||||
text = p.read_text(errors="ignore")
|
||||
for m in RE_CODE_REF.finditer(text):
|
||||
refs.add(m.group(1))
|
||||
return refs
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", default=".")
|
||||
parser.add_argument(
|
||||
"--strict-required",
|
||||
action="store_true",
|
||||
help="Fail when expansion maps miss opcodes referenced by implementation code.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.root).resolve()
|
||||
canonical_path = root / "Data/opcodes/canonical.json"
|
||||
aliases_path = root / "Data/opcodes/aliases.json"
|
||||
expansions_dir = root / "Data/expansions"
|
||||
|
||||
enum_names = read_canonical_data(canonical_path)
|
||||
aliases = read_alias_data(aliases_path)
|
||||
k_names = set(enum_names)
|
||||
code_refs = collect_code_refs(root)
|
||||
|
||||
problems: List[str] = []
|
||||
|
||||
missing_in_name_map = sorted(enum_names - k_names)
|
||||
if missing_in_name_map:
|
||||
problems.append(
|
||||
f"enum names missing from kOpcodeNames: {len(missing_in_name_map)} "
|
||||
f"(sample: {missing_in_name_map[:10]})"
|
||||
)
|
||||
|
||||
unknown_code_refs = sorted(r for r in code_refs if canonicalize(r, aliases) not in enum_names)
|
||||
if unknown_code_refs:
|
||||
problems.append(
|
||||
f"Opcode:: references not in enum/alias map: {len(unknown_code_refs)} "
|
||||
f"(sample: {unknown_code_refs[:10]})"
|
||||
)
|
||||
|
||||
print(f"Canonical enum names: {len(enum_names)}")
|
||||
print(f"kOpcodeNames entries: {len(k_names)}")
|
||||
print(f"Alias entries: {len(aliases)}")
|
||||
print(f"Opcode:: code references: {len(code_refs)}")
|
||||
|
||||
for exp_file in iter_expansion_files(expansions_dir):
|
||||
names = load_expansion_names(exp_file)
|
||||
canonical_names = {canonicalize(n, aliases) for n in names}
|
||||
unknown = sorted(n for n in canonical_names if n not in enum_names)
|
||||
missing_required = sorted(
|
||||
n for n in code_refs if canonicalize(n, aliases) not in canonical_names
|
||||
)
|
||||
|
||||
# Detect multiple raw names collapsing to one canonical name.
|
||||
collisions: Dict[str, List[str]] = {}
|
||||
for raw in names:
|
||||
c = canonicalize(raw, aliases)
|
||||
collisions.setdefault(c, []).append(raw)
|
||||
alias_collisions = sorted(
|
||||
(c, raws) for c, raws in collisions.items() if len(raws) > 1 and len(set(raws)) > 1
|
||||
)
|
||||
|
||||
print(
|
||||
f"[{exp_file.parent.name}] raw={len(names)} canonical={len(canonical_names)} "
|
||||
f"unknown={len(unknown)} missing_required={len(missing_required)} "
|
||||
f"alias_collisions={len(alias_collisions)}"
|
||||
)
|
||||
|
||||
if unknown:
|
||||
problems.append(
|
||||
f"{exp_file.parent.name}: unknown canonical names after aliasing: "
|
||||
f"{len(unknown)} (sample: {unknown[:10]})"
|
||||
)
|
||||
if missing_required and args.strict_required:
|
||||
problems.append(
|
||||
f"{exp_file.parent.name}: missing required opcodes from implementation refs: "
|
||||
f"{len(missing_required)} (sample: {missing_required[:10]})"
|
||||
)
|
||||
elif missing_required:
|
||||
print(
|
||||
f" warn: {exp_file.parent.name} missing required refs: "
|
||||
f"{len(missing_required)} (sample: {missing_required[:6]})"
|
||||
)
|
||||
|
||||
if problems:
|
||||
print("\nFAILED:")
|
||||
for p in problems:
|
||||
print(f"- {p}")
|
||||
return 1
|
||||
|
||||
print("\nOK: canonical opcode contract satisfied across expansions.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue