-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathassembler.py
More file actions
278 lines (243 loc) · 10.3 KB
/
Copy pathassembler.py
File metadata and controls
278 lines (243 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#!/usr/bin/env python3
"""
Custom Assembler for the 32-Bit RISC Processor.
Converts assembly programs (.r.asm) into machine code (Hex, Binary, or Verilog Memory format).
Author: Antigravity AI & Suyash Mahar
License: MIT
"""
import os
import re
import sys
import argparse
# Define instruction set architecture mapping (indices correspond to opcodes)
OPCODES = [
# 0 - 23: Reserved
*[None] * 24,
# 24 - 31: Control & Memory
"LD", "ST", None, "JMP", None, "BEQ", "BNE", "LDR",
# 32 - 39: Arithmetic & Comparison
"ADD", "SUB", "MUL", "DIV", "CMPEQ", "CMPLT", "CMPLE", None,
# 40 - 47: Bitwise Logical & Shift
"AND", "OR", "XOR", None, "SHL", "SHR", "SRA", None,
# 48 - 55: Immediate Arithmetic & Comparison
"ADDC", "SUBC", "MULC", "DIVC", "CMPEQC", "CMPLTC", "CMPLEC", None,
# 56 - 63: Immediate Logical & Shift
"ANDC", "ORC", "XORC", None, "SHLC", "SHRC", "SRAC", None,
# 64: Raw 32-bit constant
"LONG"
]
# Global symbol table to resolve labels
symbol_table = {}
def clean_comments_and_whitespace(file_content):
"""
Strips semi-colon and hash comments, normalizes spacing, and returns list of lines.
"""
lines = []
for raw_line in file_content.splitlines():
# Remove comments starting with ; or #
line = re.sub(r'[;#].*', '', raw_line).strip()
if line:
lines.append(line)
return lines
def parse_register(token):
"""
Parses registers like %r3, %R3, r3, R3, and returns register index (0-31).
"""
match = re.match(r'^%?[rR](\d+)$', token)
if match:
reg_num = int(match.group(1))
if 0 <= reg_num < 32:
return reg_num
raise ValueError(f"Register index r{reg_num} is out of bounds (must be 0-31)")
raise ValueError(f"Invalid register format: '{token}'")
def is_register(token):
"""
Returns True if the token matches a register format.
"""
return bool(re.match(r'^%?[rR]\d+$', token))
def twos_complement(val, bits):
"""
Returns the binary string of the two's complement representation of val with fixed bit width.
"""
if val < 0:
val = (1 << bits) + val
val = val & ((1 << bits) - 1)
return f"{val:0{bits}b}"
def fix_literal(literal, bits=16):
"""
Converts literal string (decimal or hex) to a fixed-bit two's complement binary string.
"""
try:
if literal.lower().startswith("0x"):
val = int(literal, 16)
else:
val = int(literal)
return twos_complement(val, bits)
except ValueError:
raise ValueError(f"Invalid literal format: '{literal}'")
def get_label_offset(label, current_address):
"""
Returns the PC-relative signed offset to the target label.
"""
if label not in symbol_table:
raise KeyError(f"Undefined label: '{label}'")
# RISC processors often compute label offset relative to (current_address + 4) or current_address.
# The original implementation uses: (current_address - symbol_table[label])
# Let's preserve the original behavior:
offset_bytes = current_address - symbol_table[label]
return twos_complement(offset_bytes, 16)
def preprocess_glued_syntax(line):
"""
Fixes syntax typos where the instruction is glued to a register, e.g. 'LDr1' or 'CMPLECr31'.
"""
# Exclude label declarations starting with '.'
if line.startswith("."):
return line
for op in sorted([x for x in OPCODES if x], key=len, reverse=True):
pattern = rf"^{op}(%?[rR]\d+)"
if re.match(pattern, line, re.IGNORECASE):
# Insert space between instruction and register
line = re.sub(rf"^{op}", f"{op} ", line, flags=re.IGNORECASE)
break
return line
def convert_inst(tokens, addr):
"""
Converts a single instruction represented by list of tokens into its 32-bit binary representation.
"""
op_name = tokens[0].upper()
if op_name not in OPCODES:
raise ValueError(f"Unknown instruction / opcode: '{op_name}'")
op_code = OPCODES.index(op_name)
opcode_bin = f"{op_code:06b}"
# Count registers in arguments
reg_count = sum(1 for t in tokens[1:] if is_register(t))
if op_name == "LONG":
# 32-bit absolute constant
if len(tokens) < 2:
raise ValueError("LONG directive requires a constant literal value")
return fix_literal(tokens[1], 32)
elif len(tokens) == 3:
# Two-argument instructions like JMP or LDR
# Form 1: R-Type variant with 2 registers e.g. JMP %Ra, %Rc -> JMP Ra, Rc, 0
if reg_count == 2:
ra = parse_register(tokens[1])
rc = parse_register(tokens[2])
return f"{opcode_bin}{rc:05b}{ra:05b}{0:016b}"
# Form 2: Label variant e.g. LDR label, %Rc -> LDR Rc, 31, offset
else:
rc = parse_register(tokens[2])
ra = 31 # Hardwired r31 reference
offset = get_label_offset(tokens[1], addr)
return f"{opcode_bin}{rc:05b}{ra:05b}{offset}"
elif len(tokens) == 4:
# Three-argument instructions
if reg_count == 2:
# Immediate or Memory instructions with 2 registers and a literal
if op_name == "ST":
# ST %Rc, literal, %Ra
rc = parse_register(tokens[1])
literal = fix_literal(tokens[2], 16)
ra = parse_register(tokens[3])
return f"{opcode_bin}{rc:05b}{ra:05b}{literal}"
elif op_name in ("BEQ", "BNE"):
# BEQ/BNE %Rc, label, %Ra
rc = parse_register(tokens[1])
offset = get_label_offset(tokens[2], addr)
ra = parse_register(tokens[3])
return f"{opcode_bin}{rc:05b}{ra:05b}{offset}"
else:
# Standard I-Type: OP %Ra, literal, %Rc
ra = parse_register(tokens[1])
literal = fix_literal(tokens[2], 16)
rc = parse_register(tokens[3])
return f"{opcode_bin}{rc:05b}{ra:05b}{literal}"
elif reg_count == 3:
# Standard R-Type instructions: OP %Ra, %Rb, %Rc
ra = parse_register(tokens[1])
rb = parse_register(tokens[2])
rc = parse_register(tokens[3])
return f"{opcode_bin}{rc:05b}{ra:05b}{rb:05b}{0:011b}"
else:
raise ValueError(f"Invalid operand structure for three-operand instruction '{op_name}'")
else:
raise ValueError(f"Invalid instruction argument count for '{op_name}' ({len(tokens) - 1} given)")
def main():
parser = argparse.ArgumentParser(description="Professional Assembler for custom 32-Bit RISC Processor Core.")
parser.add_argument("input_file", help="Path to assembly source file (.r.asm)")
parser.add_argument("-o", "--output", help="Path to write the assembled machine code")
parser.add_argument("-f", "--format", choices=["hex", "bin", "verilog", "binary_string"], default="hex",
help="Output format: 'hex' (32-bit hex words), 'bin' (raw binary bytes), "
"'verilog' (ready for $readmemh), 'binary_string' (32-bit bitstrings)")
args = parser.parse_args()
if not os.path.exists(args.input_file):
print(f"Error: Input file '{args.input_file}' not found.", file=sys.stderr)
sys.exit(1)
with open(args.input_file, "r") as f:
raw_content = f.read()
# Pre-process comments, normalize spacing, and fix stuck instructions
raw_lines = clean_comments_and_whitespace(raw_content)
lines = [preprocess_glued_syntax(l) for l in raw_lines]
# First Pass: Build the Symbol Table
cur_address = 0
instructions = []
for line in lines:
if line.startswith("."):
label = line[1:].strip()
# Special case for .breakpoint, which compiles to a NOP (0x00000000) or special handling
if label.lower() == "breakpoint":
instructions.append((cur_address, ["ADD", "%r31", "%r31", "%r31"]))
cur_address += 4
else:
symbol_table[label] = cur_address
else:
# Split tokens by space or commas
tokens = [t for t in re.split(r'[,\s]+', line) if t]
if tokens:
instructions.append((cur_address, tokens))
cur_address += 4
# Second Pass: Translate instructions to binary machine code
binary_instructions = []
for addr, tokens in instructions:
try:
bin_str = convert_inst(tokens, addr)
binary_instructions.append(bin_str)
except Exception as e:
print(f"Assembly Error at PC=0x{addr:04X} | Instruction: {' '.join(tokens)}", file=sys.stderr)
print(f"Reason: {str(e)}", file=sys.stderr)
sys.exit(1)
# Formatted Output Generation
output_lines = []
if args.format == "hex":
for b in binary_instructions:
val = int(b, 2)
output_lines.append(f"{val:08x}")
elif args.format == "binary_string":
output_lines = binary_instructions
elif args.format == "verilog":
# Generate $readmemh memory file starting from address 0
output_lines.append("// Verilog Memory Initialization File ($readmemh format)")
for i, b in enumerate(binary_instructions):
val = int(b, 2)
output_lines.append(f"@{i:X} {val:08x}")
elif args.format == "bin":
# Raw bytes format
byte_data = bytearray()
for b in binary_instructions:
val = int(b, 2)
# Big Endian (matching 32-bit word alignment)
byte_data.extend(val.to_bytes(4, byteorder='big'))
output_path = args.output if args.output else args.input_file.rsplit('.', 1)[0] + ".bin"
with open(output_path, "wb") as f_out:
f_out.write(byte_data)
print(f"Successfully assembled: {len(binary_instructions)} instructions -> {output_path} (Raw Bytes)")
return
# Text formats output
output_content = "\n".join(output_lines) + "\n"
if args.output:
with open(args.output, "w") as f_out:
f_out.write(output_content)
print(f"Successfully assembled: {len(binary_instructions)} instructions -> {args.output} ({args.format.upper()})")
else:
sys.stdout.write(output_content)
if __name__ == "__main__":
main()