pyvacs 0.1.0__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.
- pyvacs/__init__.py +22 -0
- pyvacs/assembler.py +542 -0
- pyvacs/cli.py +108 -0
- pyvacs/disassembler.py +166 -0
- pyvacs/isa.py +341 -0
- pyvacs/lexer.py +394 -0
- pyvacs-0.1.0.dist-info/METADATA +352 -0
- pyvacs-0.1.0.dist-info/RECORD +12 -0
- pyvacs-0.1.0.dist-info/WHEEL +5 -0
- pyvacs-0.1.0.dist-info/entry_points.txt +3 -0
- pyvacs-0.1.0.dist-info/licenses/LICENSE +201 -0
- pyvacs-0.1.0.dist-info/top_level.txt +1 -0
pyvacs/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""pyvacs: a Python 2650 (Signetics/Philips) assembler and disassembler.
|
|
2
|
+
|
|
3
|
+
A clean-room Python reimplementation of the VACS 1.24 assembler for the
|
|
4
|
+
Signetics 2650 microprocessor family, plus a round-trip-safe disassembler.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .assembler import AssembledProgram, AssemblyError, Assembler, assemble
|
|
8
|
+
from .disassembler import Disassembler, Line, disassemble, to_source
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AssembledProgram",
|
|
14
|
+
"AssemblyError",
|
|
15
|
+
"Assembler",
|
|
16
|
+
"assemble",
|
|
17
|
+
"Disassembler",
|
|
18
|
+
"Line",
|
|
19
|
+
"disassemble",
|
|
20
|
+
"to_source",
|
|
21
|
+
"__version__",
|
|
22
|
+
]
|
pyvacs/assembler.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""Two-pass assembler for the Signetics 2650.
|
|
2
|
+
|
|
3
|
+
The assembler accepts VACS-style source: an optional label, a mnemonic or
|
|
4
|
+
directive, operands, and an optional ``;`` comment. Because every 2650
|
|
5
|
+
instruction has a fixed length, the first pass only needs to track the location
|
|
6
|
+
counter and record label addresses; the second pass evaluates expressions and
|
|
7
|
+
emits bytes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from . import isa
|
|
16
|
+
from .lexer import Token, evaluate, tokenize
|
|
17
|
+
|
|
18
|
+
DIRECTIVES = {
|
|
19
|
+
"ORG",
|
|
20
|
+
"EQU",
|
|
21
|
+
"SET",
|
|
22
|
+
"DB",
|
|
23
|
+
"BYTE",
|
|
24
|
+
"ASCII",
|
|
25
|
+
"DW",
|
|
26
|
+
"WORD",
|
|
27
|
+
"DD",
|
|
28
|
+
"DS",
|
|
29
|
+
"DBX",
|
|
30
|
+
"DBFILL",
|
|
31
|
+
"END",
|
|
32
|
+
"NAME",
|
|
33
|
+
"TITLE",
|
|
34
|
+
"PAGE",
|
|
35
|
+
"PAGELEN",
|
|
36
|
+
"PGLEN",
|
|
37
|
+
"WIDTH",
|
|
38
|
+
"LMARG",
|
|
39
|
+
"LEFTMARG",
|
|
40
|
+
"TABS",
|
|
41
|
+
"OFFS",
|
|
42
|
+
"LIST",
|
|
43
|
+
"NOLIST",
|
|
44
|
+
"NOLST",
|
|
45
|
+
"EJECT",
|
|
46
|
+
"SBTTL",
|
|
47
|
+
"STITLE",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AssemblyError(Exception):
|
|
52
|
+
"""Raised on a source error, with the offending line number attached."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, message: str, lineno: int) -> None:
|
|
55
|
+
super().__init__(f"line {lineno}: {message}")
|
|
56
|
+
self.lineno = lineno
|
|
57
|
+
self.raw_message = message
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class Segment:
|
|
62
|
+
"""A contiguous run of emitted bytes starting at ``start``."""
|
|
63
|
+
|
|
64
|
+
start: int
|
|
65
|
+
data: bytearray = field(default_factory=bytearray)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class AssembledProgram:
|
|
70
|
+
"""Result of assembling a source file."""
|
|
71
|
+
|
|
72
|
+
segments: List[Segment]
|
|
73
|
+
symbols: Dict[str, int]
|
|
74
|
+
start_address: int
|
|
75
|
+
|
|
76
|
+
def to_bytes(self, fill: int = 0x00) -> bytes:
|
|
77
|
+
"""Flatten all segments into one binary, padding gaps with ``fill``."""
|
|
78
|
+
if not self.segments:
|
|
79
|
+
return b""
|
|
80
|
+
lowest = min(seg.start for seg in self.segments)
|
|
81
|
+
highest = max(seg.start + len(seg.data) for seg in self.segments)
|
|
82
|
+
out = bytearray([fill & 0xFF]) * (highest - lowest)
|
|
83
|
+
for seg in self.segments:
|
|
84
|
+
offset = seg.start - lowest
|
|
85
|
+
out[offset : offset + len(seg.data)] = seg.data
|
|
86
|
+
return bytes(out)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def origin(self) -> int:
|
|
90
|
+
"""Lowest address emitted (0 when nothing was emitted)."""
|
|
91
|
+
if not self.segments:
|
|
92
|
+
return 0
|
|
93
|
+
return min(seg.start for seg in self.segments)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Assembler:
|
|
97
|
+
"""Assemble 2650 source text into bytes."""
|
|
98
|
+
|
|
99
|
+
def __init__(self) -> None:
|
|
100
|
+
self.symbols: Dict[str, int] = {}
|
|
101
|
+
self.location = 0
|
|
102
|
+
self.start_address = 0
|
|
103
|
+
self._pass = 1
|
|
104
|
+
self._segments: List[Segment] = []
|
|
105
|
+
self._current: Optional[Segment] = None
|
|
106
|
+
self._lineno = 0
|
|
107
|
+
for name, value in isa.CONDITION_VALUES.items():
|
|
108
|
+
self.symbols[name.upper()] = value
|
|
109
|
+
|
|
110
|
+
# -- public API -------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def assemble(self, source: str) -> AssembledProgram:
|
|
113
|
+
"""Assemble ``source`` and return the resulting program."""
|
|
114
|
+
lines = source.splitlines()
|
|
115
|
+
self._run_pass(lines, first=True)
|
|
116
|
+
self._run_pass(lines, first=False)
|
|
117
|
+
return AssembledProgram(
|
|
118
|
+
segments=self._segments,
|
|
119
|
+
symbols=dict(self.symbols),
|
|
120
|
+
start_address=self.start_address,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# -- pass driver ------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def _run_pass(self, lines: List[str], first: bool) -> None:
|
|
126
|
+
self._pass = 1 if first else 2
|
|
127
|
+
self.location = 0
|
|
128
|
+
self.start_address = 0
|
|
129
|
+
self._segments = []
|
|
130
|
+
self._current = None
|
|
131
|
+
for index, line in enumerate(lines, start=1):
|
|
132
|
+
self._lineno = index
|
|
133
|
+
tokens = self._safe_tokenize(line)
|
|
134
|
+
if tokens is None:
|
|
135
|
+
continue
|
|
136
|
+
column0 = bool(line) and not line[0].isspace()
|
|
137
|
+
if self._handle_line(tokens, column0):
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
def _safe_tokenize(self, line: str) -> Optional[List[Token]]:
|
|
141
|
+
try:
|
|
142
|
+
return tokenize(line)
|
|
143
|
+
except ValueError as exc:
|
|
144
|
+
raise AssemblyError(str(exc), self._lineno) from exc
|
|
145
|
+
|
|
146
|
+
# -- line handling ----------------------------------------------------
|
|
147
|
+
|
|
148
|
+
def _handle_line(self, tokens: List[Token], column0: bool) -> bool:
|
|
149
|
+
"""Process one tokenised line. Returns True at END."""
|
|
150
|
+
pos = self._consume_label(tokens, column0)
|
|
151
|
+
tok = tokens[pos]
|
|
152
|
+
if tok.kind == "EOL":
|
|
153
|
+
return False
|
|
154
|
+
if tok.kind != "IDENT":
|
|
155
|
+
raise AssemblyError(f"unexpected token {tok.text!r}", self._lineno)
|
|
156
|
+
name = str(tok.value)
|
|
157
|
+
if name == "END":
|
|
158
|
+
self._directive_end(tokens, pos + 1)
|
|
159
|
+
return True
|
|
160
|
+
if name in DIRECTIVES:
|
|
161
|
+
self._handle_directive(name, tokens, pos + 1)
|
|
162
|
+
return False
|
|
163
|
+
if name in isa.INSTRUCTIONS:
|
|
164
|
+
self._assemble_instruction(name, tokens, pos + 1)
|
|
165
|
+
return False
|
|
166
|
+
raise AssemblyError(f"unknown opcode {tok.text!r}", self._lineno)
|
|
167
|
+
|
|
168
|
+
def _consume_label(self, tokens: List[Token], column0: bool) -> int:
|
|
169
|
+
"""Define a leading label if present; return the next token index.
|
|
170
|
+
|
|
171
|
+
``column0`` is True when the line had no leading whitespace, which (per
|
|
172
|
+
VACS convention) is what lets a bare identifier with no opcode be a
|
|
173
|
+
label rather than a mistyped instruction.
|
|
174
|
+
"""
|
|
175
|
+
first = tokens[0]
|
|
176
|
+
if first.kind != "IDENT":
|
|
177
|
+
return 0
|
|
178
|
+
nxt = tokens[1]
|
|
179
|
+
if nxt.kind == "OP" and nxt.text == ":":
|
|
180
|
+
self._define_label(str(first.value))
|
|
181
|
+
return 2
|
|
182
|
+
if str(first.value) in isa.INSTRUCTIONS or str(first.value) in DIRECTIVES:
|
|
183
|
+
return 0 # the leading word is itself an opcode/directive
|
|
184
|
+
if nxt.kind == "IDENT" and nxt.value in ("EQU", "SET"):
|
|
185
|
+
self._define_label(str(first.value))
|
|
186
|
+
return 1 # the EQU/SET directive sits at index 1
|
|
187
|
+
if nxt.kind == "EOL" and not column0:
|
|
188
|
+
return 0 # an indented lone word is an (unknown) opcode, not a label
|
|
189
|
+
self._define_label(str(first.value))
|
|
190
|
+
return 1
|
|
191
|
+
|
|
192
|
+
def _define_label(self, name: str) -> None:
|
|
193
|
+
self._set_symbol(name, self.location)
|
|
194
|
+
|
|
195
|
+
def _set_symbol(self, name: str, value: int) -> None:
|
|
196
|
+
key = name.upper()
|
|
197
|
+
if self._pass == 1 and key in self.symbols and key not in _PREDEFINED:
|
|
198
|
+
raise AssemblyError(f"duplicate symbol {name!r}", self._lineno)
|
|
199
|
+
self.symbols[key] = value
|
|
200
|
+
|
|
201
|
+
# -- expression helpers ----------------------------------------------
|
|
202
|
+
|
|
203
|
+
def _resolve(self, name: str) -> Optional[int]:
|
|
204
|
+
return self.symbols.get(name.upper())
|
|
205
|
+
|
|
206
|
+
def _eval(self, tokens: List[Token], pos: int) -> Tuple[int, int]:
|
|
207
|
+
try:
|
|
208
|
+
value, defined, new_pos = evaluate(
|
|
209
|
+
tokens, pos, self._resolve, self.location
|
|
210
|
+
)
|
|
211
|
+
except ValueError as exc:
|
|
212
|
+
raise AssemblyError(str(exc), self._lineno) from exc
|
|
213
|
+
if self._pass == 2 and not defined:
|
|
214
|
+
raise AssemblyError("undefined symbol in expression", self._lineno)
|
|
215
|
+
return value, new_pos
|
|
216
|
+
|
|
217
|
+
# -- byte emission ----------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def _emit(self, data: bytes) -> None:
|
|
220
|
+
if self._pass == 2:
|
|
221
|
+
if self._current is None or self.location != (
|
|
222
|
+
self._current.start + len(self._current.data)
|
|
223
|
+
):
|
|
224
|
+
self._current = Segment(self.location)
|
|
225
|
+
self._segments.append(self._current)
|
|
226
|
+
self._current.data.extend(data)
|
|
227
|
+
self.location += len(data)
|
|
228
|
+
|
|
229
|
+
# -- directives -------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
def _handle_directive(self, name: str, tokens: List[Token], pos: int) -> None:
|
|
232
|
+
handler = {
|
|
233
|
+
"ORG": self._directive_org,
|
|
234
|
+
"EQU": self._directive_equ,
|
|
235
|
+
"SET": self._directive_equ,
|
|
236
|
+
"DB": self._directive_db,
|
|
237
|
+
"BYTE": self._directive_db,
|
|
238
|
+
"ASCII": self._directive_db,
|
|
239
|
+
"DW": self._directive_dw,
|
|
240
|
+
"WORD": self._directive_dw,
|
|
241
|
+
"DD": self._directive_dd,
|
|
242
|
+
"DS": self._directive_ds,
|
|
243
|
+
"DBX": self._directive_dbx,
|
|
244
|
+
"DBFILL": self._directive_dbfill,
|
|
245
|
+
}.get(name)
|
|
246
|
+
if handler is None:
|
|
247
|
+
return # listing-only directive: ignore the rest of the line
|
|
248
|
+
handler(tokens, pos)
|
|
249
|
+
|
|
250
|
+
def _directive_org(self, tokens: List[Token], pos: int) -> None:
|
|
251
|
+
value, _ = self._eval(tokens, pos)
|
|
252
|
+
self.location = value
|
|
253
|
+
self._current = None
|
|
254
|
+
|
|
255
|
+
def _directive_equ(self, tokens: List[Token], pos: int) -> None:
|
|
256
|
+
# The label was not consumed yet; it sits at tokens[0].
|
|
257
|
+
if tokens[0].kind != "IDENT":
|
|
258
|
+
raise AssemblyError("EQU needs a label", self._lineno)
|
|
259
|
+
name = str(tokens[0].value)
|
|
260
|
+
value, _ = self._eval(tokens, pos)
|
|
261
|
+
self.symbols[name.upper()] = value
|
|
262
|
+
|
|
263
|
+
def _directive_db(self, tokens: List[Token], pos: int) -> None:
|
|
264
|
+
for item, next_pos in self._iter_items(tokens, pos):
|
|
265
|
+
if item.kind == "STRING":
|
|
266
|
+
self._emit(bytes(ord(ch) & 0xFF for ch in str(item.value)))
|
|
267
|
+
pos = next_pos
|
|
268
|
+
else:
|
|
269
|
+
value, pos = self._eval(tokens, pos)
|
|
270
|
+
self._check_range(value, -128, 255, "byte")
|
|
271
|
+
self._emit(bytes([value & 0xFF]))
|
|
272
|
+
pos = self._skip_comma(tokens, pos)
|
|
273
|
+
|
|
274
|
+
def _directive_dw(self, tokens: List[Token], pos: int) -> None:
|
|
275
|
+
pos = self._emit_words(tokens, pos, width=2, low=-32768, high=65535)
|
|
276
|
+
|
|
277
|
+
def _directive_dd(self, tokens: List[Token], pos: int) -> None:
|
|
278
|
+
pos = self._emit_words(tokens, pos, width=4, low=None, high=None)
|
|
279
|
+
|
|
280
|
+
def _emit_words(
|
|
281
|
+
self,
|
|
282
|
+
tokens: List[Token],
|
|
283
|
+
pos: int,
|
|
284
|
+
width: int,
|
|
285
|
+
low: Optional[int],
|
|
286
|
+
high: Optional[int],
|
|
287
|
+
) -> int:
|
|
288
|
+
while True:
|
|
289
|
+
value, pos = self._eval(tokens, pos)
|
|
290
|
+
if low is not None and high is not None:
|
|
291
|
+
self._check_range(value, low, high, "word")
|
|
292
|
+
self._emit((value & ((1 << (8 * width)) - 1)).to_bytes(width, "big"))
|
|
293
|
+
pos = self._skip_comma(tokens, pos)
|
|
294
|
+
if tokens[pos].kind == "EOL":
|
|
295
|
+
return pos
|
|
296
|
+
|
|
297
|
+
def _directive_ds(self, tokens: List[Token], pos: int) -> None:
|
|
298
|
+
count, pos = self._eval(tokens, pos)
|
|
299
|
+
self._check_range(count, 0, 32767, "storage size")
|
|
300
|
+
fill = 0
|
|
301
|
+
if tokens[pos].kind == "OP" and tokens[pos].text == ",":
|
|
302
|
+
fill, pos = self._eval(tokens, pos + 1)
|
|
303
|
+
self._emit(bytes([fill & 0xFF]) * count)
|
|
304
|
+
|
|
305
|
+
def _directive_dbfill(self, tokens: List[Token], pos: int) -> None:
|
|
306
|
+
count, pos = self._eval(tokens, pos)
|
|
307
|
+
self._check_range(count, 0, 32767, "fill count")
|
|
308
|
+
fill = 0
|
|
309
|
+
if tokens[pos].kind == "OP" and tokens[pos].text == ",":
|
|
310
|
+
fill, pos = self._eval(tokens, pos + 1)
|
|
311
|
+
self._emit(bytes([fill & 0xFF]) * count)
|
|
312
|
+
|
|
313
|
+
def _directive_dbx(self, tokens: List[Token], pos: int) -> None:
|
|
314
|
+
tok = tokens[pos]
|
|
315
|
+
if tok.kind != "STRING":
|
|
316
|
+
raise AssemblyError("DBX needs a quoted pattern", self._lineno)
|
|
317
|
+
pattern = str(tok.value)
|
|
318
|
+
if len(pattern) != 8:
|
|
319
|
+
raise AssemblyError("DBX needs 8 pattern characters", self._lineno)
|
|
320
|
+
value = 0
|
|
321
|
+
for index, ch in enumerate(pattern):
|
|
322
|
+
if ch not in (" ", "."):
|
|
323
|
+
value |= 1 << (7 - index)
|
|
324
|
+
self._emit(bytes([value]))
|
|
325
|
+
|
|
326
|
+
def _directive_end(self, tokens: List[Token], pos: int) -> None:
|
|
327
|
+
if tokens[pos].kind != "EOL":
|
|
328
|
+
value, _ = self._eval(tokens, pos)
|
|
329
|
+
self.start_address = value
|
|
330
|
+
|
|
331
|
+
# -- item iteration helpers ------------------------------------------
|
|
332
|
+
|
|
333
|
+
def _iter_items(self, tokens: List[Token], pos: int):
|
|
334
|
+
"""Yield ``(token, next_pos)`` for each comma-separated DB item."""
|
|
335
|
+
while tokens[pos].kind != "EOL":
|
|
336
|
+
yield tokens[pos], pos + 1
|
|
337
|
+
if tokens[pos].kind == "STRING":
|
|
338
|
+
pos += 1
|
|
339
|
+
else:
|
|
340
|
+
_, pos = self._eval(tokens, pos)
|
|
341
|
+
pos = self._skip_comma(tokens, pos)
|
|
342
|
+
|
|
343
|
+
@staticmethod
|
|
344
|
+
def _skip_comma(tokens: List[Token], pos: int) -> int:
|
|
345
|
+
if tokens[pos].kind == "OP" and tokens[pos].text == ",":
|
|
346
|
+
return pos + 1
|
|
347
|
+
return pos
|
|
348
|
+
|
|
349
|
+
def _check_range(self, value: int, low: int, high: int, what: str) -> None:
|
|
350
|
+
if not low <= value <= high:
|
|
351
|
+
raise AssemblyError(
|
|
352
|
+
f"{what} value {value} out of range {low}..{high}", self._lineno
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# -- instruction assembly --------------------------------------------
|
|
356
|
+
|
|
357
|
+
def _assemble_instruction(
|
|
358
|
+
self, mnemonic: str, tokens: List[Token], pos: int
|
|
359
|
+
) -> None:
|
|
360
|
+
mode = isa.INSTRUCTIONS[mnemonic][1]
|
|
361
|
+
address = self.location
|
|
362
|
+
ops, pos = self._parse_operands(mode, tokens, pos)
|
|
363
|
+
self._expect_eol(tokens, pos)
|
|
364
|
+
if self._pass == 1:
|
|
365
|
+
# Pass one only needs the (fixed) instruction length; operand values
|
|
366
|
+
# may still reference undefined forward symbols, so we must not run
|
|
367
|
+
# the encoder's range checks against placeholder zeros here.
|
|
368
|
+
self.location += isa.MODE_LENGTHS[mode]
|
|
369
|
+
return
|
|
370
|
+
try:
|
|
371
|
+
data = isa.encode(mnemonic, ops, address)
|
|
372
|
+
except isa.EncodingError as exc:
|
|
373
|
+
raise AssemblyError(str(exc), self._lineno) from exc
|
|
374
|
+
self._emit(data)
|
|
375
|
+
|
|
376
|
+
def _parse_operands(
|
|
377
|
+
self, mode: str, tokens: List[Token], pos: int
|
|
378
|
+
) -> Tuple[Dict[str, int], int]:
|
|
379
|
+
parser = {
|
|
380
|
+
"inh": self._parse_inh,
|
|
381
|
+
"reg": self._parse_reg,
|
|
382
|
+
"reg13": self._parse_reg,
|
|
383
|
+
"regz": self._parse_reg,
|
|
384
|
+
"creg": self._parse_creg,
|
|
385
|
+
"ccond": self._parse_ccond,
|
|
386
|
+
"imm": self._parse_imm,
|
|
387
|
+
"regimm": self._parse_regimm,
|
|
388
|
+
"zbr": self._parse_zbr,
|
|
389
|
+
"regrel": self._parse_regaddr,
|
|
390
|
+
"crel": self._parse_caddr,
|
|
391
|
+
"crel3": self._parse_caddr,
|
|
392
|
+
"regabs": self._parse_regaddr,
|
|
393
|
+
"cabs": self._parse_caddr,
|
|
394
|
+
"cabs3": self._parse_caddr,
|
|
395
|
+
"idxabs": self._parse_idxabs,
|
|
396
|
+
"absx": self._parse_absx,
|
|
397
|
+
}[mode]
|
|
398
|
+
return parser(tokens, pos)
|
|
399
|
+
|
|
400
|
+
def _parse_inh(self, _tokens: List[Token], pos: int) -> Tuple[Dict[str, int], int]:
|
|
401
|
+
return {}, pos
|
|
402
|
+
|
|
403
|
+
def _parse_reg(self, tokens: List[Token], pos: int) -> Tuple[Dict[str, int], int]:
|
|
404
|
+
reg, pos = self._read_register(tokens, pos)
|
|
405
|
+
return {"reg": reg}, pos
|
|
406
|
+
|
|
407
|
+
def _parse_creg(self, tokens: List[Token], pos: int) -> Tuple[Dict[str, int], int]:
|
|
408
|
+
pos = self._expect_comma(tokens, pos)
|
|
409
|
+
reg, pos = self._read_register(tokens, pos)
|
|
410
|
+
return {"reg": reg}, pos
|
|
411
|
+
|
|
412
|
+
def _parse_ccond(self, tokens: List[Token], pos: int) -> Tuple[Dict[str, int], int]:
|
|
413
|
+
pos = self._expect_comma(tokens, pos)
|
|
414
|
+
cond, pos = self._read_condition(tokens, pos)
|
|
415
|
+
return {"cond": cond}, pos
|
|
416
|
+
|
|
417
|
+
def _parse_imm(self, tokens: List[Token], pos: int) -> Tuple[Dict[str, int], int]:
|
|
418
|
+
value, pos = self._eval(tokens, pos)
|
|
419
|
+
return {"value": value}, pos
|
|
420
|
+
|
|
421
|
+
def _parse_regimm(
|
|
422
|
+
self, tokens: List[Token], pos: int
|
|
423
|
+
) -> Tuple[Dict[str, int], int]:
|
|
424
|
+
pos = self._expect_comma(tokens, pos)
|
|
425
|
+
reg, pos = self._read_register(tokens, pos)
|
|
426
|
+
value, pos = self._eval(tokens, pos)
|
|
427
|
+
return {"reg": reg, "value": value}, pos
|
|
428
|
+
|
|
429
|
+
def _parse_zbr(self, tokens: List[Token], pos: int) -> Tuple[Dict[str, int], int]:
|
|
430
|
+
indirect, pos = self._read_indirect(tokens, pos)
|
|
431
|
+
target, pos = self._eval(tokens, pos)
|
|
432
|
+
return {"target": target, "indirect": indirect}, pos
|
|
433
|
+
|
|
434
|
+
def _parse_regaddr(
|
|
435
|
+
self, tokens: List[Token], pos: int
|
|
436
|
+
) -> Tuple[Dict[str, int], int]:
|
|
437
|
+
pos = self._expect_comma(tokens, pos)
|
|
438
|
+
reg, pos = self._read_register(tokens, pos)
|
|
439
|
+
indirect, pos = self._read_indirect(tokens, pos)
|
|
440
|
+
target, pos = self._eval(tokens, pos)
|
|
441
|
+
return {"reg": reg, "indirect": indirect, "target": target}, pos
|
|
442
|
+
|
|
443
|
+
def _parse_caddr(self, tokens: List[Token], pos: int) -> Tuple[Dict[str, int], int]:
|
|
444
|
+
pos = self._expect_comma(tokens, pos)
|
|
445
|
+
cond, pos = self._read_condition(tokens, pos)
|
|
446
|
+
indirect, pos = self._read_indirect(tokens, pos)
|
|
447
|
+
target, pos = self._eval(tokens, pos)
|
|
448
|
+
return {"cond": cond, "indirect": indirect, "target": target}, pos
|
|
449
|
+
|
|
450
|
+
def _parse_idxabs(
|
|
451
|
+
self, tokens: List[Token], pos: int
|
|
452
|
+
) -> Tuple[Dict[str, int], int]:
|
|
453
|
+
indirect, pos = self._read_indirect(tokens, pos)
|
|
454
|
+
target, pos = self._eval(tokens, pos)
|
|
455
|
+
# Optional ",3" index designator (implied anyway).
|
|
456
|
+
if tokens[pos].kind == "OP" and tokens[pos].text == ",":
|
|
457
|
+
_, pos = self._read_register_or_value(tokens, pos + 1)
|
|
458
|
+
return {"target": target, "indirect": indirect}, pos
|
|
459
|
+
|
|
460
|
+
def _parse_absx(self, tokens: List[Token], pos: int) -> Tuple[Dict[str, int], int]:
|
|
461
|
+
pos = self._expect_comma(tokens, pos)
|
|
462
|
+
reg, pos = self._read_register(tokens, pos)
|
|
463
|
+
indirect, pos = self._read_indirect(tokens, pos)
|
|
464
|
+
target, pos = self._eval(tokens, pos)
|
|
465
|
+
index_ctl = isa.INDEX_NONE
|
|
466
|
+
if tokens[pos].kind == "OP" and tokens[pos].text == ",":
|
|
467
|
+
pos += 1
|
|
468
|
+
if tokens[pos].kind == "REG":
|
|
469
|
+
reg = int(tokens[pos].value)
|
|
470
|
+
index_ctl = isa.INDEX_PLAIN
|
|
471
|
+
pos += 1
|
|
472
|
+
if tokens[pos].kind == "OP" and tokens[pos].text == ",":
|
|
473
|
+
pos += 1
|
|
474
|
+
if tokens[pos].kind == "OP" and tokens[pos].text in ("+", "-"):
|
|
475
|
+
index_ctl = (
|
|
476
|
+
isa.INDEX_AUTO_INC
|
|
477
|
+
if tokens[pos].text == "+"
|
|
478
|
+
else isa.INDEX_AUTO_DEC
|
|
479
|
+
)
|
|
480
|
+
pos += 1
|
|
481
|
+
return {
|
|
482
|
+
"reg": reg,
|
|
483
|
+
"indirect": indirect,
|
|
484
|
+
"target": target,
|
|
485
|
+
"index_ctl": index_ctl,
|
|
486
|
+
}, pos
|
|
487
|
+
|
|
488
|
+
# -- low-level operand readers ---------------------------------------
|
|
489
|
+
|
|
490
|
+
def _read_register(self, tokens: List[Token], pos: int) -> Tuple[int, int]:
|
|
491
|
+
tok = tokens[pos]
|
|
492
|
+
if tok.kind != "REG":
|
|
493
|
+
raise AssemblyError("register expected", self._lineno)
|
|
494
|
+
return int(tok.value), pos + 1
|
|
495
|
+
|
|
496
|
+
def _read_register_or_value(self, tokens: List[Token], pos: int) -> Tuple[int, int]:
|
|
497
|
+
if tokens[pos].kind == "REG":
|
|
498
|
+
return int(tokens[pos].value), pos + 1
|
|
499
|
+
return self._eval(tokens, pos)
|
|
500
|
+
|
|
501
|
+
def _read_condition(self, tokens: List[Token], pos: int) -> Tuple[int, int]:
|
|
502
|
+
tok = tokens[pos]
|
|
503
|
+
if tok.kind == "NUMBER":
|
|
504
|
+
value = int(tok.value)
|
|
505
|
+
elif tok.kind == "IDENT":
|
|
506
|
+
resolved = self._resolve(str(tok.value))
|
|
507
|
+
if resolved is None:
|
|
508
|
+
if self._pass == 2:
|
|
509
|
+
raise AssemblyError("undefined condition", self._lineno)
|
|
510
|
+
value = 0
|
|
511
|
+
else:
|
|
512
|
+
value = resolved
|
|
513
|
+
else:
|
|
514
|
+
raise AssemblyError("condition expected", self._lineno)
|
|
515
|
+
if self._pass == 2 and not 0 <= value <= 3:
|
|
516
|
+
raise AssemblyError("condition must be 0..3", self._lineno)
|
|
517
|
+
return value, pos + 1
|
|
518
|
+
|
|
519
|
+
def _read_indirect(self, tokens: List[Token], pos: int) -> Tuple[int, int]:
|
|
520
|
+
if tokens[pos].kind == "OP" and tokens[pos].text == "*":
|
|
521
|
+
return 1, pos + 1
|
|
522
|
+
return 0, pos
|
|
523
|
+
|
|
524
|
+
def _expect_comma(self, tokens: List[Token], pos: int) -> int:
|
|
525
|
+
if tokens[pos].kind == "OP" and tokens[pos].text == ",":
|
|
526
|
+
return pos + 1
|
|
527
|
+
raise AssemblyError("comma expected", self._lineno)
|
|
528
|
+
|
|
529
|
+
def _expect_eol(self, tokens: List[Token], pos: int) -> None:
|
|
530
|
+
if tokens[pos].kind != "EOL":
|
|
531
|
+
raise AssemblyError(
|
|
532
|
+
f"unexpected operand {tokens[pos].text!r}", self._lineno
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
# Symbols predefined by the assembler that may be shadowed by user EQUs.
|
|
537
|
+
_PREDEFINED = {name.upper() for name in isa.CONDITION_VALUES}
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def assemble(source: str) -> AssembledProgram:
|
|
541
|
+
"""Convenience wrapper: assemble ``source`` and return the program."""
|
|
542
|
+
return Assembler().assemble(source)
|
pyvacs/cli.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Command-line interface for pyvacs.
|
|
2
|
+
|
|
3
|
+
Provides two console entry points:
|
|
4
|
+
|
|
5
|
+
* ``pyvacs-asm`` - assemble 2650 source into a binary.
|
|
6
|
+
* ``pyvacs-dasm`` - disassemble a binary into 2650 source.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import sys
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .assembler import Assembler, AssemblyError
|
|
17
|
+
from .disassembler import Disassembler
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _read_text(path: str) -> str:
|
|
21
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
22
|
+
return handle.read()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read_bytes(path: str) -> bytes:
|
|
26
|
+
with open(path, "rb") as handle:
|
|
27
|
+
return handle.read()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def assemble_main(argv: Optional[List[str]] = None) -> int:
|
|
31
|
+
"""Entry point for ``pyvacs-asm``."""
|
|
32
|
+
parser = argparse.ArgumentParser(
|
|
33
|
+
prog="pyvacs-asm", description="Assemble Signetics 2650 source into a binary."
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument("source", help="assembly source file")
|
|
36
|
+
parser.add_argument("-o", "--output", help="output binary (default: stdout)")
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-f",
|
|
39
|
+
"--fill",
|
|
40
|
+
default="0",
|
|
41
|
+
help="pad byte for gaps between ORG blocks (default: 0)",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument("--version", action="version", version=f"pyvacs {__version__}")
|
|
44
|
+
args = parser.parse_args(argv)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
program = Assembler().assemble(_read_text(args.source))
|
|
48
|
+
except AssemblyError as exc:
|
|
49
|
+
print(f"pyvacs-asm: {exc}", file=sys.stderr)
|
|
50
|
+
return 1
|
|
51
|
+
except OSError as exc:
|
|
52
|
+
print(f"pyvacs-asm: {exc}", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
data = program.to_bytes(fill=int(args.fill, 0))
|
|
56
|
+
if args.output:
|
|
57
|
+
with open(args.output, "wb") as handle:
|
|
58
|
+
handle.write(data)
|
|
59
|
+
else:
|
|
60
|
+
sys.stdout.buffer.write(data)
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def disassemble_main(argv: Optional[List[str]] = None) -> int:
|
|
65
|
+
"""Entry point for ``pyvacs-dasm``."""
|
|
66
|
+
parser = argparse.ArgumentParser(
|
|
67
|
+
prog="pyvacs-dasm",
|
|
68
|
+
description="Disassemble a Signetics 2650 binary into source.",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument("binary", help="input binary file")
|
|
71
|
+
parser.add_argument("-o", "--output", help="output source (default: stdout)")
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"-r",
|
|
74
|
+
"--origin",
|
|
75
|
+
default="0",
|
|
76
|
+
help="load address of the binary (default: 0)",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"-l",
|
|
80
|
+
"--listing",
|
|
81
|
+
action="store_true",
|
|
82
|
+
help="emit an address/hex listing instead of reassemblable source",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument("--version", action="version", version=f"pyvacs {__version__}")
|
|
85
|
+
args = parser.parse_args(argv)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
data = _read_bytes(args.binary)
|
|
89
|
+
except OSError as exc:
|
|
90
|
+
print(f"pyvacs-dasm: {exc}", file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
origin = int(args.origin, 0)
|
|
94
|
+
disassembler = Disassembler()
|
|
95
|
+
if args.listing:
|
|
96
|
+
text = (
|
|
97
|
+
"\n".join(line.listing() for line in disassembler.disassemble(data, origin))
|
|
98
|
+
+ "\n"
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
text = disassembler.to_source(data, origin)
|
|
102
|
+
|
|
103
|
+
if args.output:
|
|
104
|
+
with open(args.output, "w", encoding="utf-8") as handle:
|
|
105
|
+
handle.write(text)
|
|
106
|
+
else:
|
|
107
|
+
sys.stdout.write(text)
|
|
108
|
+
return 0
|