dasmos 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.
Files changed (49) hide show
  1. dasmos/__init__.py +65 -0
  2. dasmos/_text.py +57 -0
  3. dasmos/cli.py +485 -0
  4. dasmos/core/__init__.py +8 -0
  5. dasmos/core/annotations.py +207 -0
  6. dasmos/core/classification.py +223 -0
  7. dasmos/core/config.py +74 -0
  8. dasmos/core/cpu_state.py +46 -0
  9. dasmos/core/disassembly.py +161 -0
  10. dasmos/core/labels.py +373 -0
  11. dasmos/core/markdown_asm.py +374 -0
  12. dasmos/core/memory.py +318 -0
  13. dasmos/core/move.py +301 -0
  14. dasmos/cpu.py +388 -0
  15. dasmos/disassembler.py +1350 -0
  16. dasmos/environment.py +141 -0
  17. dasmos/exceptions.py +6 -0
  18. dasmos/ext/__init__.py +8 -0
  19. dasmos/ext/cpus/__init__.py +10 -0
  20. dasmos/ext/cpus/cmos65c02/__init__.py +22 -0
  21. dasmos/ext/cpus/cmos65c02/cpu.py +148 -0
  22. dasmos/ext/cpus/nmos6502/__init__.py +22 -0
  23. dasmos/ext/cpus/nmos6502/cpu.py +550 -0
  24. dasmos/ext/environments/__init__.py +0 -0
  25. dasmos/ext/environments/acorn_bbc_hardware/__init__.py +13 -0
  26. dasmos/ext/environments/acorn_bbc_hardware/environment.py +164 -0
  27. dasmos/ext/environments/acorn_mos/__init__.py +13 -0
  28. dasmos/ext/environments/acorn_mos/enums.py +299 -0
  29. dasmos/ext/environments/acorn_mos/environment.py +182 -0
  30. dasmos/ext/environments/acorn_mos/hooks.py +212 -0
  31. dasmos/ext/environments/acorn_sideways_rom/__init__.py +13 -0
  32. dasmos/ext/environments/acorn_sideways_rom/environment.py +183 -0
  33. dasmos/ext/renderers/__init__.py +10 -0
  34. dasmos/ext/renderers/beebasm/__init__.py +16 -0
  35. dasmos/ext/renderers/beebasm/renderer.py +1743 -0
  36. dasmos/ext/renderers/json/__init__.py +16 -0
  37. dasmos/ext/renderers/json/renderer.py +718 -0
  38. dasmos/extension.py +250 -0
  39. dasmos/hooks.py +76 -0
  40. dasmos/ir.py +117 -0
  41. dasmos/output.py +82 -0
  42. dasmos/py.typed +0 -0
  43. dasmos/renderer.py +272 -0
  44. dasmos-0.1.2.dist-info/METADATA +315 -0
  45. dasmos-0.1.2.dist-info/RECORD +49 -0
  46. dasmos-0.1.2.dist-info/WHEEL +5 -0
  47. dasmos-0.1.2.dist-info/entry_points.txt +15 -0
  48. dasmos-0.1.2.dist-info/licenses/LICENSE +21 -0
  49. dasmos-0.1.2.dist-info/top_level.txt +1 -0
dasmos/__init__.py ADDED
@@ -0,0 +1,65 @@
1
+ """dasmos — a pluggable tracing disassembler.
2
+
3
+ The top-level package re-exports the public API. Consumers should import
4
+ from :mod:`dasmos` rather than reaching into sub-modules.
5
+ """
6
+
7
+ __version__ = "0.1.2"
8
+
9
+ from dasmos.core.annotations import Align, Annotation, AnnotationStore, Banner, Comment
10
+ from dasmos.cpu import (
11
+ Cpu,
12
+ CpuExtensionError,
13
+ FlowControl,
14
+ Opcode,
15
+ OperandKind,
16
+ cpu_names,
17
+ create_cpu,
18
+ describe_cpu,
19
+ )
20
+ from dasmos.disassembler import Disassembler, DisassemblerError
21
+ from dasmos.exceptions import DasmosError
22
+ from dasmos.extension import Extension, ExtensionError
23
+ from dasmos.ir import IntermediateRepresentation
24
+ from dasmos.output import Output, StructuredOutput, TextOutput
25
+ from dasmos.renderer import (
26
+ Renderer,
27
+ RendererExtensionError,
28
+ StructuredRenderer,
29
+ TextRenderer,
30
+ create_renderer,
31
+ describe_renderer,
32
+ renderer_names,
33
+ )
34
+
35
+ __all__ = [
36
+ "Align",
37
+ "Annotation",
38
+ "AnnotationStore",
39
+ "Banner",
40
+ "Comment",
41
+ "Cpu",
42
+ "CpuExtensionError",
43
+ "DasmosError",
44
+ "Disassembler",
45
+ "DisassemblerError",
46
+ "Extension",
47
+ "ExtensionError",
48
+ "FlowControl",
49
+ "IntermediateRepresentation",
50
+ "Opcode",
51
+ "OperandKind",
52
+ "Output",
53
+ "Renderer",
54
+ "RendererExtensionError",
55
+ "StructuredOutput",
56
+ "StructuredRenderer",
57
+ "TextOutput",
58
+ "TextRenderer",
59
+ "cpu_names",
60
+ "create_cpu",
61
+ "create_renderer",
62
+ "describe_cpu",
63
+ "describe_renderer",
64
+ "renderer_names",
65
+ ]
dasmos/_text.py ADDED
@@ -0,0 +1,57 @@
1
+ """Internal text utilities.
2
+
3
+ Small helpers used by :mod:`dasmos.extension` for rendering extension
4
+ descriptions. Internal (underscore-prefixed module) — not part of the
5
+ public API.
6
+ """
7
+
8
+
9
+ def _is_blank(line: str) -> bool:
10
+ return not line or line.isspace()
11
+
12
+
13
+ def strip_lines(text: str) -> str:
14
+ """Remove leading and trailing blank lines.
15
+
16
+ Args:
17
+ text: The text to process.
18
+
19
+ Returns:
20
+ The text with any leading and trailing blank-or-whitespace-only lines
21
+ removed. Interior blank lines are preserved.
22
+ """
23
+ lines = text.splitlines()
24
+ start = 0
25
+ while start < len(lines) and _is_blank(lines[start]):
26
+ start += 1
27
+ end = len(lines)
28
+ while end > start and _is_blank(lines[end - 1]):
29
+ end -= 1
30
+ return "\n".join(lines[start:end])
31
+
32
+
33
+ def normalize_name(name: str) -> str:
34
+ """Normalise a name by converting hyphens to underscores."""
35
+ return name.replace("-", "_")
36
+
37
+
38
+ def first_line(text: str) -> str:
39
+ """Extract the first non-empty line from text.
40
+
41
+ Useful for displaying descriptions in tables where multi-line text wraps
42
+ awkwardly.
43
+
44
+ Args:
45
+ text: The text to extract the first line from.
46
+
47
+ Returns:
48
+ The first non-empty line, stripped of leading/trailing whitespace.
49
+ Returns empty string if text is empty or contains only whitespace.
50
+ """
51
+ if not text:
52
+ return ""
53
+ for line in text.splitlines():
54
+ stripped = line.strip()
55
+ if stripped:
56
+ return stripped
57
+ return ""
dasmos/cli.py ADDED
@@ -0,0 +1,485 @@
1
+ """Command-line interface for dasmos.
2
+
3
+ The CLI is built on Click and uses :mod:`asyoulikeit` for report
4
+ output, so every introspection command supports
5
+ ``--as / --report / --header / --detailed``.
6
+
7
+ Commands fall into three groups:
8
+
9
+ - **Discovery** — ``list-cpus`` / ``describe-cpu`` (and the renderer
10
+ / environment equivalents). Read-only, useful for finding what's
11
+ installed.
12
+ - **Disassemble** — the headline one-shot
13
+ ``dasmos disassemble ROM --load-addr ADDR`` that exercises the
14
+ whole pipeline without requiring a driver script.
15
+ - **Init** — ``dasmos init DRIVER --rom ROM --load-addr ADDR``
16
+ scaffolds a starter driver file the user can edit and run with
17
+ ``python DRIVER`` to get the same output as ``disassemble``.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ import click
26
+ from asyoulikeit import (
27
+ Report,
28
+ Reports,
29
+ ScalarContent,
30
+ TableContent,
31
+ report_output,
32
+ )
33
+
34
+ from dasmos import __version__
35
+ from dasmos.cpu import CPU_NAMESPACE, cpu_names, describe_cpu
36
+ from dasmos.environment import (
37
+ ENVIRONMENT_NAMESPACE,
38
+ describe_environment,
39
+ environment_names,
40
+ )
41
+ from dasmos.renderer import (
42
+ RENDERER_NAMESPACE,
43
+ describe_renderer,
44
+ renderer_names,
45
+ )
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Address parsing — accepts ``0x8000``, ``&8000``, ``$8000``, decimal.
50
+ # Hex prefixes match the conventions a 6502/Acorn user already knows
51
+ # (``&`` = beebasm; ``$`` = ca65/Apple; ``0x`` = C/Python).
52
+ # ---------------------------------------------------------------------------
53
+
54
+
55
+ def _parse_address(s: str) -> int:
56
+ """Parse a CLI address argument.
57
+
58
+ Accepts ``0x8000`` / ``&8000`` / ``$8000`` (hex) and bare decimal.
59
+ Raises :class:`ValueError` on anything else so the click error
60
+ path produces a clean usage message.
61
+ """
62
+ text = s.strip()
63
+ if text.startswith(("0x", "0X")):
64
+ try:
65
+ return int(text, 16)
66
+ except ValueError:
67
+ pass
68
+ elif text.startswith(("&", "$")):
69
+ try:
70
+ return int(text[1:], 16)
71
+ except ValueError:
72
+ pass
73
+ else:
74
+ try:
75
+ return int(text)
76
+ except ValueError:
77
+ pass
78
+ raise ValueError(
79
+ f"{s!r} is not a valid address — use 0x1234, &1234, $1234, or decimal"
80
+ )
81
+
82
+
83
+ class _AddressParam(click.ParamType):
84
+ """Click parameter type for an address argument."""
85
+
86
+ name = "ADDR"
87
+
88
+ def convert(self, value, param, ctx):
89
+ try:
90
+ return _parse_address(value)
91
+ except ValueError as exc:
92
+ self.fail(str(exc), param, ctx)
93
+
94
+
95
+ ADDRESS = _AddressParam()
96
+
97
+
98
+ @click.group()
99
+ @click.version_option(__version__, prog_name="dasmos")
100
+ def cli() -> None:
101
+ """A pluggable tracing disassembler."""
102
+
103
+
104
+ @cli.command(name="list-cpus")
105
+ @report_output(reports={
106
+ "cpus": "Registered CPU (processor) plug-ins with one-line descriptions.",
107
+ })
108
+ def list_cpus_command() -> Reports:
109
+ """List the available CPU plug-ins."""
110
+ table = (
111
+ TableContent(title=f"CPUs registered under {CPU_NAMESPACE!r}")
112
+ .add_column("name", "Name")
113
+ .add_column("description", "Description")
114
+ )
115
+ for name in sorted(cpu_names()):
116
+ table.add_row(
117
+ name=name,
118
+ description=describe_cpu(name, single_line=True),
119
+ )
120
+ return Reports(cpus=Report(data=table))
121
+
122
+
123
+ @cli.command(name="describe-cpu")
124
+ @click.argument("name")
125
+ @report_output(reports={
126
+ "cpu": "The full description of one registered CPU plug-in.",
127
+ })
128
+ def describe_cpu_command(name: str) -> Reports:
129
+ """Describe a specific CPU plug-in."""
130
+ return Reports(cpu=Report(data=ScalarContent(
131
+ value=describe_cpu(name),
132
+ title=name,
133
+ )))
134
+
135
+
136
+ @cli.command(name="list-renderers")
137
+ @report_output(reports={
138
+ "renderers": "Registered renderer plug-ins with one-line descriptions.",
139
+ })
140
+ def list_renderers_command() -> Reports:
141
+ """List the available renderer plug-ins."""
142
+ table = (
143
+ TableContent(title=f"Renderers registered under {RENDERER_NAMESPACE!r}")
144
+ .add_column("name", "Name")
145
+ .add_column("description", "Description")
146
+ )
147
+ for name in sorted(renderer_names()):
148
+ table.add_row(
149
+ name=name,
150
+ description=describe_renderer(name, single_line=True),
151
+ )
152
+ return Reports(renderers=Report(data=table))
153
+
154
+
155
+ @cli.command(name="describe-renderer")
156
+ @click.argument("name")
157
+ @report_output(reports={
158
+ "renderer": "The full description of one registered renderer plug-in.",
159
+ })
160
+ def describe_renderer_command(name: str) -> Reports:
161
+ """Describe a specific renderer plug-in."""
162
+ return Reports(renderer=Report(data=ScalarContent(
163
+ value=describe_renderer(name),
164
+ title=name,
165
+ )))
166
+
167
+
168
+ @cli.command(name="list-environments")
169
+ @report_output(reports={
170
+ "environments": "Registered environment plug-ins with one-line descriptions.",
171
+ })
172
+ def list_environments_command() -> Reports:
173
+ """List the available environment plug-ins."""
174
+ table = (
175
+ TableContent(title=f"Environments registered under {ENVIRONMENT_NAMESPACE!r}")
176
+ .add_column("name", "Name")
177
+ .add_column("description", "Description")
178
+ )
179
+ for name in sorted(environment_names()):
180
+ table.add_row(
181
+ name=name,
182
+ description=describe_environment(name, single_line=True),
183
+ )
184
+ return Reports(environments=Report(data=table))
185
+
186
+
187
+ @cli.command(name="describe-environment")
188
+ @click.argument("name")
189
+ @report_output(reports={
190
+ "environment": "The full description of one registered environment plug-in.",
191
+ })
192
+ def describe_environment_command(name: str) -> Reports:
193
+ """Describe a specific environment plug-in."""
194
+ return Reports(environment=Report(data=ScalarContent(
195
+ value=describe_environment(name),
196
+ title=name,
197
+ )))
198
+
199
+
200
+ # ---------------------------------------------------------------------------
201
+ # disassemble — the headline one-shot command
202
+ # ---------------------------------------------------------------------------
203
+
204
+
205
+ def _split_envs(env_options: tuple[str, ...]) -> list[str]:
206
+ """Flatten the ``--env`` option list, accepting both repeated
207
+ flags AND comma-separated values per flag (so ``--env a,b`` and
208
+ ``--env a --env b`` are equivalent).
209
+ """
210
+ out: list[str] = []
211
+ for value in env_options:
212
+ for part in value.split(","):
213
+ part = part.strip()
214
+ if part:
215
+ out.append(part)
216
+ return out
217
+
218
+
219
+ @cli.command(name="disassemble")
220
+ @click.argument("rom", type=click.Path(exists=True, dir_okay=False, path_type=Path))
221
+ @click.option(
222
+ "-a", "--load-addr", "load_addr", type=ADDRESS, required=True,
223
+ help="Address where the binary is loaded (hex with 0x/&/$ prefix or decimal).",
224
+ )
225
+ @click.option(
226
+ "-c", "--cpu", default="nmos6502", show_default=True,
227
+ help="CPU plug-in name (see ``dasmos list-cpus``).",
228
+ )
229
+ @click.option(
230
+ "-r", "--renderer", default="beebasm", show_default=True,
231
+ help="Renderer plug-in name (see ``dasmos list-renderers``).",
232
+ )
233
+ @click.option(
234
+ "-e", "--env", "envs", multiple=True,
235
+ help="Environment plug-in to activate. Repeat or comma-separate "
236
+ "to activate several (see ``dasmos list-environments``).",
237
+ )
238
+ @click.option(
239
+ "--entry", "entries", type=ADDRESS, multiple=True,
240
+ help="Entry-point address to seed the trace from. Repeat for "
241
+ "several. Defaults to the load address.",
242
+ )
243
+ @click.option(
244
+ "-o", "--out", "out_path",
245
+ type=click.Path(dir_okay=False, path_type=Path),
246
+ help="Write the rendered output to this file. Defaults to stdout.",
247
+ )
248
+ @click.option(
249
+ "--md5", "md5_hash",
250
+ help="Pin the ROM's MD5 hash. The disassembly fails (without "
251
+ "writing output) if the actual hash differs.",
252
+ )
253
+ @click.option(
254
+ "--encoding", "encoding", default="utf-8", show_default=True,
255
+ help="Encoding for the rendered output written via --out. "
256
+ "Default UTF-8. Reading the ROM is unaffected — ROMs are "
257
+ "binary.",
258
+ )
259
+ def disassemble_command(
260
+ rom: Path,
261
+ load_addr: int,
262
+ cpu: str,
263
+ renderer: str,
264
+ envs: tuple[str, ...],
265
+ entries: tuple[int, ...],
266
+ out_path: Path | None,
267
+ md5_hash: str | None,
268
+ encoding: str,
269
+ ) -> None:
270
+ """Disassemble ROM and write the rendered output.
271
+
272
+ Single-shot: builds a Disassembler with the chosen CPU and
273
+ environments, loads ROM at LOAD-ADDR, seeds the trace from each
274
+ --entry (or LOAD-ADDR by default), runs the trace + classification
275
+ + reference-analysis pipeline, and renders via the chosen
276
+ renderer plug-in.
277
+
278
+ Use ``dasmos init`` to scaffold a driver script you can edit
279
+ instead of re-running this command with growing flag lists.
280
+ """
281
+ from dasmos.disassembler import Disassembler
282
+ from dasmos.extension import ExtensionError
283
+
284
+ env_list = _split_envs(envs)
285
+ try:
286
+ d = Disassembler.create(cpu=cpu, environments=env_list)
287
+ except ExtensionError as exc:
288
+ raise click.ClickException(str(exc)) from exc
289
+ d.load(rom, load_addr, md5sum=md5_hash)
290
+ seed_addrs = entries if entries else (load_addr,)
291
+ for addr in seed_addrs:
292
+ d.entry(addr)
293
+ output = d.disassemble().render(renderer)
294
+ text = str(output)
295
+ if out_path is not None:
296
+ out_path.write_text(text, encoding=encoding)
297
+ else:
298
+ click.echo(text, nl=False)
299
+
300
+
301
+ # ---------------------------------------------------------------------------
302
+ # init — scaffold a starter driver script
303
+ # ---------------------------------------------------------------------------
304
+
305
+
306
+ _DRIVER_TEMPLATE = '''\
307
+ """Disassembly driver for {rom_name}.
308
+
309
+ Generated by ``dasmos init``. Edit this file to add labels,
310
+ comments, and classifications as you analyse the ROM. Run with::
311
+
312
+ python {driver_name}
313
+
314
+ to produce the rendered listing on stdout. See the dasmos docs for
315
+ the full driver-script API and the acornaeology authoring guide
316
+ (``acornaeology.github.io/AUTHORING.md``) for the comment-markdown
317
+ + memory-map metadata conventions.
318
+ """
319
+
320
+ from pathlib import Path
321
+
322
+ import dasmos
323
+
324
+ # ---------------------------------------------------------------------
325
+ # Configuration
326
+ # ---------------------------------------------------------------------
327
+
328
+ ROM_PATH = Path({rom_path_repr})
329
+ LOAD_ADDR = {load_addr_hex}{md5_line}
330
+
331
+ # ---------------------------------------------------------------------
332
+ # Build the disassembler
333
+ # ---------------------------------------------------------------------
334
+
335
+ d = dasmos.Disassembler.create(
336
+ cpu={cpu_repr},
337
+ environments={envs_repr},
338
+ )
339
+ d.load(ROM_PATH, LOAD_ADDR{md5_kwarg})
340
+
341
+ # Entry points — addresses where the trace seeds. Add more as you
342
+ # discover them.
343
+ {entries_block}
344
+
345
+ # ---------------------------------------------------------------------
346
+ # Add your analysis below
347
+ # ---------------------------------------------------------------------
348
+ #
349
+ # As you discover labels, subroutines, and data regions, add them
350
+ # here. The driver-script API:
351
+ #
352
+ # d.label(addr, "name") — required label at addr
353
+ # d.optional_label(addr, "name") — optional (emits only if
354
+ # referenced)
355
+ # d.subroutine(addr, "name", — entry point + label + banner
356
+ # title="...",
357
+ # description="...") — descriptions are full Markdown
358
+ # d.comment(addr, "text") — comment (also Markdown);
359
+ # pass align=Align.INLINE for
360
+ # a trailing remark
361
+ # d.byte(addr, length=N) — classify N bytes as raw data
362
+ # d.string(addr, length=N) — classify N bytes as a string
363
+ # d.constant(value, "name") — name a value (e.g. magic
364
+ # numbers, OSBYTE call IDs)
365
+ #
366
+ # Cross-reference links in comments / descriptions:
367
+ #
368
+ # "see [foo](address:E000)" — bare label link
369
+ # "see [foo](address:E000?hex)" — label + hex literal
370
+
371
+
372
+ # ---------------------------------------------------------------------
373
+ # Render
374
+ # ---------------------------------------------------------------------
375
+
376
+ ir = d.disassemble()
377
+ print(str(ir.render({renderer_repr})))
378
+ '''
379
+
380
+
381
+ def _format_entries_block(entries: tuple[int, ...]) -> str:
382
+ """Render the entry-point block of the generated driver."""
383
+ if not entries:
384
+ return "d.entry(LOAD_ADDR)"
385
+ lines = [f"d.entry({entry:#06x})" for entry in entries]
386
+ return "\n".join(lines)
387
+
388
+
389
+ @cli.command(name="init")
390
+ @click.argument(
391
+ "driver_path", type=click.Path(dir_okay=False, path_type=Path),
392
+ )
393
+ @click.option(
394
+ "--rom", "rom_path",
395
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
396
+ required=True,
397
+ help="Path to the ROM the driver will disassemble.",
398
+ )
399
+ @click.option(
400
+ "-a", "--load-addr", "load_addr", type=ADDRESS, required=True,
401
+ help="Address where the binary is loaded.",
402
+ )
403
+ @click.option(
404
+ "-c", "--cpu", default="nmos6502", show_default=True,
405
+ help="CPU plug-in name.",
406
+ )
407
+ @click.option(
408
+ "-r", "--renderer", default="beebasm", show_default=True,
409
+ help="Renderer plug-in name to use in the generated driver.",
410
+ )
411
+ @click.option(
412
+ "-e", "--env", "envs", multiple=True,
413
+ help="Environment plug-in to activate (repeatable / comma-separated).",
414
+ )
415
+ @click.option(
416
+ "--entry", "entries", type=ADDRESS, multiple=True,
417
+ help="Entry-point address (repeatable). Defaults to LOAD_ADDR.",
418
+ )
419
+ @click.option(
420
+ "--md5", "md5_hash",
421
+ help="Pin the ROM's MD5 hash in the generated driver.",
422
+ )
423
+ @click.option(
424
+ "--force", is_flag=True,
425
+ help="Overwrite an existing driver file.",
426
+ )
427
+ @click.option(
428
+ "--encoding", "encoding", default="utf-8", show_default=True,
429
+ help="Encoding used to write the generated driver file. "
430
+ "Default UTF-8.",
431
+ )
432
+ def init_command(
433
+ driver_path: Path,
434
+ rom_path: Path,
435
+ load_addr: int,
436
+ cpu: str,
437
+ renderer: str,
438
+ envs: tuple[str, ...],
439
+ entries: tuple[int, ...],
440
+ md5_hash: str | None,
441
+ force: bool,
442
+ encoding: str,
443
+ ) -> None:
444
+ """Scaffold a starter dasmos driver at DRIVER_PATH.
445
+
446
+ The generated driver is FUNCTIONAL — running it with
447
+ ``python DRIVER_PATH`` produces the same output as
448
+ ``dasmos disassemble`` with the same flags. Edit the file to
449
+ add labels, comments, classifications, and other analysis.
450
+ """
451
+ if driver_path.exists() and not force:
452
+ raise click.ClickException(
453
+ f"{driver_path} already exists; pass --force to overwrite."
454
+ )
455
+ env_list = _split_envs(envs)
456
+ md5_line = (
457
+ f"\nROM_MD5 = {md5_hash!r}" if md5_hash else ""
458
+ )
459
+ md5_kwarg = ", md5sum=ROM_MD5" if md5_hash else ""
460
+ text = _DRIVER_TEMPLATE.format(
461
+ rom_name=rom_path.name,
462
+ driver_name=driver_path.name,
463
+ # as_posix() so the embedded path is portable: forward slashes
464
+ # work as a Path constructor argument on Windows and don't need
465
+ # backslash-escaping in the str literal.
466
+ rom_path_repr=repr(rom_path.as_posix()),
467
+ load_addr_hex=f"{load_addr:#06x}",
468
+ md5_line=md5_line,
469
+ cpu_repr=repr(cpu),
470
+ envs_repr=repr(env_list),
471
+ md5_kwarg=md5_kwarg,
472
+ entries_block=_format_entries_block(entries),
473
+ renderer_repr=repr(renderer),
474
+ )
475
+ # Always write with an explicit encoding (default UTF-8). On
476
+ # Windows the locale default is cp1252, which would mangle the
477
+ # em-dashes in the template into \x97 bytes — Python would then
478
+ # refuse to run the generated driver since source files must be
479
+ # UTF-8.
480
+ driver_path.write_text(text, encoding=encoding)
481
+ click.echo(f"Wrote driver to {driver_path}", err=True)
482
+
483
+
484
+ if __name__ == "__main__":
485
+ cli()
@@ -0,0 +1,8 @@
1
+ """Internal core machinery for the dasmos disassembler.
2
+
3
+ Lifted from the py8dis fork with design-smell fixes applied per port:
4
+ module-level globals replaced with instance state, ``sys.exit``-style
5
+ failures replaced with raised exceptions, cyclic imports replaced with
6
+ injected dependencies. Consumers should import from the top-level
7
+ :mod:`dasmos` package — this sub-package is internal and may change.
8
+ """