dasmos 4.2.0__tar.gz → 4.2.1__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 (112) hide show
  1. {dasmos-4.2.0/src/dasmos.egg-info → dasmos-4.2.1}/PKG-INFO +2 -2
  2. {dasmos-4.2.0 → dasmos-4.2.1}/README.md +1 -1
  3. {dasmos-4.2.0 → dasmos-4.2.1}/pyproject.toml +1 -1
  4. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/__init__.py +1 -1
  5. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/asm_renderer.py +261 -2
  6. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/renderers/beebasm/renderer.py +6 -0
  7. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/renderers/tass64/renderer.py +14 -0
  8. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/renderer.py +23 -0
  9. {dasmos-4.2.0 → dasmos-4.2.1/src/dasmos.egg-info}/PKG-INFO +2 -2
  10. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos.egg-info/SOURCES.txt +3 -0
  11. dasmos-4.2.1/tests/test_absolute_operands.py +211 -0
  12. dasmos-4.2.1/tests/test_expression_symbols.py +162 -0
  13. dasmos-4.2.1/tests/test_forward_operand_size.py +237 -0
  14. {dasmos-4.2.0 → dasmos-4.2.1}/LICENSE +0 -0
  15. {dasmos-4.2.0 → dasmos-4.2.1}/setup.cfg +0 -0
  16. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/_text.py +0 -0
  17. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/cli.py +0 -0
  18. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/__init__.py +0 -0
  19. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/annotations.py +0 -0
  20. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/classification.py +0 -0
  21. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/config.py +0 -0
  22. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/cpu_state.py +0 -0
  23. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/data_type.py +0 -0
  24. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/disassembly.py +0 -0
  25. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/expr.py +0 -0
  26. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/expr_parse.py +0 -0
  27. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/file_header.py +0 -0
  28. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/format_hint.py +0 -0
  29. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/labels.py +0 -0
  30. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/markdown_asm.py +0 -0
  31. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/memory.py +0 -0
  32. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/move.py +0 -0
  33. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/program.py +0 -0
  34. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/core/regions.py +0 -0
  35. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/cpu.py +0 -0
  36. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/disassembler.py +0 -0
  37. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/environment.py +0 -0
  38. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/exceptions.py +0 -0
  39. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/expr.py +0 -0
  40. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/__init__.py +0 -0
  41. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/cpus/__init__.py +0 -0
  42. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/cpus/cpu6502/__init__.py +0 -0
  43. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/cpus/cpu6502/cpu.py +0 -0
  44. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/cpus/cpu65c02/__init__.py +0 -0
  45. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/cpus/cpu65c02/cpu.py +0 -0
  46. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/__init__.py +0 -0
  47. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/_acorn_bbc_common.py +0 -0
  48. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_fdc_1770/__init__.py +0 -0
  49. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_fdc_1770/environment.py +0 -0
  50. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_fdc_8271/__init__.py +0 -0
  51. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_fdc_8271/environment.py +0 -0
  52. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_master_hardware/__init__.py +0 -0
  53. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_master_hardware/environment.py +0 -0
  54. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_model_b_hardware/__init__.py +0 -0
  55. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_model_b_hardware/environment.py +0 -0
  56. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_mos/__init__.py +0 -0
  57. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_mos/enums.py +0 -0
  58. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_mos/environment.py +0 -0
  59. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_mos/hooks.py +0 -0
  60. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_sideways_rom/__init__.py +0 -0
  61. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/acorn_sideways_rom/environment.py +0 -0
  62. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/bbc_basic_6502/__init__.py +0 -0
  63. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/bbc_basic_6502/environment.py +0 -0
  64. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/environments/bbc_basic_6502/floats.py +0 -0
  65. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/renderers/__init__.py +0 -0
  66. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/renderers/beebasm/__init__.py +0 -0
  67. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/renderers/json/__init__.py +0 -0
  68. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/renderers/json/renderer.py +0 -0
  69. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ext/renderers/tass64/__init__.py +0 -0
  70. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/extension.py +0 -0
  71. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/hooks.py +0 -0
  72. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/ir.py +0 -0
  73. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/output.py +0 -0
  74. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos/py.typed +0 -0
  75. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos.egg-info/dependency_links.txt +0 -0
  76. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos.egg-info/entry_points.txt +0 -0
  77. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos.egg-info/requires.txt +0 -0
  78. {dasmos-4.2.0 → dasmos-4.2.1}/src/dasmos.egg-info/top_level.txt +0 -0
  79. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_adfs_roundtrip.py +0 -0
  80. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_anfs_418_roundtrip.py +0 -0
  81. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_anfs_421_roundtrip.py +0 -0
  82. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_beebasm.py +0 -0
  83. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_cli.py +0 -0
  84. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_cpu.py +0 -0
  85. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_cpu6502.py +0 -0
  86. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_cpu65c02.py +0 -0
  87. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_disassembler.py +0 -0
  88. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_driver_roundtrip.py +0 -0
  89. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_econet_bridge_roundtrip.py +0 -0
  90. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_environment.py +0 -0
  91. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_expr.py +0 -0
  92. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_expr_operators.py +0 -0
  93. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_expr_parse.py +0 -0
  94. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_file_header.py +0 -0
  95. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_golden_asm.py +0 -0
  96. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_golden_json.py +0 -0
  97. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_include_binary.py +0 -0
  98. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_index_regions.py +0 -0
  99. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_json_renderer.py +0 -0
  100. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_macros.py +0 -0
  101. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_mnemonic_hash.py +0 -0
  102. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_nfs_334_roundtrip.py +0 -0
  103. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_nfs_roundtrip.py +0 -0
  104. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_output.py +0 -0
  105. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_program.py +0 -0
  106. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_py8dis2dasmos.py +0 -0
  107. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_reference_kinds.py +0 -0
  108. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_renderer.py +0 -0
  109. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_smoke.py +0 -0
  110. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_tass64.py +0 -0
  111. {dasmos-4.2.0 → dasmos-4.2.1}/tests/test_trace.py +0 -0
  112. {dasmos-4.2.0 → dasmos-4.2.1}/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.0
3
+ Version: 4.2.1
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.0`.
35
+ An extensible tracing disassembler for classic CPUs, version `4.2.1`.
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.0`.
5
+ An extensible tracing disassembler for classic CPUs, version `4.2.1`.
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.0"
111
+ current_version = "4.2.1"
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.0"
7
+ __version__ = "4.2.1"
8
8
 
9
9
  from dasmos.core.annotations import Align, Annotation, AnnotationStore, Banner, Comment
10
10
  from dasmos.core.format_hint import FormatHint
@@ -77,6 +77,21 @@ BYTE_COLUMN_TOTAL_WIDTH = 27
77
77
  # surrounding text.
78
78
  BANNER_SEPARATOR_WIDTH = 87
79
79
 
80
+ # Absolute addressing modes and the zero-page mode a size-optimising
81
+ # assembler (beebasm, 64tass) substitutes when the operand value is
82
+ # below ``&100`` *and* the mnemonic has that zero-page form. An
83
+ # instruction encoded in one of these modes with such an operand must
84
+ # be rendered so the assembler keeps the absolute encoding (#46).
85
+ _ZERO_PAGE_FORM_OF_ABSOLUTE_MODE = {
86
+ "ABSOLUTE": "ZERO_PAGE",
87
+ "ABSOLUTE_X": "ZERO_PAGE_X",
88
+ "ABSOLUTE_Y": "ZERO_PAGE_Y",
89
+ }
90
+ _ABSOLUTE_FORM_OF_ZERO_PAGE_MODE = {
91
+ zero_page: absolute
92
+ for absolute, zero_page in _ZERO_PAGE_FORM_OF_ABSOLUTE_MODE.items()
93
+ }
94
+
80
95
  if TYPE_CHECKING:
81
96
  from dasmos.ir import IntermediateRepresentation
82
97
 
@@ -163,6 +178,11 @@ class AssemblerRenderer(TextRenderer):
163
178
  # so). Drivers wanting per-expression control can fold selected
164
179
  # expressions themselves before registering them.
165
180
  self.fold_string_ops = fold_string_ops
181
+ # Initialise the per-render state so the public helpers that
182
+ # rely on it (e.g. :meth:`render_expression`) also work on a
183
+ # renderer that hasn't rendered anything yet; :meth:`render`
184
+ # resets it again at the start of every render.
185
+ self._reset_render_state()
166
186
 
167
187
  @property
168
188
  def emit_boundary_labels(self) -> bool:
@@ -259,6 +279,18 @@ class AssemblerRenderer(TextRenderer):
259
279
  # Macro names already warned about being inline-expanded, so the
260
280
  # warning fires once per macro rather than once per call site.
261
281
  self._macros_expansion_warned: set[str] = set()
282
+ # ``(operation, addressing-mode name)`` pairs the IR's CPU
283
+ # offers, built lazily on first use (see
284
+ # :meth:`_needs_forced_absolute`).
285
+ self._cpu_operation_modes: frozenset | None = None
286
+ # Explicit label name -> runtime address, so an expression that
287
+ # names a label (a ``Sym``) can mark it used (see
288
+ # :meth:`_note_symbol_use`). Filled in by :meth:`render`.
289
+ self._label_addr_by_name: dict[str, int] = {}
290
+ # While an instruction's operand is being rendered: the runtime
291
+ # addresses of the labels it names (see :meth:`_render_opcode`);
292
+ # ``None`` otherwise.
293
+ self._operand_label_refs: set[int] | None = None
262
294
 
263
295
  def render(self, ir: "IntermediateRepresentation") -> TextOutput:
264
296
  """Walk the IR's classifications and emit a beebasm source
@@ -304,6 +336,7 @@ class AssemblerRenderer(TextRenderer):
304
336
  """
305
337
  self._reset_render_state()
306
338
  self._comment_label_resolver = self._build_comment_label_resolver(ir)
339
+ self._label_addr_by_name = self._build_label_addr_by_name(ir)
307
340
 
308
341
  lines: list[str] = []
309
342
  lines.extend(self.disassembly_start())
@@ -859,6 +892,34 @@ class AssemblerRenderer(TextRenderer):
859
892
  name_to_addr.setdefault(name, addr)
860
893
  return name_to_addr.get
861
894
 
895
+ def _build_label_addr_by_name(self, ir) -> dict[str, int]:
896
+ """Map each explicit label name to its runtime address (first
897
+ registration wins), for :meth:`_note_symbol_use`."""
898
+ addr_by_name: dict[str, int] = {}
899
+ for runtime_addr_obj, label in ir.labels.items():
900
+ for name in label.explicit_names_in_insertion_order():
901
+ addr_by_name.setdefault(name, int(runtime_addr_obj))
902
+ return addr_by_name
903
+
904
+ def _note_symbol_use(self, ir, name: str) -> None:
905
+ """Record that an emitted expression names the label ``name``.
906
+
907
+ An expression can refer to a label by *name* (a ``Sym`` — e.g.
908
+ the ``fscv+1`` alias the ``acorn_mos`` environment registers for
909
+ ``&021F``, or a driver's ``d.expr(addr, "ptr+1")``) rather than by
910
+ address. That is still a use of the label: an optional label
911
+ outside the loaded range must then get its ``name = &xxxx``
912
+ definition, exactly as an operand that resolves to it by address
913
+ does (#47). A name dasmos doesn't know is left to the driver.
914
+ """
915
+ addr = self._label_addr_by_name.get(name)
916
+ if addr is None:
917
+ return
918
+ if not self._label_address_is_in_range(ir, addr):
919
+ self._used_external_labels.add(addr)
920
+ if self._operand_label_refs is not None:
921
+ self._operand_label_refs.add(addr)
922
+
862
923
  def _build_explicit_label_table(self, ir) -> list[str]:
863
924
  """Return ``name = &xxxx`` definition lines for out-of-range
864
925
  labels:
@@ -1488,15 +1549,210 @@ class AssemblerRenderer(TextRenderer):
1488
1549
  self, ir, binary_addr, opcode: Opcode, *, active_move=None,
1489
1550
  ) -> str:
1490
1551
  """Format a single instruction line."""
1552
+ if self._needs_forced_absolute(ir, binary_addr, opcode):
1553
+ return self._render_pinned_size(
1554
+ ir, binary_addr, opcode, absolute=True, active_move=active_move,
1555
+ )
1491
1556
  mnemonic = self._apply_case(opcode.default_mnemonic())
1492
- operand = self._render_operand(
1493
- ir, binary_addr, opcode, active_move=active_move,
1557
+ # Collect the labels the operand names, to spot a forward
1558
+ # reference whose size the assembler would have to guess.
1559
+ self._operand_label_refs = set()
1560
+ try:
1561
+ operand = self._render_operand(
1562
+ ir, binary_addr, opcode, active_move=active_move,
1563
+ )
1564
+ referenced = self._operand_label_refs
1565
+ finally:
1566
+ self._operand_label_refs = None
1567
+ pin_absolute = self._forward_reference_pin(
1568
+ ir, binary_addr, opcode, referenced, active_move=active_move,
1494
1569
  )
1570
+ if pin_absolute is not None:
1571
+ return self._render_pinned_size(
1572
+ ir, binary_addr, opcode,
1573
+ absolute=pin_absolute, active_move=active_move,
1574
+ )
1495
1575
  char_hint = self._immediate_char_hint(ir, binary_addr, opcode)
1496
1576
  if operand:
1497
1577
  return f" {mnemonic} {operand}{char_hint}"
1498
1578
  return f" {mnemonic}"
1499
1579
 
1580
+ def _cpu_has_mode(self, ir, operation, mode_name: str) -> bool:
1581
+ """True iff the IR's CPU offers ``operation`` in the addressing
1582
+ mode named ``mode_name``."""
1583
+ if self._cpu_operation_modes is None:
1584
+ self._cpu_operation_modes = frozenset(
1585
+ (op.operation, op.addressing_mode.name)
1586
+ for op in ir.cpu.opcodes().values()
1587
+ )
1588
+ return (operation, mode_name) in self._cpu_operation_modes
1589
+
1590
+ def _needs_forced_absolute(self, ir, binary_addr, opcode: Opcode) -> bool:
1591
+ """True when the instruction is encoded with absolute addressing
1592
+ but a size-optimising assembler would reassemble it to the
1593
+ shorter zero-page form.
1594
+
1595
+ That happens exactly when the operand value is below ``&100``
1596
+ *and* the mnemonic has the matching zero-page mode — the rule
1597
+ beebasm and 64tass both apply. ``LDA abs,Y`` with a zero-page
1598
+ operand is therefore safe (LDA has no ``zp,Y``), while
1599
+ ``LDX abs,Y`` is not (#46).
1600
+ """
1601
+ zero_page_mode = _ZERO_PAGE_FORM_OF_ABSOLUTE_MODE.get(
1602
+ opcode.addressing_mode.name,
1603
+ )
1604
+ if zero_page_mode is None:
1605
+ return False
1606
+ if ir.memory.get_u16_le(int(binary_addr) + 1) >= 0x100:
1607
+ return False
1608
+ return self._cpu_has_mode(ir, opcode.operation, zero_page_mode)
1609
+
1610
+ def _forward_reference_pin(
1611
+ self, ir, binary_addr, opcode: Opcode, referenced, *, active_move=None,
1612
+ ) -> bool | None:
1613
+ """Decide whether an instruction's operand size must be pinned
1614
+ because its operand forward-references a label (#48).
1615
+
1616
+ When a mnemonic has both a zero-page and an absolute form, the
1617
+ assembler can only choose between them once it knows the
1618
+ operand's value. For a label defined later in the listing it
1619
+ must guess on its first pass, and a wrong guess can settle into
1620
+ a self-consistent but wrong layout: the guessed size moves the
1621
+ label to the matching side of ``&100``. So the size is pinned
1622
+ when the operand names a label defined later *and* this
1623
+ assembler's first-pass guess
1624
+ (:meth:`forward_reference_assumes_zero_page`) disagrees with
1625
+ the encoded size. Where the guess already matches, the
1626
+ instruction renders normally.
1627
+
1628
+ Returns ``True`` to pin the absolute form, ``False`` to pin the
1629
+ zero-page form, or ``None`` when no pinning is needed.
1630
+ """
1631
+ mode_name = opcode.addressing_mode.name
1632
+ if mode_name in _ZERO_PAGE_FORM_OF_ABSOLUTE_MODE:
1633
+ encoded_zero_page = False
1634
+ alternative = _ZERO_PAGE_FORM_OF_ABSOLUTE_MODE[mode_name]
1635
+ elif mode_name in _ABSOLUTE_FORM_OF_ZERO_PAGE_MODE:
1636
+ encoded_zero_page = True
1637
+ alternative = _ABSOLUTE_FORM_OF_ZERO_PAGE_MODE[mode_name]
1638
+ else:
1639
+ return None
1640
+ if not self._cpu_has_mode(ir, opcode.operation, alternative):
1641
+ return None
1642
+ if not any(self._label_defined_later(ir, addr) for addr in referenced):
1643
+ return None
1644
+ if active_move is not None:
1645
+ pc = int(active_move.dest_runtime_addr) + (
1646
+ int(binary_addr) - int(active_move.src_binary_addr)
1647
+ )
1648
+ else:
1649
+ pc = int(ir.moves.b2r(BinaryAddr(int(binary_addr))))
1650
+ if self.forward_reference_assumes_zero_page(pc) == encoded_zero_page:
1651
+ return None
1652
+ return not encoded_zero_page
1653
+
1654
+ def _label_defined_later(self, ir, runtime_addr: int) -> bool:
1655
+ """True iff the label at ``runtime_addr`` is defined further on
1656
+ in the listing than the line being rendered.
1657
+
1658
+ Labels outside the loaded range are equates at the top of the
1659
+ listing, and in-range labels with no inline anchor go into the
1660
+ same table, so only an inline anchor (or an inline
1661
+ ``name = base+offset``) not yet emitted is still to come.
1662
+ """
1663
+ if runtime_addr in self._inline_emitted_runtime_addrs:
1664
+ return False
1665
+ if not self._label_address_is_in_range(ir, runtime_addr):
1666
+ return False
1667
+ return (
1668
+ self._label_address_is_classification_start(ir, runtime_addr)
1669
+ or self._label_has_inline_offset_base(ir, runtime_addr)
1670
+ )
1671
+
1672
+ def forward_reference_assumes_zero_page(self, pc: int) -> bool:
1673
+ """Whether this assembler's first pass assumes zero-page
1674
+ addressing for an instruction at ``pc`` whose operand is a
1675
+ not-yet-defined symbol.
1676
+
1677
+ Default ``False``: an undefined symbol is assumed to be 16-bit,
1678
+ so the absolute form is chosen (64tass's behaviour). Backends
1679
+ whose assembler guesses otherwise override this; beebasm
1680
+ substitutes the instruction's own address, so it guesses zero
1681
+ page exactly when ``pc`` is below ``&100``.
1682
+ """
1683
+ return False
1684
+
1685
+ def _render_pinned_size(
1686
+ self, ir, binary_addr, opcode: Opcode, *, absolute: bool,
1687
+ active_move=None,
1688
+ ) -> str:
1689
+ """Render an instruction so it reassembles to its encoded size —
1690
+ absolute (three bytes) or zero page (two bytes) — whatever the
1691
+ assembler would otherwise choose.
1692
+
1693
+ The backend's :meth:`force_abs_instruction` /
1694
+ :meth:`force_zp_instruction` supply its operand-size syntax
1695
+ where it has one (64tass ``@w`` / ``@b``). Otherwise the
1696
+ instruction is emitted as data via
1697
+ :meth:`forced_absolute_as_data` /
1698
+ :meth:`forced_zero_page_as_data`, with the instruction kept
1699
+ readable in a trailing comment.
1700
+ """
1701
+ mnemonic = self._apply_case(opcode.default_mnemonic())
1702
+ symbol = self._resolve_operand_symbol(
1703
+ ir, binary_addr, opcode, int(binary_addr) + 1,
1704
+ active_move=active_move,
1705
+ )
1706
+ mode_name = opcode.addressing_mode.name
1707
+ if mode_name.endswith("_X"):
1708
+ suffix = f",{self._apply_case('X')}"
1709
+ elif mode_name.endswith("_Y"):
1710
+ suffix = f",{self._apply_case('Y')}"
1711
+ else:
1712
+ suffix = ""
1713
+ if absolute:
1714
+ forced = self.force_abs_instruction(mnemonic, "", symbol, suffix)
1715
+ else:
1716
+ forced = self.force_zp_instruction(mnemonic, "", symbol, suffix)
1717
+ if forced is not None:
1718
+ return f" {forced}"
1719
+ opcode_byte = ir.memory.get_u8(binary_addr)
1720
+ if absolute:
1721
+ data = self.forced_absolute_as_data(opcode_byte, symbol)
1722
+ size = "absolute"
1723
+ else:
1724
+ data = self.forced_zero_page_as_data(opcode_byte, symbol)
1725
+ size = "zero page"
1726
+ return (
1727
+ f" {data} {self.comment_prefix()} "
1728
+ f"{mnemonic} {symbol}{suffix} ({size})"
1729
+ )
1730
+
1731
+ def forced_absolute_as_data(self, opcode_byte: int, operand: str) -> str:
1732
+ """An absolute-mode instruction spelled as data: its opcode byte
1733
+ then its 16-bit operand, as one source line.
1734
+
1735
+ Used when the assembler has no syntax to force absolute
1736
+ addressing (:meth:`force_abs_instruction` returned ``None``).
1737
+ The default joins a byte and a word directive with ``:``, the
1738
+ statement separator beebasm (the backend that needs this)
1739
+ accepts. Override for an assembler that separates statements
1740
+ differently.
1741
+ """
1742
+ return (
1743
+ f"{self.byte_prefix()}{self.hex2(opcode_byte)} : "
1744
+ f"{self.word_prefix()}{operand}"
1745
+ )
1746
+
1747
+ def forced_zero_page_as_data(self, opcode_byte: int, operand: str) -> str:
1748
+ """A zero-page instruction spelled as data: its opcode byte then
1749
+ its 8-bit operand, as one byte directive.
1750
+
1751
+ Used when the assembler has no syntax to force zero-page
1752
+ addressing (:meth:`force_zp_instruction` returned ``None``).
1753
+ """
1754
+ return f"{self.byte_prefix()}{self.hex2(opcode_byte)}, {operand}"
1755
+
1500
1756
  def _apply_case(self, text: str) -> str:
1501
1757
  """Apply the renderer's ``lower_case`` setting to ``text``.
1502
1758
 
@@ -1834,6 +2090,8 @@ class AssemblerRenderer(TextRenderer):
1834
2090
  if names:
1835
2091
  if not self._label_address_is_in_range(ir, addr):
1836
2092
  self._used_external_labels.add(addr)
2093
+ if self._operand_label_refs is not None:
2094
+ self._operand_label_refs.add(addr)
1837
2095
  return names[0]
1838
2096
  # No explicit name — fall back to an expression. Drivers
1839
2097
  # use ``d.expr_label(addr, "<base>-1")`` to register a
@@ -2190,6 +2448,7 @@ class AssemblerRenderer(TextRenderer):
2190
2448
  if isinstance(e, Int):
2191
2449
  return self._render_int_node(e), self._ATOM_PRECEDENCE
2192
2450
  if isinstance(e, Sym):
2451
+ self._note_symbol_use(ir, e.name)
2193
2452
  return e.name, self._ATOM_PRECEDENCE
2194
2453
  if isinstance(e, Ref):
2195
2454
  return (
@@ -412,6 +412,12 @@ class BeebasmRenderer(AssemblerRenderer):
412
412
  def render_string_length(self, s: str) -> str:
413
413
  return f"LEN({s})"
414
414
 
415
+ def forward_reference_assumes_zero_page(self, pc: int) -> bool:
416
+ # On its first pass beebasm substitutes the current PC for an
417
+ # undefined symbol (assemble.cpp), so it guesses zero page
418
+ # exactly when the instruction itself sits below &100.
419
+ return pc < 0x100
420
+
415
421
  def included_binary_directive(self, path: str) -> list[str]:
416
422
  # beebasm includes an external file verbatim at assembly time
417
423
  # with ``incbin "<path>"``; the file must sit next to the
@@ -202,6 +202,20 @@ class Tass64Renderer(AssemblerRenderer):
202
202
  return chr(n)
203
203
  return None
204
204
 
205
+ def force_abs_instruction(
206
+ self, instruction: str, prefix: str, operand: str, suffix: str,
207
+ ) -> str | None:
208
+ # 64tass's ``@w`` operand-size prefix forces the 16-bit
209
+ # (absolute) encoding even when the value fits in zero page.
210
+ return f"{instruction} {prefix}@w {operand}{suffix}"
211
+
212
+ def force_zp_instruction(
213
+ self, instruction: str, prefix: str, operand: str, suffix: str,
214
+ ) -> str | None:
215
+ # ``@b`` forces the 8-bit (zero-page) encoding, so a forward
216
+ # reference can't settle on the absolute form (#48).
217
+ return f"{instruction} {prefix}@b {operand}{suffix}"
218
+
205
219
  def included_binary_directive(self, path: str) -> list[str]:
206
220
  # 64tass includes an external file with ``.binary "<path>"``
207
221
  # (raw bytes, no header). The file must sit next to the listing
@@ -235,11 +235,34 @@ class TextRenderer(Renderer[TextOutput]):
235
235
  def force_zp_instruction(
236
236
  self, instruction: str, prefix: str, operand: str, suffix: str,
237
237
  ) -> str | None:
238
+ """Instruction text that forces zero-page (8-bit) addressing, or
239
+ ``None`` when the assembler has no syntax for it.
240
+
241
+ Called for a zero-page instruction whose operand names a label
242
+ defined later in the listing, when the assembler's first pass
243
+ would otherwise guess the absolute form (see
244
+ ``AssemblerRenderer.forward_reference_assumes_zero_page``).
245
+ Arguments as for :meth:`force_abs_instruction`. 64tass returns
246
+ ``lda @b name``; the default ``None`` makes the renderer emit
247
+ the instruction as data instead.
248
+ """
238
249
  return None
239
250
 
240
251
  def force_abs_instruction(
241
252
  self, instruction: str, prefix: str, operand: str, suffix: str,
242
253
  ) -> str | None:
254
+ """Instruction text that forces absolute (16-bit) addressing, or
255
+ ``None`` when the assembler has no syntax for it.
256
+
257
+ Called for an instruction encoded with absolute addressing whose
258
+ operand is below ``&100`` and whose mnemonic also has a
259
+ zero-page form — the case a size-optimising assembler would
260
+ otherwise shrink to two bytes. ``instruction`` is the mnemonic,
261
+ ``operand`` the resolved symbol, ``prefix`` / ``suffix`` the
262
+ surrounding mode punctuation (``""`` / ``",x"`` / ``",y"``).
263
+ 64tass returns ``sta @w name``; the default ``None`` makes the
264
+ renderer emit the instruction as data instead.
265
+ """
243
266
  return None
244
267
 
245
268
  def force_zp_label_prefix(self) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dasmos
3
- Version: 4.2.0
3
+ Version: 4.2.1
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.0`.
35
+ An extensible tracing disassembler for classic CPUs, version `4.2.1`.
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>
@@ -71,6 +71,7 @@ src/dasmos/ext/renderers/json/__init__.py
71
71
  src/dasmos/ext/renderers/json/renderer.py
72
72
  src/dasmos/ext/renderers/tass64/__init__.py
73
73
  src/dasmos/ext/renderers/tass64/renderer.py
74
+ tests/test_absolute_operands.py
74
75
  tests/test_adfs_roundtrip.py
75
76
  tests/test_anfs_418_roundtrip.py
76
77
  tests/test_anfs_421_roundtrip.py
@@ -86,7 +87,9 @@ tests/test_environment.py
86
87
  tests/test_expr.py
87
88
  tests/test_expr_operators.py
88
89
  tests/test_expr_parse.py
90
+ tests/test_expression_symbols.py
89
91
  tests/test_file_header.py
92
+ tests/test_forward_operand_size.py
90
93
  tests/test_golden_asm.py
91
94
  tests/test_golden_json.py
92
95
  tests/test_include_binary.py