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 ADDED
@@ -0,0 +1,5 @@
1
+ """Fantasm — the Fantastic (dis-/re-)Assembly tools for 6502 code."""
2
+
3
+ __version__ = "0.1.2"
4
+
5
+ __all__ = ["__version__"]
@@ -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
+ ]