fantasm 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fantasm/__init__.py +5 -0
- fantasm/api/__init__.py +240 -0
- fantasm/api/asm_extract.py +183 -0
- fantasm/api/audit.py +486 -0
- fantasm/api/backfill.py +742 -0
- fantasm/api/blockmatch.py +259 -0
- fantasm/api/cfg.py +358 -0
- fantasm/api/comment_check.py +588 -0
- fantasm/api/compare.py +423 -0
- fantasm/api/context.py +315 -0
- fantasm/api/find_shared.py +164 -0
- fantasm/api/fingerprint.py +92 -0
- fantasm/api/insert_point.py +194 -0
- fantasm/api/labels.py +228 -0
- fantasm/api/lint.py +301 -0
- fantasm/api/mos6502.py +221 -0
- fantasm/api/paths.py +220 -0
- fantasm/api/project.py +197 -0
- fantasm/api/promote.py +174 -0
- fantasm/api/rename_labels.py +139 -0
- fantasm/api/suggest.py +175 -0
- fantasm/api/verify.py +140 -0
- fantasm/api/version_graph.py +477 -0
- fantasm/cli.py +2556 -0
- fantasm/cli_helpers.py +179 -0
- fantasm/config.py +65 -0
- fantasm-0.1.2.dist-info/METADATA +201 -0
- fantasm-0.1.2.dist-info/RECORD +31 -0
- fantasm-0.1.2.dist-info/WHEEL +4 -0
- fantasm-0.1.2.dist-info/entry_points.txt +2 -0
- fantasm-0.1.2.dist-info/licenses/LICENSE +21 -0
fantasm/__init__.py
ADDED
fantasm/api/__init__.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Programmatic API for fantasm.
|
|
2
|
+
|
|
3
|
+
Sub-modules will hold per-topic surfaces (labels, comments, cfg, audit, ...).
|
|
4
|
+
This package re-exports the stable, flat surface so callers can do either:
|
|
5
|
+
|
|
6
|
+
from fantasm.api import rename_labels
|
|
7
|
+
from fantasm.api.labels import rename_labels
|
|
8
|
+
|
|
9
|
+
The flat re-exports are populated as topics are ported in from the sibling
|
|
10
|
+
``disasm_tools`` packages.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .asm_extract import (
|
|
16
|
+
AsmSection,
|
|
17
|
+
build_index,
|
|
18
|
+
extract_section,
|
|
19
|
+
find_line_for_target,
|
|
20
|
+
parse_address,
|
|
21
|
+
)
|
|
22
|
+
from .labels import (
|
|
23
|
+
AUTO_LABEL_RE,
|
|
24
|
+
CATEGORY_ORDER,
|
|
25
|
+
build_target_refs,
|
|
26
|
+
classify_labels,
|
|
27
|
+
collect_auto_labels,
|
|
28
|
+
find_containing_sub_for_addr,
|
|
29
|
+
sort_labels,
|
|
30
|
+
)
|
|
31
|
+
from .cfg import (
|
|
32
|
+
BasicBlock,
|
|
33
|
+
BasicBlockEntry,
|
|
34
|
+
BasicBlockExit,
|
|
35
|
+
build_call_graph,
|
|
36
|
+
find_basic_blocks,
|
|
37
|
+
resolve_sub_node,
|
|
38
|
+
)
|
|
39
|
+
from .backfill import (
|
|
40
|
+
AnnotationDiff,
|
|
41
|
+
PropagationCandidate,
|
|
42
|
+
PropagationReport,
|
|
43
|
+
build_confidence_map,
|
|
44
|
+
build_confidence_map_for_block,
|
|
45
|
+
compose_chained_map,
|
|
46
|
+
diff_annotations,
|
|
47
|
+
group_logical_statements,
|
|
48
|
+
parse_annotations,
|
|
49
|
+
propose_propagations,
|
|
50
|
+
translate_address_in_text,
|
|
51
|
+
translate_subroutine,
|
|
52
|
+
)
|
|
53
|
+
from .blockmatch import (
|
|
54
|
+
RelocatedBlock,
|
|
55
|
+
build_full_address_map,
|
|
56
|
+
build_primary_map,
|
|
57
|
+
disassemble_to_opcodes,
|
|
58
|
+
find_relocated_blocks,
|
|
59
|
+
)
|
|
60
|
+
from .fingerprint import (
|
|
61
|
+
find_duplicate_blocks,
|
|
62
|
+
fingerprint_blocks,
|
|
63
|
+
fingerprint_rom_file,
|
|
64
|
+
fingerprint_window,
|
|
65
|
+
opcode_fingerprint,
|
|
66
|
+
)
|
|
67
|
+
from .find_shared import (
|
|
68
|
+
find_matching_spans,
|
|
69
|
+
is_trivial_span,
|
|
70
|
+
load_rom,
|
|
71
|
+
matching_byte_count,
|
|
72
|
+
parse_rom_spec,
|
|
73
|
+
sweep_opcodes,
|
|
74
|
+
)
|
|
75
|
+
from .suggest import (
|
|
76
|
+
CommentSuggestion,
|
|
77
|
+
DEFAULT_INSTRUCTION_HINTS,
|
|
78
|
+
suggest_comments,
|
|
79
|
+
suggest_for_instruction,
|
|
80
|
+
)
|
|
81
|
+
from .promote import (
|
|
82
|
+
CALL_MNEMONICS,
|
|
83
|
+
TERMINAL_MNEMONICS,
|
|
84
|
+
analyze_labels,
|
|
85
|
+
load_and_analyze_labels,
|
|
86
|
+
)
|
|
87
|
+
from .compare import (
|
|
88
|
+
Instruction,
|
|
89
|
+
RomHeader,
|
|
90
|
+
compare_headers,
|
|
91
|
+
compare_roms,
|
|
92
|
+
disassemble_linear,
|
|
93
|
+
format_address,
|
|
94
|
+
format_instruction,
|
|
95
|
+
parse_rom_header,
|
|
96
|
+
)
|
|
97
|
+
from .rename_labels import (
|
|
98
|
+
apply_renames_to_lines,
|
|
99
|
+
find_insert_position,
|
|
100
|
+
find_rename_section,
|
|
101
|
+
parse_label_declarations,
|
|
102
|
+
)
|
|
103
|
+
from .project import (
|
|
104
|
+
ProjectInitConfig,
|
|
105
|
+
VersionInfo,
|
|
106
|
+
add_version,
|
|
107
|
+
init_project,
|
|
108
|
+
list_versions,
|
|
109
|
+
render_fantasm_toml,
|
|
110
|
+
)
|
|
111
|
+
from .verify import BeebasmNotFoundError, VerifyResult, verify_round_trip
|
|
112
|
+
from .version_graph import (
|
|
113
|
+
Edge,
|
|
114
|
+
NoPathError,
|
|
115
|
+
Region,
|
|
116
|
+
RelocBlock,
|
|
117
|
+
Version,
|
|
118
|
+
VersionGraph,
|
|
119
|
+
VersionGraphCycleError,
|
|
120
|
+
VersionGraphError,
|
|
121
|
+
VersionNotInGraphError,
|
|
122
|
+
load_version_graph,
|
|
123
|
+
)
|
|
124
|
+
from .context import (
|
|
125
|
+
CallSiteContext,
|
|
126
|
+
ExitPointContext,
|
|
127
|
+
SubContext,
|
|
128
|
+
UncommentedSubReport,
|
|
129
|
+
analyse_uncommented_subs,
|
|
130
|
+
compute_call_depths,
|
|
131
|
+
extract_sub_context,
|
|
132
|
+
)
|
|
133
|
+
from .lint import (
|
|
134
|
+
address_in_ranges,
|
|
135
|
+
address_ranges_from_data,
|
|
136
|
+
extract_annotations,
|
|
137
|
+
find_code_block_ranges,
|
|
138
|
+
find_nth_occurrence,
|
|
139
|
+
load_address_ranges,
|
|
140
|
+
load_valid_addresses,
|
|
141
|
+
offset_in_code_block,
|
|
142
|
+
valid_addresses_from_data,
|
|
143
|
+
)
|
|
144
|
+
from .insert_point import (
|
|
145
|
+
AlreadyDeclared,
|
|
146
|
+
InsertPoint,
|
|
147
|
+
compute_insert_point,
|
|
148
|
+
find_main_block,
|
|
149
|
+
parse_subroutine_declarations,
|
|
150
|
+
)
|
|
151
|
+
from .comment_check import (
|
|
152
|
+
ALL_CHECKS,
|
|
153
|
+
CR_ADDRESSES,
|
|
154
|
+
IMM_REG_MNEMONICS,
|
|
155
|
+
TUBE_REGISTERS,
|
|
156
|
+
build_known_addrs,
|
|
157
|
+
check_block_stale_addr,
|
|
158
|
+
check_branch_target,
|
|
159
|
+
check_chain_comments,
|
|
160
|
+
check_cr_value,
|
|
161
|
+
check_desc_stale_addr,
|
|
162
|
+
check_reg_value,
|
|
163
|
+
check_stale_addr,
|
|
164
|
+
check_tube_register,
|
|
165
|
+
find_chains,
|
|
166
|
+
find_stale_addrs,
|
|
167
|
+
parse_imm_value,
|
|
168
|
+
run_checks,
|
|
169
|
+
)
|
|
170
|
+
from .audit import (
|
|
171
|
+
ALL_FLAGS,
|
|
172
|
+
BRANCH_MNEMONICS,
|
|
173
|
+
TERMINATING_MNEMONICS,
|
|
174
|
+
build_memory_regions,
|
|
175
|
+
end_type,
|
|
176
|
+
find_containing_sub,
|
|
177
|
+
find_sub,
|
|
178
|
+
find_undeclared_subs,
|
|
179
|
+
load_subroutines,
|
|
180
|
+
region_for_addr,
|
|
181
|
+
scan_routine_range,
|
|
182
|
+
)
|
|
183
|
+
from .mos6502 import (
|
|
184
|
+
OPCODE_LENGTHS,
|
|
185
|
+
OPCODE_LENGTHS_65C02,
|
|
186
|
+
OPCODE_MNEMONICS,
|
|
187
|
+
OPCODE_MNEMONICS_65C02,
|
|
188
|
+
instruction_length,
|
|
189
|
+
mnemonic,
|
|
190
|
+
opcode_tables,
|
|
191
|
+
)
|
|
192
|
+
from .paths import (
|
|
193
|
+
VersionNotFoundError,
|
|
194
|
+
project_rom_prefixes,
|
|
195
|
+
project_versions_dirpath,
|
|
196
|
+
resolve_version_dirpath,
|
|
197
|
+
resolve_version_dirpath_for_project,
|
|
198
|
+
rom_prefix,
|
|
199
|
+
rom_prefix_for_project,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
__all__ = [
|
|
203
|
+
"ALL_FLAGS",
|
|
204
|
+
"AUTO_LABEL_RE",
|
|
205
|
+
"AsmSection",
|
|
206
|
+
"BRANCH_MNEMONICS",
|
|
207
|
+
"CATEGORY_ORDER",
|
|
208
|
+
"OPCODE_LENGTHS",
|
|
209
|
+
"OPCODE_LENGTHS_65C02",
|
|
210
|
+
"OPCODE_MNEMONICS",
|
|
211
|
+
"OPCODE_MNEMONICS_65C02",
|
|
212
|
+
"TERMINATING_MNEMONICS",
|
|
213
|
+
"VersionNotFoundError",
|
|
214
|
+
"build_index",
|
|
215
|
+
"build_memory_regions",
|
|
216
|
+
"build_target_refs",
|
|
217
|
+
"classify_labels",
|
|
218
|
+
"collect_auto_labels",
|
|
219
|
+
"end_type",
|
|
220
|
+
"extract_section",
|
|
221
|
+
"find_containing_sub",
|
|
222
|
+
"find_containing_sub_for_addr",
|
|
223
|
+
"find_line_for_target",
|
|
224
|
+
"find_sub",
|
|
225
|
+
"find_undeclared_subs",
|
|
226
|
+
"instruction_length",
|
|
227
|
+
"load_subroutines",
|
|
228
|
+
"mnemonic",
|
|
229
|
+
"opcode_tables",
|
|
230
|
+
"parse_address",
|
|
231
|
+
"project_rom_prefixes",
|
|
232
|
+
"project_versions_dirpath",
|
|
233
|
+
"region_for_addr",
|
|
234
|
+
"resolve_version_dirpath",
|
|
235
|
+
"resolve_version_dirpath_for_project",
|
|
236
|
+
"rom_prefix",
|
|
237
|
+
"rom_prefix_for_project",
|
|
238
|
+
"scan_routine_range",
|
|
239
|
+
"sort_labels",
|
|
240
|
+
]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Extract sections of a disassembly ``.asm`` file by address or label.
|
|
2
|
+
|
|
3
|
+
Pure helpers for indexing assembly lines and resolving address/label
|
|
4
|
+
targets. CLI-facing wrappers (file open + print) live separately.
|
|
5
|
+
|
|
6
|
+
Refactors relative to the sibling copies:
|
|
7
|
+
|
|
8
|
+
- :func:`find_line_for_target` no longer takes the unused ``lines``
|
|
9
|
+
parameter and emits ambiguous-match warnings via :mod:`warnings`
|
|
10
|
+
instead of printing to stderr.
|
|
11
|
+
- :func:`extract_section` returns the captured slice (start line and
|
|
12
|
+
text) as a pure function; the file-based ``extract`` entry was a
|
|
13
|
+
CLI-style mixed-IO helper and has been split out so the rest of the
|
|
14
|
+
module is easier to test and re-use.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
import warnings
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_ADDR_COMMENT_RE = re.compile(r";\s*([0-9a-f]{4}):")
|
|
26
|
+
_RUNTIME_ADDR_RE = re.compile(r":([0-9a-f]{4})\[")
|
|
27
|
+
_LABEL_RE = re.compile(r"^\.(\w+)")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_address(text: str) -> int | None:
|
|
31
|
+
"""Parse a hex address in any of the conventional notations.
|
|
32
|
+
|
|
33
|
+
Recognises ``$8000``, ``&8000``, ``0x8000`` and bare ``8000``.
|
|
34
|
+
Returns ``None`` if the text is not a valid hex integer.
|
|
35
|
+
"""
|
|
36
|
+
cleaned = text.strip().lstrip("$&").removeprefix("0x")
|
|
37
|
+
try:
|
|
38
|
+
return int(cleaned, 16)
|
|
39
|
+
except ValueError:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_index(
|
|
44
|
+
asm_lines: Sequence[str],
|
|
45
|
+
) -> tuple[dict[int, int], dict[str, int]]:
|
|
46
|
+
"""Build ``(addr_to_line, label_to_line)`` indices.
|
|
47
|
+
|
|
48
|
+
For each line, looks for a ``;NNNN:`` comment marker (the
|
|
49
|
+
disassembler's address tag) and a ``:NNNN[...]`` runtime-address
|
|
50
|
+
annotation; both are recorded in ``addr_to_line``. Lines starting
|
|
51
|
+
with ``.label`` are recorded in ``label_to_line``. The first
|
|
52
|
+
occurrence wins for any given key.
|
|
53
|
+
"""
|
|
54
|
+
addr_to_line: dict[int, int] = {}
|
|
55
|
+
label_to_line: dict[str, int] = {}
|
|
56
|
+
|
|
57
|
+
for line_index, line in enumerate(asm_lines):
|
|
58
|
+
match = _ADDR_COMMENT_RE.search(line)
|
|
59
|
+
if match:
|
|
60
|
+
addr = int(match.group(1), 16)
|
|
61
|
+
addr_to_line.setdefault(addr, line_index)
|
|
62
|
+
|
|
63
|
+
match = _RUNTIME_ADDR_RE.search(line)
|
|
64
|
+
if match:
|
|
65
|
+
addr = int(match.group(1), 16)
|
|
66
|
+
addr_to_line.setdefault(addr, line_index)
|
|
67
|
+
|
|
68
|
+
match = _LABEL_RE.match(line)
|
|
69
|
+
if match:
|
|
70
|
+
label_to_line.setdefault(match.group(1), line_index)
|
|
71
|
+
|
|
72
|
+
return addr_to_line, label_to_line
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def find_line_for_target(
|
|
76
|
+
target: str,
|
|
77
|
+
addr_to_line: dict[int, int],
|
|
78
|
+
label_to_line: dict[str, int],
|
|
79
|
+
) -> int | None:
|
|
80
|
+
"""Resolve a textual target to a 0-based line number.
|
|
81
|
+
|
|
82
|
+
Resolution order:
|
|
83
|
+
|
|
84
|
+
1. If ``target`` parses as a hex address, return the line that maps
|
|
85
|
+
to that address — or, failing an exact match, the line for the
|
|
86
|
+
nearest indexed address less than or equal to ``target``.
|
|
87
|
+
2. If ``target`` matches a label exactly, return that label's line.
|
|
88
|
+
3. If ``target`` is a substring of exactly one label, return that
|
|
89
|
+
label's line. If it matches multiple labels, emits a
|
|
90
|
+
``UserWarning`` listing the matches and returns the lexically
|
|
91
|
+
first match's line so callers continue to make progress.
|
|
92
|
+
|
|
93
|
+
Returns ``None`` when nothing matches.
|
|
94
|
+
"""
|
|
95
|
+
addr = parse_address(target)
|
|
96
|
+
if addr is not None:
|
|
97
|
+
if addr in addr_to_line:
|
|
98
|
+
return addr_to_line[addr]
|
|
99
|
+
candidates = [a for a in addr_to_line if a <= addr]
|
|
100
|
+
if candidates:
|
|
101
|
+
return addr_to_line[max(candidates)]
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
if target in label_to_line:
|
|
105
|
+
return label_to_line[target]
|
|
106
|
+
|
|
107
|
+
matches = sorted(label for label in label_to_line if target in label)
|
|
108
|
+
if not matches:
|
|
109
|
+
return None
|
|
110
|
+
if len(matches) > 1:
|
|
111
|
+
warnings.warn(
|
|
112
|
+
f"ambiguous label {target!r}; matches: {', '.join(matches)}",
|
|
113
|
+
stacklevel=2,
|
|
114
|
+
)
|
|
115
|
+
return label_to_line[matches[0]]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class AsmSection:
|
|
120
|
+
"""A slice of assembly lines extracted by :func:`extract_section`.
|
|
121
|
+
|
|
122
|
+
``start_line`` and ``end_line`` are 0-based; ``end_line`` is
|
|
123
|
+
exclusive. ``lines`` is the captured text without line numbers.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
start_line: int
|
|
127
|
+
end_line: int
|
|
128
|
+
lines: list[str]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def extract_section(
|
|
132
|
+
asm_lines: Sequence[str],
|
|
133
|
+
start_target: str,
|
|
134
|
+
end_target: str | None = None,
|
|
135
|
+
default_window: int = 40,
|
|
136
|
+
) -> AsmSection:
|
|
137
|
+
"""Extract a section of assembly lines spanning two targets.
|
|
138
|
+
|
|
139
|
+
The section starts at the line resolved from ``start_target`` and
|
|
140
|
+
ends at the line resolved from ``end_target`` (inclusive). When no
|
|
141
|
+
``end_target`` is given, ``default_window`` lines are taken.
|
|
142
|
+
|
|
143
|
+
The start is backed up across preceding blank, comment-only, and
|
|
144
|
+
label-only lines so the captured section includes the natural
|
|
145
|
+
"header" of the routine or block.
|
|
146
|
+
|
|
147
|
+
Raises ``LookupError`` if either target cannot be resolved.
|
|
148
|
+
"""
|
|
149
|
+
addr_to_line, label_to_line = build_index(asm_lines)
|
|
150
|
+
|
|
151
|
+
start_line = find_line_for_target(start_target, addr_to_line, label_to_line)
|
|
152
|
+
if start_line is None:
|
|
153
|
+
raise LookupError(f"could not find {start_target!r}")
|
|
154
|
+
|
|
155
|
+
while start_line > 0 and not _ADDR_COMMENT_RE.search(asm_lines[start_line - 1]):
|
|
156
|
+
previous = asm_lines[start_line - 1].strip()
|
|
157
|
+
if previous == "" or previous.startswith(";") or previous.startswith("."):
|
|
158
|
+
start_line -= 1
|
|
159
|
+
else:
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
if end_target is not None:
|
|
163
|
+
end_line = find_line_for_target(end_target, addr_to_line, label_to_line)
|
|
164
|
+
if end_line is None:
|
|
165
|
+
raise LookupError(f"could not find {end_target!r}")
|
|
166
|
+
end_line += 1
|
|
167
|
+
else:
|
|
168
|
+
end_line = min(start_line + default_window, len(asm_lines))
|
|
169
|
+
|
|
170
|
+
return AsmSection(
|
|
171
|
+
start_line=start_line,
|
|
172
|
+
end_line=end_line,
|
|
173
|
+
lines=list(asm_lines[start_line:end_line]),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = [
|
|
178
|
+
"AsmSection",
|
|
179
|
+
"build_index",
|
|
180
|
+
"extract_section",
|
|
181
|
+
"find_line_for_target",
|
|
182
|
+
"parse_address",
|
|
183
|
+
]
|