dasmos 4.2.1__tar.gz → 4.3.0__tar.gz

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 (117) hide show
  1. {dasmos-4.2.1/src/dasmos.egg-info → dasmos-4.3.0}/PKG-INFO +2 -2
  2. {dasmos-4.2.1 → dasmos-4.3.0}/README.md +1 -1
  3. {dasmos-4.2.1 → dasmos-4.3.0}/pyproject.toml +1 -1
  4. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/__init__.py +1 -1
  5. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/asm_renderer.py +277 -94
  6. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/labels.py +42 -0
  7. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/move.py +12 -0
  8. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/program.py +39 -0
  9. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/disassembler.py +235 -60
  10. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_mos/hooks.py +7 -8
  11. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/renderers/beebasm/renderer.py +63 -11
  12. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/renderers/json/renderer.py +59 -16
  13. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/renderers/tass64/renderer.py +48 -1
  14. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/hooks.py +14 -12
  15. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ir.py +7 -1
  16. {dasmos-4.2.1 → dasmos-4.3.0/src/dasmos.egg-info}/PKG-INFO +2 -2
  17. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos.egg-info/SOURCES.txt +5 -0
  18. dasmos-4.3.0/tests/test_binary_addressed_api.py +134 -0
  19. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_golden_json.py +5 -3
  20. dasmos-4.3.0/tests/test_move_owned_labels.py +166 -0
  21. dasmos-4.3.0/tests/test_multi_image.py +399 -0
  22. dasmos-4.3.0/tests/test_offset_bases.py +113 -0
  23. dasmos-4.3.0/tests/test_overlapping_moves.py +151 -0
  24. {dasmos-4.2.1 → dasmos-4.3.0}/LICENSE +0 -0
  25. {dasmos-4.2.1 → dasmos-4.3.0}/setup.cfg +0 -0
  26. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/_text.py +0 -0
  27. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/cli.py +0 -0
  28. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/__init__.py +0 -0
  29. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/annotations.py +0 -0
  30. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/classification.py +0 -0
  31. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/config.py +0 -0
  32. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/cpu_state.py +0 -0
  33. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/data_type.py +0 -0
  34. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/disassembly.py +0 -0
  35. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/expr.py +0 -0
  36. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/expr_parse.py +0 -0
  37. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/file_header.py +0 -0
  38. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/format_hint.py +0 -0
  39. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/markdown_asm.py +0 -0
  40. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/memory.py +0 -0
  41. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/core/regions.py +0 -0
  42. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/cpu.py +0 -0
  43. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/environment.py +0 -0
  44. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/exceptions.py +0 -0
  45. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/expr.py +0 -0
  46. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/__init__.py +0 -0
  47. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/cpus/__init__.py +0 -0
  48. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/cpus/cpu6502/__init__.py +0 -0
  49. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/cpus/cpu6502/cpu.py +0 -0
  50. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/cpus/cpu65c02/__init__.py +0 -0
  51. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/cpus/cpu65c02/cpu.py +0 -0
  52. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/__init__.py +0 -0
  53. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/_acorn_bbc_common.py +0 -0
  54. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_fdc_1770/__init__.py +0 -0
  55. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_fdc_1770/environment.py +0 -0
  56. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_fdc_8271/__init__.py +0 -0
  57. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_fdc_8271/environment.py +0 -0
  58. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_master_hardware/__init__.py +0 -0
  59. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_master_hardware/environment.py +0 -0
  60. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_model_b_hardware/__init__.py +0 -0
  61. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_model_b_hardware/environment.py +0 -0
  62. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_mos/__init__.py +0 -0
  63. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_mos/enums.py +0 -0
  64. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_mos/environment.py +0 -0
  65. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_sideways_rom/__init__.py +0 -0
  66. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/acorn_sideways_rom/environment.py +0 -0
  67. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/bbc_basic_6502/__init__.py +0 -0
  68. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/bbc_basic_6502/environment.py +0 -0
  69. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/environments/bbc_basic_6502/floats.py +0 -0
  70. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/renderers/__init__.py +0 -0
  71. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/renderers/beebasm/__init__.py +0 -0
  72. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/renderers/json/__init__.py +0 -0
  73. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/ext/renderers/tass64/__init__.py +0 -0
  74. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/extension.py +0 -0
  75. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/output.py +0 -0
  76. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/py.typed +0 -0
  77. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos/renderer.py +0 -0
  78. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos.egg-info/dependency_links.txt +0 -0
  79. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos.egg-info/entry_points.txt +0 -0
  80. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos.egg-info/requires.txt +0 -0
  81. {dasmos-4.2.1 → dasmos-4.3.0}/src/dasmos.egg-info/top_level.txt +0 -0
  82. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_absolute_operands.py +0 -0
  83. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_adfs_roundtrip.py +0 -0
  84. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_anfs_418_roundtrip.py +0 -0
  85. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_anfs_421_roundtrip.py +0 -0
  86. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_beebasm.py +0 -0
  87. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_cli.py +0 -0
  88. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_cpu.py +0 -0
  89. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_cpu6502.py +0 -0
  90. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_cpu65c02.py +0 -0
  91. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_disassembler.py +0 -0
  92. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_driver_roundtrip.py +0 -0
  93. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_econet_bridge_roundtrip.py +0 -0
  94. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_environment.py +0 -0
  95. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_expr.py +0 -0
  96. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_expr_operators.py +0 -0
  97. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_expr_parse.py +0 -0
  98. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_expression_symbols.py +0 -0
  99. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_file_header.py +0 -0
  100. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_forward_operand_size.py +0 -0
  101. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_golden_asm.py +0 -0
  102. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_include_binary.py +0 -0
  103. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_index_regions.py +0 -0
  104. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_json_renderer.py +0 -0
  105. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_macros.py +0 -0
  106. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_mnemonic_hash.py +0 -0
  107. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_nfs_334_roundtrip.py +0 -0
  108. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_nfs_roundtrip.py +0 -0
  109. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_output.py +0 -0
  110. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_program.py +0 -0
  111. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_py8dis2dasmos.py +0 -0
  112. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_reference_kinds.py +0 -0
  113. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_renderer.py +0 -0
  114. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_smoke.py +0 -0
  115. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_tass64.py +0 -0
  116. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_trace.py +0 -0
  117. {dasmos-4.2.1 → dasmos-4.3.0}/tests/test_tube_client_roundtrip.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dasmos
3
- Version: 4.2.1
3
+ Version: 4.3.0
4
4
  Summary: Extensible tracing disassembler for classic CPUs, with CPU, renderer and environment extension points.
5
5
  Author-email: Robert Smallshire <rob@sixty-north.com>
6
6
  License-Expression: MIT
@@ -32,7 +32,7 @@ Dynamic: license-file
32
32
  <img src="https://raw.githubusercontent.com/acornaeology/dasmos/master/docs/_static/dasmos-banner.svg" alt="Dasmos — an extensible tracing disassembler for classic CPUs">
33
33
  </p>
34
34
 
35
- An extensible tracing disassembler for classic CPUs, version `4.2.1`.
35
+ An extensible tracing disassembler for classic CPUs, version `4.3.0`.
36
36
 
37
37
  <p align="center">
38
38
  <a href="https://pypi.org/project/dasmos/"><img src="https://img.shields.io/pypi/v/dasmos.svg" alt="PyPI"></a>
@@ -2,7 +2,7 @@
2
2
  <img src="https://raw.githubusercontent.com/acornaeology/dasmos/master/docs/_static/dasmos-banner.svg" alt="Dasmos — an extensible tracing disassembler for classic CPUs">
3
3
  </p>
4
4
 
5
- An extensible tracing disassembler for classic CPUs, version `4.2.1`.
5
+ An extensible tracing disassembler for classic CPUs, version `4.3.0`.
6
6
 
7
7
  <p align="center">
8
8
  <a href="https://pypi.org/project/dasmos/"><img src="https://img.shields.io/pypi/v/dasmos.svg" alt="PyPI"></a>
@@ -108,7 +108,7 @@ dev = [
108
108
  ]
109
109
 
110
110
  [tool.bumpversion]
111
- current_version = "4.2.1"
111
+ current_version = "4.3.0"
112
112
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
113
113
  serialize = ["{major}.{minor}.{patch}"]
114
114
  tag = true
@@ -4,7 +4,7 @@ The top-level package re-exports the public API. Consumers should import
4
4
  from :mod:`dasmos` rather than reaching into sub-modules.
5
5
  """
6
6
 
7
- __version__ = "4.2.1"
7
+ __version__ = "4.3.0"
8
8
 
9
9
  from dasmos.core.annotations import Align, Annotation, AnnotationStore, Banner, Comment
10
10
  from dasmos.core.format_hint import FormatHint
@@ -19,6 +19,7 @@ subclass's syntax methods.
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
+ import re
22
23
  import warnings
23
24
  from typing import TYPE_CHECKING
24
25
 
@@ -54,7 +55,7 @@ from dasmos.core.expr import (
54
55
  from dasmos.core.format_hint import FormatHint
55
56
  from dasmos.core.markdown_asm import markdown_to_asm_text, strip_address_uri_links
56
57
  from dasmos.core.memory import BinaryAddr
57
- from dasmos.core.move import Move
58
+ from dasmos.core.move import BASE_MOVE_ID, Move
58
59
  from dasmos.cpu import Opcode, OperandKind
59
60
  from dasmos.exceptions import MacroRenderError
60
61
  from dasmos.output import TextOutput
@@ -220,7 +221,9 @@ class AssemblerRenderer(TextRenderer):
220
221
  """
221
222
  return None
222
223
 
223
- def _save_directive(self, load_start, load_end, program=None) -> str | None:
224
+ def _save_directive(
225
+ self, load_start, load_end, program=None, name=None,
226
+ ) -> str | None:
224
227
  """A directive writing the loaded range to a binary file, or
225
228
  ``None`` when the assembler saves via a command-line flag
226
229
  instead. Default: ``None`` (no in-source save). Beebasm
@@ -229,10 +232,76 @@ class AssemblerRenderer(TextRenderer):
229
232
 
230
233
  ``program`` is the IR's :class:`~dasmos.core.program.ProgramInfo`
231
234
  (or ``None``) — load-and-run metadata (exec/reload addresses) a
232
- backend may fold into its save directive.
235
+ backend may fold into its save directive. ``name`` is the
236
+ single image's declared name (``d.load(name=...)``), or
237
+ ``None``; an explicit :attr:`output_filename` takes precedence.
233
238
  """
234
239
  return None
235
240
 
241
+ def _image_save_directive(self, image, start_ref: str, end_ref: str) -> str | None:
242
+ """A directive saving one of several loaded images as its own
243
+ file (#49), or ``None`` when the assembler has no in-source save.
244
+
245
+ ``image`` is the :class:`~dasmos.core.program.LoadedImage`;
246
+ ``start_ref`` / ``end_ref`` are the expressions for its range —
247
+ its boundary labels, or hex literals when those are suppressed.
248
+ Default ``None``; beebasm overrides with its ``save``.
249
+ """
250
+ return None
251
+
252
+ def _segment_boundary_labels(
253
+ self, segments,
254
+ ) -> list[tuple[str | None, str | None]]:
255
+ """``(start_label, end_label)`` for each segment of the main walk.
256
+
257
+ A single segment uses the familiar ``{prefix}start`` /
258
+ ``{prefix}end``. Several (one per image) get
259
+ ``{prefix}{name}_start`` / ``{prefix}{name}_end``, the image
260
+ name made into an identifier (``Dunjun4`` -> ``dunjun4``, ``Game
261
+ B`` -> ``game_b``) and kept unique. Suppressed boundary labels
262
+ (an empty prefix) give ``(None, None)``.
263
+ """
264
+ if len(segments) == 1:
265
+ return [(self.boundary_start_label, self.boundary_end_label)]
266
+ if not self.emit_boundary_labels:
267
+ return [(None, None)] * len(segments)
268
+ prefix = self.boundary_label_prefix
269
+ return [
270
+ (f"{prefix}{ident}_start", f"{prefix}{ident}_end")
271
+ for ident in self._image_identifiers(
272
+ [image for _start, _end, image in segments],
273
+ )
274
+ ]
275
+
276
+ @staticmethod
277
+ def _image_identifiers(images) -> list[str]:
278
+ """An identifier for each image, from its name — lower-cased,
279
+ other characters folded to ``_`` (``Game B`` -> ``game_b``) —
280
+ made unique by a numeric suffix. Used for per-image boundary
281
+ labels and section names."""
282
+ identifiers: list[str] = []
283
+ for image in images:
284
+ stem = re.sub(r"[^0-9a-z_]+", "_", image.name.lower()).strip("_") or "image"
285
+ candidate, n = stem, 2
286
+ while candidate in identifiers:
287
+ candidate, n = f"{stem}_{n}", n + 1
288
+ identifiers.append(candidate)
289
+ return identifiers
290
+
291
+ def image_section_declarations(self, section_ids: list[str]) -> list[str]:
292
+ """Lines declaring one output section per loaded image, emitted
293
+ near the top when several images are loaded (#49). Default: none
294
+ — a backend that saves in-source (beebasm) needs no sections."""
295
+ return []
296
+
297
+ def image_section_start(self, section_id: str) -> list[str]:
298
+ """Lines opening (or reopening) an image's section. Default: none."""
299
+ return []
300
+
301
+ def image_section_end(self, section_id: str) -> list[str]:
302
+ """Lines closing an image's section. Default: none."""
303
+ return []
304
+
236
305
  def included_binary_directive(self, path: str) -> list[str]:
237
306
  """Lines emitting an external binary include for ``path`` (an
238
307
  :class:`~dasmos.core.classification.IncludedBinary` region).
@@ -272,6 +341,10 @@ class AssemblerRenderer(TextRenderer):
272
341
  # uses ``b2r`` (most-recent move) and so misses inline anchors
273
342
  # from a non-most-recent move's body emission.
274
343
  self._inline_emitted_runtime_addrs: set[int] = set()
344
+ # Every label name emitted inline so far (as ``.name`` or as an
345
+ # inline ``name = base+offset``), so each is defined exactly
346
+ # once however many overlays reach its address.
347
+ self._inline_emitted_names: set[str] = set()
275
348
  # Macros invoked *natively* (as a value function or a statement)
276
349
  # during this render — only these get a definition emitted; a
277
350
  # macro used only via inline-expansion needs none.
@@ -291,6 +364,10 @@ class AssemblerRenderer(TextRenderer):
291
364
  # addresses of the labels it names (see :meth:`_render_opcode`);
292
365
  # ``None`` otherwise.
293
366
  self._operand_label_refs: set[int] | None = None
367
+ # The images saved as separate files by this render — set when
368
+ # more than one is loaded (#49), so :meth:`build_command` can
369
+ # describe one output per image. Empty for a single image.
370
+ self._saved_images: list = []
294
371
 
295
372
  def render(self, ir: "IntermediateRepresentation") -> TextOutput:
296
373
  """Walk the IR's classifications and emit a beebasm source
@@ -362,21 +439,53 @@ class AssemblerRenderer(TextRenderer):
362
439
  for move in [moves_by_src[src]]
363
440
  }
364
441
 
442
+ # The main walk runs one segment per saved image. A single image
443
+ # (the usual case) is one segment spanning the whole load range.
444
+ # Several separately-loaded images (#49) are each their own
445
+ # segment — own ``org``, boundary labels and save — so the gaps
446
+ # between them are never assembled or saved; a backend that
447
+ # saves per section (64tass) also gets each image's content,
448
+ # moved blocks included, wrapped in that image's section.
449
+ images = sorted(ir.images, key=lambda image: image.start)
450
+ if len(images) > 1:
451
+ segments = [(image.start, image.end, image) for image in images]
452
+ section_ids = self._image_identifiers(images)
453
+ self._saved_images = images
454
+ if self.output_filename is not None:
455
+ warnings.warn(
456
+ f"output_filename {self.output_filename!r} is ignored: "
457
+ f"{len(images)} images are loaded and each is saved "
458
+ f"under its own name (see d.load(name=...))",
459
+ UserWarning,
460
+ stacklevel=2,
461
+ )
462
+ lines.extend(self.image_section_declarations(section_ids))
463
+ else:
464
+ segments = [(int(load_start), int(load_end), None)]
465
+ section_ids = [None]
466
+ boundary_labels = self._segment_boundary_labels(segments)
467
+
468
+ def section_of(binary_addr: int):
469
+ """The section id of the image holding ``binary_addr``."""
470
+ for (start, end, _image), section_id in zip(segments, section_ids):
471
+ if start <= binary_addr < end:
472
+ return section_id
473
+ return None
474
+
365
475
  # PHASE 1: emit move regions first, in source-binary-address
366
476
  # order. Each move's emission is self-contained: org-to-src,
367
477
  # source label, move-enter, body, move-exit.
368
478
  for src_addr in sorted(moves_by_src):
369
479
  move = moves_by_src[src_addr]
480
+ section_id = section_of(int(src_addr))
481
+ if section_id is not None:
482
+ lines.extend(self.image_section_start(section_id))
370
483
  lines.extend(
371
484
  self._emit_move_section(ir, move, moves_by_src, move_ids)
372
485
  )
486
+ if section_id is not None:
487
+ lines.extend(self.image_section_end(section_id))
373
488
 
374
- # PHASE 2: main code. ``org`` to the load start (resetting
375
- # PC after the moves' restore-to-source positions left it
376
- # somewhere arbitrary) plus the boundary start marker.
377
- lines.extend(self.code_start(load_start, load_end, first=True))
378
- if self.boundary_start_label is not None:
379
- lines.append(self.inline_label(self.boundary_start_label))
380
489
 
381
490
  # Skip ranges: any binary address covered by ANY move's
382
491
  # source range was emitted in phase 1.
@@ -384,39 +493,67 @@ class AssemblerRenderer(TextRenderer):
384
493
  (src, src + mv.length) for src, mv in moves_by_src.items()
385
494
  ]
386
495
 
387
- # Track PC through the walk so we can emit ``org &<addr>``
388
- # whenever we resume after a skipped (move-source) range —
389
- # the main walk needs to resume at the byte just after the
390
- # last move source.
391
- expected_pc = int(load_start)
392
- for binary_addr, classification in ir.classifications.iter_classified_starts():
393
- ba = int(binary_addr)
394
- if not (int(load_start) <= ba < int(load_end)):
395
- continue
396
- if any(s <= ba < e for s, e in move_src_ranges):
397
- continue
398
- if ba != expected_pc:
399
- lines.extend(self.set_origin(ba))
400
- expected_pc = ba
401
- lines.extend(self._emit_classification_at(
402
- ir, ba, classification,
403
- active_move=None, active_move_id=None,
404
- ))
405
- expected_pc = ba + classification.length()
406
-
407
- # Position PC at load_end before emitting the boundary end
408
- # marker so the label takes the load_end value (otherwise the
409
- # marker lands wherever the last classification's last byte
410
- # ended, which can be earlier when a move source extends to
411
- # load_end).
412
- if self.boundary_end_label is not None:
413
- if expected_pc != int(load_end):
414
- lines.extend(self.set_origin(int(load_end)))
415
- lines.append(self.inline_label(self.boundary_end_label))
416
- save = self._save_directive(load_start, load_end, ir.program)
417
- if save is not None:
496
+ for index, (seg_start, seg_end, _image) in enumerate(segments):
497
+ start_label, end_label = boundary_labels[index]
498
+ if section_ids[index] is not None:
499
+ lines.extend(self.image_section_start(section_ids[index]))
500
+ # ``org`` to the segment start (resetting PC after the moves'
501
+ # restore-to-source positions left it somewhere arbitrary)
502
+ # plus the boundary start marker.
503
+ lines.extend(self.code_start(seg_start, seg_end, first=index == 0))
504
+ if start_label is not None:
505
+ lines.append(self.inline_label(start_label))
506
+
507
+ # Track PC through the walk so we can emit ``org &<addr>``
508
+ # whenever we resume after a skipped (move-source) range —
509
+ # the main walk needs to resume at the byte just after the
510
+ # last move source.
511
+ expected_pc = seg_start
512
+ for binary_addr, classification in ir.classifications.iter_classified_starts():
513
+ ba = int(binary_addr)
514
+ if not (seg_start <= ba < seg_end):
515
+ continue
516
+ if any(s <= ba < e for s, e in move_src_ranges):
517
+ continue
518
+ if ba != expected_pc:
519
+ lines.extend(self.set_origin(ba))
520
+ expected_pc = ba
521
+ lines.extend(self._emit_classification_at(
522
+ ir, ba, classification,
523
+ active_move=None, active_move_id=None,
524
+ ))
525
+ expected_pc = ba + classification.length()
526
+
527
+ # Position PC at the segment end before emitting the boundary
528
+ # end marker so the label takes that value (otherwise the
529
+ # marker lands wherever the last classification's last byte
530
+ # ended, which can be earlier when a move source extends to
531
+ # the end).
532
+ if end_label is not None:
533
+ if expected_pc != seg_end:
534
+ lines.extend(self.set_origin(seg_end))
535
+ lines.append(self.inline_label(end_label))
536
+ if section_ids[index] is not None:
537
+ lines.extend(self.image_section_end(section_ids[index]))
538
+
539
+ if len(segments) > 1:
540
+ saves = [
541
+ self._image_save_directive(
542
+ image,
543
+ boundary_labels[index][0] or self.hex4(seg_start),
544
+ boundary_labels[index][1] or self.hex4(seg_end),
545
+ )
546
+ for index, (seg_start, seg_end, image) in enumerate(segments)
547
+ ]
548
+ else:
549
+ # A single image's declared name names its save, unless the
550
+ # renderer was given an explicit output filename.
551
+ name = images[0].name if images and images[0].name_declared else None
552
+ saves = [self._save_directive(load_start, load_end, ir.program, name)]
553
+ saves = [save for save in saves if save is not None]
554
+ if saves:
418
555
  lines.append("")
419
- lines.append(save)
556
+ lines.extend(saves)
420
557
  lines.extend(self.disassembly_end())
421
558
 
422
559
  # End-of-file label-reference frequency table.
@@ -626,7 +763,10 @@ class AssemblerRenderer(TextRenderer):
626
763
  end = src_addr + length
627
764
  move_id = move_ids.get(id(move))
628
765
  src_label = self._first_explicit_name(ir, src_addr)
629
- dest_label = self._first_explicit_name(ir, dest_addr)
766
+ # The anchor for the trailer's copyblock / clear: a name this
767
+ # move's own body defines (or one belonging to no overlay), never
768
+ # another overlay's name from a later section.
769
+ dest_label = self._first_explicit_name(ir, dest_addr, move.move_id)
630
770
 
631
771
  # Position PC at the move's source binary address. Every
632
772
  # move emits this so the moves can appear in any order in
@@ -635,6 +775,7 @@ class AssemblerRenderer(TextRenderer):
635
775
  if src_label is not None:
636
776
  lines.append(self.inline_label(src_label))
637
777
  self._inline_emitted_runtime_addrs.add(src_addr)
778
+ self._inline_emitted_names.add(src_label)
638
779
 
639
780
  # Switch PC into the relocated destination range via the
640
781
  # backend's pseudo-PC hook (beebasm ``org``+``copyblock`` idiom;
@@ -699,31 +840,33 @@ class AssemblerRenderer(TextRenderer):
699
840
  for ann in ir.annotations.get_for_align(binary_addr, Align.BEFORE_LABEL):
700
841
  lines.extend(self._render_annotation(ann))
701
842
 
702
- # Inline labels at this runtime address. Skip if a previous
703
- # body walk has already emitted the inline anchor — this
704
- # happens for multi-source-same-destination moves: the label
705
- # at the shared destination is emitted once (during the move
706
- # whose body walk reaches the address first), and the
707
- # subsequent moves' body walks hit the same runtime address
708
- # and would otherwise re-emit ``.<name>`` (which beebasm
709
- # rejects with "Symbol already defined"). The ``copyblock``
710
- # / ``clear`` directives in the per-move trailers still
711
- # resolve the name correctly via the single inline definition.
843
+ # Inline labels at this runtime address. A name is emitted once:
844
+ # moves that overlay the same run range (ADFS's three NMI
845
+ # handlers at &0D0A) each walk the shared addresses, and beebasm
846
+ # rejects a second ``.<name>`` (D-023). Each overlay emits the
847
+ # names visible from it — its own (declared with ``move=``, or
848
+ # made for its references) plus any belonging to no overlay — so
849
+ # a name lands in the section of the move it describes (#53),
850
+ # and a name belonging to no overlay lands in the first section
851
+ # to reach it. The ``copyblock`` / ``clear`` directives in the
852
+ # per-move trailers resolve names wherever they are defined.
712
853
  label = ir.labels.get_label(runtime_addr)
713
- if (
714
- label is not None
715
- and runtime_addr not in self._inline_emitted_runtime_addrs
716
- ):
717
- xref = self._format_inline_xref_summary(label, runtime_addr)
718
- if xref is not None:
719
- lines.append(xref)
854
+ overlay = active_move.move_id if active_move is not None else BASE_MOVE_ID
855
+ if label is not None:
856
+ visible = ir.labels.names_in_overlay(runtime_addr, overlay)
857
+ names = [n for n in visible if n not in self._inline_emitted_names]
858
+ first_visit = runtime_addr not in self._inline_emitted_runtime_addrs
859
+ if first_visit and (names or not label.explicit_name_texts()):
860
+ xref = self._format_inline_xref_summary(label, runtime_addr)
861
+ if xref is not None:
862
+ lines.append(xref)
720
863
  # Insertion order, not alphabetical — preserves the label
721
864
  # author's intended sequence at addresses with multiple
722
865
  # names. The first-registered name is the structural one
723
866
  # and should lead.
724
- names = label.explicit_names_in_insertion_order()
725
867
  for name in names:
726
868
  lines.append(self.inline_label(name))
869
+ self._inline_emitted_names.add(name)
727
870
  if names:
728
871
  self._inline_emitted_runtime_addrs.add(runtime_addr)
729
872
  # Mid-instruction labels INSIDE this classification's span
@@ -735,9 +878,8 @@ class AssemblerRenderer(TextRenderer):
735
878
  # against the inline anchor's PC, which IS at the base's
736
879
  # runtime. (Example: ``tube_cmd_lo = tube_dispatch_cmd+1``
737
880
  # inside move 1 of NFS-3.65.)
738
- base_names = label.explicit_names_in_insertion_order()
739
- if base_names:
740
- base_name = base_names[0]
881
+ if visible:
882
+ base_name = visible[0]
741
883
  for off in range(1, classification.length()):
742
884
  inner_binary = binary_addr + off
743
885
  if active_move is not None:
@@ -750,6 +892,19 @@ class AssemblerRenderer(TextRenderer):
750
892
  inner_label = ir.labels.get_label(inner_runtime)
751
893
  if inner_label is None:
752
894
  continue
895
+ inner_names = [
896
+ name for name in ir.labels.names_in_overlay(
897
+ inner_runtime, overlay,
898
+ )
899
+ if name not in self._inline_emitted_names
900
+ ]
901
+ # A nameless inner label (references only) still gets
902
+ # its xref summary and annotations, once. Names that
903
+ # are already defined, or that belong to another
904
+ # overlay of this run range, are left alone.
905
+ nameless = not inner_label.explicit_name_texts()
906
+ if not inner_names and not (nameless and first_visit):
907
+ continue
753
908
  # Mid-instruction labels live inside an opcode's
754
909
  # operand bytes — the addr the human cares about
755
910
  # is the BINARY one (where the byte sits in the
@@ -781,8 +936,9 @@ class AssemblerRenderer(TextRenderer):
781
936
  inner_binary, Align.BEFORE_LINE,
782
937
  ):
783
938
  lines.extend(self._render_annotation(ann))
784
- for inner_name in inner_label.explicit_names_in_insertion_order():
939
+ for inner_name in inner_names:
785
940
  lines.append(f"{inner_name} = {base_name}+{off}")
941
+ self._inline_emitted_names.add(inner_name)
786
942
  self._inline_emitted_runtime_addrs.add(inner_runtime)
787
943
 
788
944
  for ann in ir.annotations.get_for_align(binary_addr, Align.AFTER_LABEL):
@@ -971,29 +1127,35 @@ class AssemblerRenderer(TextRenderer):
971
1127
  # emission yet (e.g. user added them after rendering
972
1128
  # started — currently impossible but the codepath is
973
1129
  # defensive).
974
- if runtime_addr in self._inline_emitted_runtime_addrs:
975
- continue
976
- inline_anchor = (
977
- in_range
978
- and self._label_address_is_classification_start(ir, runtime_addr)
979
- )
980
- if inline_anchor:
981
- continue
982
- # Mid-instruction labels with a named base are emitted
983
- # inline under the base as ``name = base+offset`` — skip
984
- # the equate table to avoid double-definition.
985
- if (
986
- in_range
987
- and self._label_has_inline_offset_base(ir, runtime_addr)
988
- ):
989
- continue
1130
+ #
1131
+ # The test is per name: where several moves overlay the
1132
+ # address, each overlay anchors only its own names (#53), so
1133
+ # one name can be anchored while another at the same address
1134
+ # still needs its equate here.
990
1135
  explicit_names = sorted({
991
1136
  name.text
992
1137
  for name_list in label.explicit_names.values()
993
1138
  for name in name_list
994
- })
1139
+ } - self._inline_emitted_names)
995
1140
  if not explicit_names:
996
1141
  continue
1142
+ if runtime_addr not in self._inline_emitted_runtime_addrs:
1143
+ inline_anchor = (
1144
+ in_range
1145
+ and self._label_address_is_classification_start(
1146
+ ir, runtime_addr,
1147
+ )
1148
+ )
1149
+ if inline_anchor:
1150
+ continue
1151
+ # Mid-instruction labels with a named base are emitted
1152
+ # inline under the base as ``name = base+offset`` — skip
1153
+ # the equate table to avoid double-definition.
1154
+ if (
1155
+ in_range
1156
+ and self._label_has_inline_offset_base(ir, runtime_addr)
1157
+ ):
1158
+ continue
997
1159
  if (
998
1160
  not in_range
999
1161
  and not label.required
@@ -1085,16 +1247,16 @@ class AssemblerRenderer(TextRenderer):
1085
1247
  return out
1086
1248
 
1087
1249
  @staticmethod
1088
- def _first_explicit_name(ir, runtime_addr: int) -> str | None:
1089
- """First (alphabetically) explicit name on the label at this
1090
- runtime address, or ``None``. Used as the symbolic anchor for
1091
- relocation directives — beebasm's ``copyblock`` / ``clear`` /
1092
- ``org`` need named anchors to reference.
1250
+ def _first_explicit_name(
1251
+ ir, runtime_addr: int, move_id: int = BASE_MOVE_ID,
1252
+ ) -> str | None:
1253
+ """First-registered explicit name at this runtime address as seen
1254
+ from the overlay laid out by ``move_id``, or ``None``. Used as
1255
+ the symbolic anchor for relocation directives — beebasm's
1256
+ ``copyblock`` / ``clear`` / ``org`` need named anchors to
1257
+ reference.
1093
1258
  """
1094
- label = ir.labels.get_label(runtime_addr)
1095
- if label is None:
1096
- return None
1097
- names = label.explicit_names_in_insertion_order()
1259
+ names = ir.labels.names_in_overlay(runtime_addr, move_id)
1098
1260
  return names[0] if names else None
1099
1261
 
1100
1262
  def _build_stats_block(self, ir) -> list[str]:
@@ -2011,11 +2173,17 @@ class AssemblerRenderer(TextRenderer):
2011
2173
 
2012
2174
  if kind is OperandKind.ADDRESS_8:
2013
2175
  v = ir.memory.get_u8(operand_addr)
2014
- return self._addr_text(ir, v, width=8, using_binary_addr=using_addr)
2176
+ return self._addr_text(
2177
+ ir, v, width=8, using_binary_addr=using_addr,
2178
+ active_move=active_move,
2179
+ )
2015
2180
 
2016
2181
  if kind in (OperandKind.ADDRESS_16, OperandKind.ADDRESS_16_INDIRECT):
2017
2182
  v = ir.memory.get_u16_le(operand_addr)
2018
- return self._addr_text(ir, v, width=16, using_binary_addr=using_addr)
2183
+ return self._addr_text(
2184
+ ir, v, width=16, using_binary_addr=using_addr,
2185
+ active_move=active_move,
2186
+ )
2019
2187
 
2020
2188
  if kind is OperandKind.RELATIVE_OFFSET:
2021
2189
  offset = ir.memory.get_u8(operand_addr)
@@ -2041,6 +2209,7 @@ class AssemblerRenderer(TextRenderer):
2041
2209
  target = base_runtime + opcode.length() + offset
2042
2210
  return self._addr_text(
2043
2211
  ir, target, width=16, using_binary_addr=using_addr,
2212
+ active_move=active_move,
2044
2213
  )
2045
2214
 
2046
2215
  # Defensive — the mode-name dispatch in _render_operand
@@ -2054,6 +2223,7 @@ class AssemblerRenderer(TextRenderer):
2054
2223
  *,
2055
2224
  width: int,
2056
2225
  using_binary_addr: int | None = None,
2226
+ active_move=None,
2057
2227
  ) -> str:
2058
2228
  """Render an address operand: a label name if one is
2059
2229
  registered, otherwise the appropriate hex literal.
@@ -2086,7 +2256,18 @@ class AssemblerRenderer(TextRenderer):
2086
2256
  # they flow through this path naturally. Insertion order
2087
2257
  # picks the structural / first-registered name when an
2088
2258
  # address has multiple aliases.
2089
- names = label.explicit_names_in_insertion_order()
2259
+ # Inside a move that overlays a shared run range, prefer the
2260
+ # names that overlay sees (its own, then any belonging to no
2261
+ # overlay) over another overlay's (#53); every name at the
2262
+ # address has the same value, so this is about readability.
2263
+ overlay = (
2264
+ active_move.move_id if active_move is not None else BASE_MOVE_ID
2265
+ )
2266
+ visible = ir.labels.names_in_overlay(addr, overlay)
2267
+ names = visible + [
2268
+ n for n in label.explicit_names_in_insertion_order()
2269
+ if n not in visible
2270
+ ]
2090
2271
  if names:
2091
2272
  if not self._label_address_is_in_range(ir, addr):
2092
2273
  self._used_external_labels.add(addr)
@@ -2658,7 +2839,9 @@ class AssemblerRenderer(TextRenderer):
2658
2839
  def _render_ref_node(self, ir, node: Ref, active_move) -> str:
2659
2840
  """Resolve a :class:`Ref` to its label's best name, or a hex
2660
2841
  literal when the address has no name."""
2661
- return self._addr_text(ir, int(node.runtime_addr), width=16)
2842
+ return self._addr_text(
2843
+ ir, int(node.runtime_addr), width=16, active_move=active_move,
2844
+ )
2662
2845
 
2663
2846
  #: Unary operators rendered as a *function-like* form that brackets
2664
2847
  #: its own operand (so operand precedence is irrelevant and no
@@ -216,6 +216,15 @@ class Label:
216
216
  for name in name_list
217
217
  }
218
218
 
219
+ def names_for_move(self, move_id: int) -> list[str]:
220
+ """The explicit names recorded for ``move_id``, de-duplicated,
221
+ in registration order."""
222
+ out: list[str] = []
223
+ for name in self.explicit_names.get(move_id, []):
224
+ if name.text not in out:
225
+ out.append(name.text)
226
+ return out
227
+
219
228
  def explicit_names_in_insertion_order(self) -> list[str]:
220
229
  """The explicit-name strings, de-duplicated but in registration
221
230
  order. Use this when rendering the visible label sequence at
@@ -314,6 +323,39 @@ class LabelManager:
314
323
  def get_label(self, runtime_addr) -> Label | None:
315
324
  return self._labels.get(RuntimeAddr(runtime_addr))
316
325
 
326
+ def names_in_overlay(self, runtime_addr, move_id: int) -> list[str]:
327
+ """The explicit names at ``runtime_addr`` as seen from the
328
+ overlay laid out by ``move_id`` (``BASE_MOVE_ID`` for unmoved
329
+ code), in registration order.
330
+
331
+ Several moves can share a run range — alternative overlays of
332
+ the same RAM. A name recorded for one of the moves covering
333
+ ``runtime_addr`` names that move's layout, so the other
334
+ overlays don't see it (#53); a name recorded for no covering
335
+ move is visible from all of them. Where at most one move covers
336
+ the address there is nothing to tell apart and every name is
337
+ returned.
338
+ """
339
+ label = self.get_label(runtime_addr)
340
+ if label is None:
341
+ return []
342
+ covering = self._move_manager.move_ids_for_runtime_addr(runtime_addr)
343
+ if len(covering) <= 1:
344
+ return label.explicit_names_in_insertion_order()
345
+ out: list[str] = []
346
+ for bucket, name_list in label.explicit_names.items():
347
+ if bucket != move_id and bucket in covering:
348
+ continue
349
+ for name in name_list:
350
+ if name.text not in out:
351
+ out.append(name.text)
352
+ return out
353
+ covering = self._move_manager.move_ids_for_runtime_addr(runtime_addr)
354
+ for bucket, name_list in label.explicit_names.items():
355
+ if bucket in covering and any(n.text == name for n in name_list):
356
+ return bucket
357
+ return None
358
+
317
359
  def addr_for_name(self, name: str) -> RuntimeAddr | None:
318
360
  """Find the runtime address of a label by one of its names.
319
361