1
0

More decompiling work

This commit is contained in:
Aiden
2026-05-25 17:32:00 +10:00
parent 56829b6e0b
commit 07f48c76e0
22 changed files with 9837 additions and 5 deletions

830
h8536/table_xrefs.py Normal file
View File

@@ -0,0 +1,830 @@
from __future__ import annotations
import argparse
import json
import re
from collections.abc import Iterable, Mapping
from pathlib import Path
from typing import Any
from .formatting import h16, label_for
from .serial_semantics import DIRECT_TABLE_TO_LOGICAL_OFFSET, LOGICAL_TABLES
JsonObject = dict[str, Any]
TABLES: tuple[JsonObject, ...] = (
{
"name": "primary_value_table_candidate",
"logical_base_address": 0xE000,
"logical_range_end": 0xE3FF,
"negative_offset": 0x2000,
"element_candidate": "word_value",
"direct_addresses": [0xF900],
"direct_range_end": 0xF91F,
},
{
"name": "secondary_value_table_candidate",
"logical_base_address": 0xE400,
"logical_range_end": 0xE7FF,
"negative_offset": 0x1C00,
"element_candidate": "word_value",
"direct_addresses": [0xF940],
"direct_range_end": 0xF95F,
},
{
"name": "current_value_table_candidate",
"logical_base_address": 0xE800,
"logical_range_end": 0xEBFF,
"negative_offset": 0x1800,
"element_candidate": "word_value",
"direct_addresses": [0xF920],
"direct_range_end": 0xF93F,
},
{
"name": "flag_table_candidate",
"logical_base_address": 0xEC00,
"logical_range_end": 0xEFFF,
"negative_offset": 0x1400,
"element_candidate": "bit_flags",
"direct_addresses": [0xF980],
"direct_range_end": 0xF99F,
},
)
_TABLE_BY_NEGATIVE_OFFSET = {int(item["negative_offset"]): item for item in TABLES}
_TABLE_BY_DIRECT_ADDRESS = {
address: item
for item in TABLES
for address in item["direct_addresses"]
}
LCD_CORRELATION_TERMS = (
"CONNECT",
"CONNECT: OK",
"CONNECT: NOT ACT",
"NOT ACT",
"COMM LINK",
"COMPLETED",
)
def load_table_xref_input(path: Path) -> JsonObject:
with path.open("r", encoding="utf-8") as handle:
payload = json.load(handle)
if not isinstance(payload, dict) or "instructions" not in payload:
raise ValueError(f"{path} does not look like h8536_decompiler JSON output")
return payload
def analyze_table_xrefs(payload: Mapping[str, Any]) -> JsonObject:
instructions = _instruction_sequence(payload.get("instructions"))
functions = _function_ranges(payload)
semantic_accesses = _semantic_access_locations(payload)
accesses_by_table = {str(table["name"]): [] for table in TABLES}
for index, ins in enumerate(instructions):
for access in _logical_operand_accesses(instructions, index, functions, semantic_accesses):
accesses_by_table.setdefault(str(access["table"]), []).append(access)
for access in _direct_address_accesses(ins, functions, semantic_accesses):
accesses_by_table.setdefault(str(access["table"]), []).append(access)
tables: list[JsonObject] = []
for table in TABLES:
name = str(table["name"])
accesses = sorted(accesses_by_table.get(name, []), key=lambda item: int(item["instruction_address"]))
reads = sum(1 for access in accesses if access["access"] == "read")
writes = sum(1 for access in accesses if access["access"] == "write")
read_write = sum(1 for access in accesses if access["access"] == "read_write_candidate")
dynamic = sum(1 for access in accesses if access.get("index") == "dynamic")
static_offsets = sorted(
{
int(access["offset"])
for access in accesses
if isinstance(access.get("offset"), int)
}
)
tables.append(
{
"name": name,
"logical_base_address": table["logical_base_address"],
"logical_base_address_hex": h16(int(table["logical_base_address"])),
"logical_range_end": table["logical_range_end"],
"logical_range_end_hex": h16(int(table["logical_range_end"])),
"negative_offset": table["negative_offset"],
"negative_offset_hex": h16(int(table["negative_offset"])),
"element_candidate": table["element_candidate"],
"direct_addresses": table["direct_addresses"],
"direct_addresses_hex": [h16(int(address)) for address in table["direct_addresses"]],
"direct_range_end": table["direct_range_end"],
"direct_range_end_hex": h16(int(table["direct_range_end"])),
"access_count": len(accesses),
"read_count": reads,
"write_count": writes,
"read_write_candidate_count": read_write,
"dynamic_index_count": dynamic,
"static_offsets": static_offsets,
"static_offsets_hex": [h16(offset) for offset in static_offsets],
"functions": _summarize_functions(accesses),
"accesses": accesses,
}
)
return {
"kind": "table_xrefs",
"tables": tables,
"summary": {
"table_count": len(tables),
"access_count": sum(int(table["access_count"]) for table in tables),
"dynamic_index_count": sum(int(table["dynamic_index_count"]) for table in tables),
"source_instruction_count": len(instructions),
},
"lcd_correlation": _lcd_correlation_hints(payload),
"caveat": (
"Static offsets are emitted only when an index register value can be derived from "
"nearby immediate loads in the current JSON. Other indexed accesses are dynamic."
),
}
def generate_table_xref_report(payload: Mapping[str, Any], *, source_name: str = "") -> str:
analysis = analyze_table_xrefs(payload)
lines: list[str] = []
suffix = f" for {source_name}" if source_name else ""
lines.append(f"Table/Index Cross-Reference Report{suffix}")
lines.append("=" * len(lines[0]))
lines.append("")
lines.append(str(analysis["caveat"]))
lines.append("")
lines.extend(_format_lcd_correlation_lines(analysis.get("lcd_correlation")))
if lines[-1] != "":
lines.append("")
for table in analysis["tables"]:
name = str(table["name"])
direct = ", ".join(str(item) for item in table["direct_addresses_hex"])
lines.append(
f"{name} {table['logical_base_address_hex']}-{table['logical_range_end_hex']} "
f"(negative {table['negative_offset_hex']}; direct {direct}-{table['direct_range_end_hex']})"
)
lines.append(
f" accesses={table['access_count']} reads={table['read_count']} "
f"writes={table['write_count']} dynamic={table['dynamic_index_count']}"
)
offsets = table.get("static_offsets_hex") or []
if offsets:
lines.append(f" static offsets: {', '.join(str(item) for item in offsets[:16])}")
function_summaries = table.get("functions") or []
if function_summaries:
joined = ", ".join(
f"{item['label']}:{item['access_count']}" for item in function_summaries[:12]
)
lines.append(f" functions: {joined}")
accesses = table.get("accesses")
if isinstance(accesses, list) and accesses:
for access in accesses[:80]:
lines.append(f" - {_format_access_line(access)}")
if len(accesses) > 80:
lines.append(f" - ... {len(accesses) - 80} more accesses omitted")
else:
lines.append(" no references found in current JSON")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def _format_lcd_correlation_lines(value: Any) -> list[str]:
if not isinstance(value, Mapping):
return []
lines = ["LCD correlation hints"]
for hit in value.get("term_hits", []):
if not isinstance(hit, Mapping):
continue
term = hit.get("term")
count = int(hit.get("hit_count", 0))
if count:
samples = ", ".join(
f"{item['address_hex']} {item['trimmed']!r}"
for item in hit.get("hits", [])[:4]
if isinstance(item, Mapping)
)
lines.append(f" term {term!r}: {count} candidate hit(s): {samples}")
else:
lines.append(f" term {term!r}: no LCD/text candidate hits in current decompile")
builders = value.get("display_builder_targets", [])
if isinstance(builders, list) and builders:
parts = [
f"{item['target_hex']}:{item['xref_count']}"
for item in builders[:8]
if isinstance(item, Mapping)
]
lines.append(f" display builder xrefs: {', '.join(parts)}")
routines = value.get("lcd_driver_routines", [])
if isinstance(routines, list) and routines:
parts = [
f"{item['start_hex']} {item['role_hint']}"
for item in routines[:4]
if isinstance(item, Mapping)
]
lines.append(f" LCD driver routines: {', '.join(parts)}")
lines.append(
" caveat: LCD strings can be builder/script output; absence of a literal term does not disprove runtime composition."
)
return lines
def write_table_xrefs(input_path: Path, output_path: Path, *, as_json: bool = False) -> None:
payload = load_table_xref_input(input_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
if as_json:
analysis = analyze_table_xrefs(payload)
analysis["source"] = str(input_path)
output_path.write_text(json.dumps(analysis, indent=2), encoding="utf-8")
else:
output_path.write_text(generate_table_xref_report(payload, source_name=str(input_path)), encoding="utf-8")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Generate table/index cross-references for candidate serial protocol data tables.",
)
parser.add_argument(
"input",
nargs="?",
type=Path,
default=Path("build/rom_decompiled.json"),
help="structured JSON emitted by h8536_decompiler.py",
)
parser.add_argument(
"--out",
type=Path,
default=Path("build/rom_table_xrefs.txt"),
help="table cross-reference report output path",
)
parser.add_argument("--json", action="store_true", help="write structured JSON instead of text")
args = parser.parse_args(argv)
write_table_xrefs(args.input, args.out, as_json=args.json)
print(f"wrote {args.out}")
return 0
def _logical_operand_accesses(
instructions: list[JsonObject],
index: int,
functions: list[JsonObject],
semantic_accesses: Mapping[int, list[JsonObject]],
) -> list[JsonObject]:
ins = instructions[index]
accesses: list[JsonObject] = []
operands = str(ins.get("operands", ""))
for operand in _negative_indexed_operands(operands):
table = _TABLE_BY_NEGATIVE_OFFSET.get(int(operand["negative_offset"]))
if table is None:
continue
register = str(operand["index_register"])
known = _nearby_register_value(instructions, index, register)
offset: int | str = known if known is not None else "dynamic"
logical_address: int | None = None
if isinstance(offset, int):
logical_address = (int(table["logical_base_address"]) + offset) & 0xFFFF
access = _base_access(ins, functions, semantic_accesses)
access.update(
{
"table": table["name"],
"table_base_address": table["logical_base_address"],
"table_base_address_hex": h16(int(table["logical_base_address"])),
"kind": "logical_negative_indexed_access",
"operand": operand["operand"],
"negative_offset": operand["negative_offset"],
"negative_offset_hex": h16(int(operand["negative_offset"])),
"index_register": register,
"index": offset,
"offset": offset,
"access": _operand_access_kind(ins, str(operand["operand"])),
}
)
if logical_address is not None:
access["logical_address"] = logical_address
access["logical_address_hex"] = h16(logical_address)
accesses.append(access)
return accesses
def _direct_address_accesses(
ins: Mapping[str, Any],
functions: list[JsonObject],
semantic_accesses: Mapping[int, list[JsonObject]],
) -> list[JsonObject]:
accesses: list[JsonObject] = []
refs = _references(ins)
for address in refs:
logical_table = _table_for_logical_address(address)
if logical_table is not None:
accesses.append(
_direct_logical_address_access(ins, logical_table, address, functions, semantic_accesses),
)
continue
direct_table = _table_for_direct_candidate_address(address)
if direct_table is not None:
accesses.append(
_direct_candidate_address_access(ins, direct_table, address, functions, semantic_accesses),
)
return accesses
def _direct_logical_address_access(
ins: Mapping[str, Any],
table: Mapping[str, Any],
address: int,
functions: list[JsonObject],
semantic_accesses: Mapping[int, list[JsonObject]],
) -> JsonObject:
base = int(table["logical_base_address"])
offset = address - base
access = _base_access(ins, functions, semantic_accesses)
access.update(
{
"table": table["name"],
"table_base_address": base,
"table_base_address_hex": h16(base),
"kind": "direct_logical_address_access",
"direct_address": address,
"direct_address_hex": h16(address),
"logical_address": address,
"logical_address_hex": h16(address),
"index": offset,
"offset": offset,
"offset_hex": h16(offset),
"access": _access_direction(ins, address) or "read_write_candidate",
}
)
return access
def _direct_candidate_address_access(
ins: Mapping[str, Any],
table: Mapping[str, Any],
address: int,
functions: list[JsonObject],
semantic_accesses: Mapping[int, list[JsonObject]],
) -> JsonObject:
base = min(int(item) for item in table["direct_addresses"])
offset = address - base
access = _base_access(ins, functions, semantic_accesses)
logical_offset = DIRECT_TABLE_TO_LOGICAL_OFFSET.get(base)
access.update(
{
"table": table["name"],
"table_base_address": table["logical_base_address"],
"table_base_address_hex": h16(int(table["logical_base_address"])),
"kind": "direct_candidate_address_access",
"direct_address": address,
"direct_address_hex": h16(address),
"direct_base_address": base,
"direct_base_address_hex": h16(base),
"index": offset,
"offset": offset,
"offset_hex": h16(offset),
"access": _access_direction(ins, address) or "read_write_candidate",
}
)
if logical_offset is not None:
access["semantic_negative_offset"] = logical_offset
access["semantic_negative_offset_hex"] = h16(logical_offset)
return access
def _lcd_correlation_hints(payload: Mapping[str, Any]) -> JsonObject:
lcd_text = payload.get("lcd_text")
strings = []
if isinstance(lcd_text, Mapping) and isinstance(lcd_text.get("strings"), list):
strings = [item for item in lcd_text["strings"] if isinstance(item, Mapping)]
term_hits = []
for term in LCD_CORRELATION_TERMS:
hits = []
upper_term = term.upper()
for item in strings:
text = f"{item.get('text', '')} {item.get('trimmed', '')}".upper()
if upper_term not in text:
continue
hits.append(_lcd_string_summary(item))
term_hits.append(
{
"term": term,
"hit_count": len(hits),
"hits": hits[:24],
"status": "candidate_hits" if hits else "not_found",
}
)
builder_targets: dict[int, JsonObject] = {}
for item in strings:
for xref in item.get("xrefs", []):
if not isinstance(xref, Mapping):
continue
following = xref.get("following_bsr")
if not isinstance(following, Mapping) or not isinstance(following.get("target"), int):
continue
target = int(following["target"])
record = builder_targets.setdefault(
target,
{
"target": target,
"target_hex": h16(target),
"xref_count": 0,
"examples": [],
},
)
record["xref_count"] = int(record["xref_count"]) + 1
examples = record["examples"]
if isinstance(examples, list) and len(examples) < 8:
examples.append(
{
"text_address": item.get("address"),
"text_address_hex": h16(int(item["address"])) if isinstance(item.get("address"), int) else None,
"trimmed": item.get("trimmed"),
"xref_address": xref.get("address"),
"xref_address_hex": h16(int(xref["address"])) if isinstance(xref.get("address"), int) else None,
}
)
lcd_driver = payload.get("lcd_driver")
routines = []
if isinstance(lcd_driver, Mapping) and isinstance(lcd_driver.get("routines"), list):
for routine in lcd_driver["routines"]:
if not isinstance(routine, Mapping) or not isinstance(routine.get("start"), int):
continue
routines.append(
{
"start": routine["start"],
"start_hex": h16(int(routine["start"])),
"end": routine.get("end"),
"end_hex": h16(int(routine["end"])) if isinstance(routine.get("end"), int) else None,
"role_hint": routine.get("role_hint"),
"roles": routine.get("roles", []),
}
)
return {
"terms": list(LCD_CORRELATION_TERMS),
"term_hits": term_hits,
"display_builder_targets": sorted(
builder_targets.values(),
key=lambda item: (-int(item["xref_count"]), int(item["target"])),
),
"lcd_driver_routines": routines,
"caveat": (
"This is a static correlation helper. It reports text/script candidates and LCD driver "
"routines in the same decompile; it does not prove a protocol field directly causes a string."
),
}
def _lcd_string_summary(item: Mapping[str, Any]) -> JsonObject:
address = item.get("address")
return {
"address": address,
"address_hex": h16(int(address)) if isinstance(address, int) else None,
"text": item.get("text"),
"trimmed": item.get("trimmed"),
"confidence": item.get("confidence"),
"xref_count": item.get("xref_count", 0),
}
def _base_access(
ins: Mapping[str, Any],
functions: list[JsonObject],
semantic_accesses: Mapping[int, list[JsonObject]],
) -> JsonObject:
address = int(ins["address"])
function = _function_for_address(functions, address)
access: JsonObject = {
"instruction_address": address,
"instruction_address_hex": h16(address),
"mnemonic": str(ins.get("mnemonic", "")),
"operands": str(ins.get("operands", "")),
"instruction": str(ins.get("text") or _instruction_text(ins)),
"references": _references(ins),
"references_hex": [h16(ref) for ref in _references(ins)],
"targets": _targets(ins),
"targets_hex": [h16(target) for target in _targets(ins)],
"label": _label_for_instruction(ins),
"semantic_candidates": semantic_accesses.get(address, []),
}
if function:
access["function_start"] = function["start"]
access["function_start_hex"] = h16(int(function["start"]))
access["function_label"] = function["label"]
return access
def _semantic_access_locations(payload: Mapping[str, Any]) -> dict[int, list[JsonObject]]:
locations: dict[int, list[JsonObject]] = {}
semantics = payload.get("serial_semantics")
if not isinstance(semantics, Mapping):
return locations
sources: list[Any] = []
protocols = semantics.get("protocol_semantics")
if isinstance(protocols, list):
sources.extend(protocols)
sources.append(semantics)
for source in sources:
if not isinstance(source, Mapping):
continue
for item in _table_candidate_items(source.get("table_map_candidates")):
for access in _table_candidate_items(item.get("accesses")):
address = access.get("instruction_address")
if isinstance(address, int):
locations.setdefault(address, []).append(
{
"name_candidate": item.get("name_candidate"),
"kind": item.get("kind"),
"confidence": item.get("confidence"),
}
)
return locations
def _table_candidate_items(value: Any) -> list[Mapping[str, Any]]:
if isinstance(value, Mapping):
return [item for item in value.values() if isinstance(item, Mapping)]
if isinstance(value, list):
return [item for item in value if isinstance(item, Mapping)]
return []
def _format_access_line(access: Mapping[str, Any]) -> str:
function = access.get("function_label") or "<no function>"
operand = access.get("operand") or access.get("direct_address_hex")
index = access.get("index")
if index == "dynamic":
index_text = f"index dynamic via {access.get('index_register')} operand {operand}"
else:
index_text = f"offset {h16(int(index or 0))}"
if access.get("logical_address_hex"):
index_text += f" -> {access['logical_address_hex']}"
elif access.get("direct_address_hex"):
index_text += f" at {access['direct_address_hex']}"
return (
f"{access['instruction_address_hex']} {access['access']} {index_text}; "
f"{function}; {access['instruction']}"
)
def _summarize_functions(accesses: Iterable[Mapping[str, Any]]) -> list[JsonObject]:
summaries: dict[int, JsonObject] = {}
for access in accesses:
start = access.get("function_start")
if not isinstance(start, int):
start = -1
summary = summaries.setdefault(
start,
{
"start": start if start >= 0 else None,
"start_hex": h16(start) if start >= 0 else None,
"label": access.get("function_label") or "<no function>",
"access_count": 0,
"reads": 0,
"writes": 0,
},
)
summary["access_count"] = int(summary["access_count"]) + 1
if access.get("access") == "read":
summary["reads"] = int(summary["reads"]) + 1
elif access.get("access") == "write":
summary["writes"] = int(summary["writes"]) + 1
return sorted(summaries.values(), key=lambda item: (-int(item["access_count"]), str(item["label"])))
def _function_ranges(payload: Mapping[str, Any]) -> list[JsonObject]:
call_graph = payload.get("call_graph")
if not isinstance(call_graph, Mapping):
return []
nodes = call_graph.get("nodes")
if not isinstance(nodes, list):
return []
ranges: list[JsonObject] = []
for node in nodes:
if not isinstance(node, Mapping):
continue
start = node.get("start")
end = node.get("end")
if isinstance(start, int) and isinstance(end, int):
ranges.append({"start": start, "end": end, "label": str(node.get("label") or label_for(start))})
return sorted(ranges, key=lambda item: int(item["start"]))
def _function_for_address(functions: list[JsonObject], address: int) -> JsonObject | None:
for function in functions:
if int(function["start"]) <= address <= int(function["end"]):
return function
return None
def _nearby_register_value(instructions: list[JsonObject], index: int, register: str) -> int | None:
register = register.upper()
for prior_index in range(index - 1, max(-1, index - 10), -1):
prior = instructions[prior_index]
source, destination = _source_destination_operands(str(prior.get("operands", "")))
if destination.upper() != register:
continue
value = _parse_immediate(source)
if value is not None:
return value
if _writes_register(prior, register):
return None
return None
def _writes_register(ins: Mapping[str, Any], register: str) -> bool:
_source, destination = _source_destination_operands(str(ins.get("operands", "")))
return destination.upper() == register
def _instruction_sequence(value: object) -> list[JsonObject]:
if isinstance(value, Mapping):
values: Iterable[Any] = value.values()
elif isinstance(value, list):
values = value
else:
values = []
return sorted(
[item for item in values if isinstance(item, dict) and isinstance(item.get("address"), int)],
key=lambda item: int(item["address"]),
)
def _label_for_instruction(ins: Mapping[str, Any]) -> str | None:
address = int(ins["address"])
for key in ("label", "target_label"):
value = ins.get(key)
if isinstance(value, str) and value:
return value
if _targets(ins):
return label_for(address)
return None
def _instruction_text(ins: Mapping[str, Any]) -> str:
operands = str(ins.get("operands", ""))
return f"{ins.get('mnemonic', '')} {operands}".strip()
def _references(ins: Mapping[str, Any]) -> list[int]:
refs = ins.get("references", [])
if not isinstance(refs, list):
return []
output: list[int] = []
for ref in refs:
if isinstance(ref, Mapping) and isinstance(ref.get("address"), int):
output.append(int(ref["address"]))
elif isinstance(ref, int):
output.append(ref)
return output
def _targets(ins: Mapping[str, Any]) -> list[int]:
targets = ins.get("targets", [])
if not isinstance(targets, list):
return []
return [int(target) for target in targets if isinstance(target, int)]
def _negative_indexed_operands(operands: str) -> list[JsonObject]:
matches: list[JsonObject] = []
for match in re.finditer(r"@\(-H'([0-9A-Fa-f]+),\s*(R[0-7])\)", operands):
offset = int(match.group(1), 16) & 0xFFFF
if offset not in LOGICAL_TABLES:
continue
matches.append(
{
"operand": match.group(0),
"negative_offset": offset,
"index_register": match.group(2).upper(),
}
)
return matches
def _table_for_logical_address(address: int) -> Mapping[str, Any] | None:
for table in TABLES:
if int(table["logical_base_address"]) <= address <= int(table["logical_range_end"]):
return table
return None
def _table_for_direct_candidate_address(address: int) -> Mapping[str, Any] | None:
for table in TABLES:
direct_addresses = [int(item) for item in table["direct_addresses"]]
if min(direct_addresses) <= address <= int(table["direct_range_end"]):
return table
return None
def _operand_access_kind(ins: Mapping[str, Any], operand: str) -> str:
root = _mnemonic_root(str(ins.get("mnemonic", "")))
source, destination = _source_destination_operands(str(ins.get("operands", "")))
if root in {"BTST", "CMP", "CMP:E", "CMP:G", "CMP:I", "TST"}:
return "read"
if root in {"BCLR", "BNOT", "BSET", "CLR", "INC", "INC:G", "NEG", "NOT"}:
return "write"
if operand in destination and operand not in source:
return "write"
if operand in source and operand not in destination:
return "read"
if root in {"ADD:Q", "ADD:G", "ADDS", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}:
return "write"
return "read_write_candidate"
def _access_direction(ins: Mapping[str, Any], address: int) -> str | None:
root = _mnemonic_root(str(ins.get("mnemonic", "")))
if root in {"BTST", "CMP", "CMP:E", "CMP:G", "CMP:I", "MOVFPE", "TST"}:
return "read"
if root in {"BCLR", "BNOT", "BSET", "CLR", "INC", "INC:G", "NEG", "NOT"}:
return "write"
if root in {"ADD:Q", "ADD:G", "ADDS", "ADDX", "AND", "OR", "SUB", "SUBS", "SUBX", "XOR"}:
return "write"
if root in {"MOV:G", "MOV:S", "MOVTPE"}:
source, destination = _source_destination_operands(str(ins.get("operands", "")))
if _operand_mentions_address(destination, address):
return "write"
if _operand_mentions_address(source, address):
return "read"
if address in _references(ins):
if destination.startswith("@") and not _operand_mentions_any_reference(source, _references(ins)):
return "write"
if source.startswith("@") and not _operand_mentions_any_reference(destination, _references(ins)):
return "read"
if root in {"MOV:L", "MOV:F"}:
return "read"
if root == "STC":
return "write"
if root == "LDC":
return "read"
return None
def _source_destination_operands(operands: str) -> tuple[str, str]:
depth = 0
split_at: int | None = None
for index, char in enumerate(operands):
if char in "({":
depth += 1
elif char in ")}" and depth:
depth -= 1
elif char == "," and depth == 0:
split_at = index
if split_at is None:
operand = operands.strip()
return "", operand
return operands[:split_at].strip(), operands[split_at + 1 :].strip()
def _parse_immediate(operand: str) -> int | None:
text = operand.strip()
if text.startswith("#"):
text = text[1:].strip()
try:
if text.upper().startswith("H'"):
return int(text[2:], 16) & 0xFFFF
if text.upper().startswith("0X"):
return int(text, 16) & 0xFFFF
if text.upper().startswith("$"):
return int(text[1:], 16) & 0xFFFF
return int(text, 10) & 0xFFFF
except ValueError:
return None
def _operand_mentions_any_reference(operand: str, references: list[int]) -> bool:
return any(_operand_mentions_address(operand, address) for address in references)
def _operand_mentions_address(operand: str, address: int) -> bool:
operand_upper = operand.upper().replace(" ", "")
negative = (0x10000 - address) & 0xFFFF
return (
f"H'{address:04X}" in operand_upper
or f"0X{address:04X}" in operand_upper
or f"${address:04X}" in operand_upper
or f"-H'{negative:04X}" in operand_upper
or f"-0X{negative:04X}" in operand_upper
or f"-${negative:04X}" in operand_upper
)
def _mnemonic_root(mnemonic: str) -> str:
return mnemonic.rsplit(".", 1)[0].upper()
__all__ = [
"analyze_table_xrefs",
"generate_table_xref_report",
"load_table_xref_input",
"main",
"write_table_xrefs",
]