summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xnandgame/assembler/assemble.py307
1 files changed, 307 insertions, 0 deletions
diff --git a/nandgame/assembler/assemble.py b/nandgame/assembler/assemble.py
new file mode 100755
index 0000000..5aa05ce
--- /dev/null
+++ b/nandgame/assembler/assemble.py
@@ -0,0 +1,307 @@
+#!/usr/bin/env python3
+
+###
+# LLM generated
+###
+
+"""
+Assembler for nandgame, matching the custom disassembler above.
+
+Syntax (as produced by print_decoded):
+
+ mnemonic[.jump] DEST, OP1[, OP2]
+
+Examples:
+
+ mov A, #123
+ add.jgt D, D, A
+ sub _, D, M
+ not D, D
+ inc.jeq D, D
+ and _, D, M
+ xor M, D, M
+
+DEST:
+ A, D, M, any combination like AD, AM, DM, ADM, or "_" for no destination.
+
+OP1 / OP2:
+ D, A, M, #0 (for OP1 only), or #<number> for mov A,#imm (A-instruction).
+
+Jumps:
+ jlt, jle, jeq, jne, jgt, jge, jmp, or none.
+"""
+
+import sys
+
+ZERO = "#0"
+DEST_NONE = "_"
+JUMP_NONE = ""
+ENDIANNESS = "little"
+
+# mapping from mnemonic to (opcode, two_op)
+MNEMONICS = {
+ "and": (0b000, True),
+ "or": (0b001, True),
+ "xor": (0b010, True),
+ "not": (0b011, False),
+ "add": (0b100, True),
+ "inc": (0b101, False),
+ "sub": (0b110, True),
+ "dec": (0b111, False),
+}
+
+# jump mnemonic -> bits 0..2
+JUMP_ENCODE = {
+ "": 0b000,
+ "jgt": 0b001,
+ "jeq": 0b010,
+ "jge": 0b011,
+ "jlt": 0b100,
+ "jne": 0b101,
+ "jle": 0b110,
+ "jmp": 0b111,
+}
+
+
+def encode_dest(dest: str) -> int:
+ """
+ dest is something like "A", "D", "M", "AD", "ADM", or "_" for none.
+ Returns bits for A,D,M in positions 5,4,3.
+ """
+ dest = dest.strip()
+ if dest == DEST_NONE:
+ return 0
+
+ bits = 0
+ if "A" in dest:
+ bits |= (1 << 5)
+ if "D" in dest:
+ bits |= (1 << 4)
+ if "M" in dest:
+ bits |= (1 << 3)
+ return bits
+
+
+def encode_jump(jump: str) -> int:
+ jump = jump.strip()
+ if jump not in JUMP_ENCODE:
+ raise ValueError(f"Unknown jump condition: {jump}")
+ return JUMP_ENCODE[jump]
+
+
+def encode_args_two_op(op1: str, op2: str) -> int:
+ """
+ For two-operand instructions, find zx, sw, use_mem bits that reproduce
+ the given op1/op2 under decode_arg1/decode_arg2.
+
+ op1 in {D, A, M, #0}
+ op2 in {D, A, M}
+ """
+ op1 = op1.strip()
+ op2 = op2.strip()
+
+ # brute-force all combinations of zx, sw, use_mem and pick the one that matches
+ for zx in (0, 1):
+ for sw in (0, 1):
+ for use_mem in (0, 1):
+ # simulate decode_arg1/2
+ if zx:
+ dec_op1 = ZERO
+ else:
+ if not sw:
+ dec_op1 = "D"
+ else:
+ dec_op1 = "M" if use_mem else "A"
+
+ if sw:
+ dec_op2 = "D"
+ else:
+ dec_op2 = "M" if use_mem else "A"
+
+ if dec_op1 == op1 and dec_op2 == op2:
+ bits = 0
+ if use_mem:
+ bits |= (1 << 12)
+ if zx:
+ bits |= (1 << 7)
+ if sw:
+ bits |= (1 << 6)
+ return bits
+
+ raise ValueError(f"Unsupported operand combination for two-op: {op1}, {op2}")
+
+
+def encode_args_one_op(op1: str) -> int:
+ """
+ For one-operand instructions, only decode_arg1 matters.
+ We choose canonical encodings:
+
+ D -> zx=0, sw=0
+ A -> zx=0, sw=1, use_mem=0
+ M -> zx=0, sw=1, use_mem=1
+ #0 -> zx=1
+ """
+ op1 = op1.strip()
+ bits = 0
+
+ if op1 == ZERO:
+ bits |= (1 << 7) # zx
+ # sw/use_mem don't matter for arg1 when zx=1, but keep them 0
+ return bits
+
+ if op1 == "D":
+ # zx=0, sw=0, use_mem=0
+ return bits
+
+ if op1 == "A":
+ bits |= (1 << 6) # sw=1
+ # use_mem=0
+ return bits
+
+ if op1 == "M":
+ bits |= (1 << 6) # sw=1
+ bits |= (1 << 12) # use_mem=1
+ return bits
+
+ raise ValueError(f"Unsupported operand for one-op: {op1}")
+
+
+def encode_instruction(mnemonic: str, dest: str, op1: str, op2: str, jump: str) -> int:
+ """
+ Encode a single instruction into a 16-bit integer.
+ """
+ mnemonic = mnemonic.strip()
+
+ if mnemonic == "hlt":
+ return (0xFFFF & ~0x4000)
+
+ dest = dest.strip()
+ op1 = op1.strip()
+ op2 = op2.strip()
+ jump = jump.strip()
+
+ # A-instruction: mov A, #imm
+ if mnemonic == "mov":
+ if dest == "A" and op1.startswith("#") and not op2 and not jump:
+ imm_str = op1[1:]
+ if imm_str.startswith("0x") or imm_str.startswith("0X"):
+ value = int(imm_str, 16)
+ else:
+ value = int(imm_str, 10)
+ if not (0 <= value < 0x8000):
+ raise ValueError(f"Immediate out of range (0..32767): {value}")
+ return value & 0x7FFF
+ else:
+ raise ValueError(f"Invalid args to mov.")
+
+ # C-instruction
+ if mnemonic not in MNEMONICS:
+ raise ValueError(f"Unknown mnemonic: {mnemonic}")
+
+ opcode, two_op = MNEMONICS[mnemonic]
+
+ # bit 14, 15 = 1
+ ins = 0xC000
+
+ # opcode bits: low 2 bits in 8..9, high bit in 10 (ar_n_log)
+ low2 = opcode & 0b11
+ high1 = (opcode >> 2) & 0b1
+ ins |= (low2 << 8)
+ if high1:
+ ins |= (1 << 10)
+
+ # dest bits
+ ins |= encode_dest(dest)
+
+ # jump bits
+ ins |= encode_jump(jump)
+
+ # arg bits
+ if two_op:
+ if not op2:
+ raise ValueError(f"Two-op instruction {mnemonic} requires two operands")
+ ins |= encode_args_two_op(op1, op2)
+ else:
+ if not op1:
+ raise ValueError(f"One-op instruction {mnemonic} requires one operand")
+ ins |= encode_args_one_op(op1)
+
+ return ins
+
+
+def parse_line(line: str):
+ """
+ Parse a single assembly line into (mnemonic, dest, op1, op2, jump).
+ Returns None if the line is empty or comment.
+ """
+ # strip comments starting with ';' or '#'
+ for sep in (";"):
+ idx = line.find(sep)
+ if idx != -1:
+ line = line[:idx]
+ line = line.strip()
+ if not line:
+ return None
+
+ # first token: mnemonic[.jump]
+ parts = line.split(None, 1)
+ if not parts:
+ return None
+ opcode_part = parts[0]
+ rest = parts[1] if len(parts) > 1 else ""
+
+ if "." in opcode_part:
+ mnemonic, jump = opcode_part.split(".", 1)
+ else:
+ mnemonic, jump = opcode_part, ""
+
+ # operands: dest, op1[, op2]
+ dest = ""
+ op1 = ""
+ op2 = ""
+
+ if rest:
+ ops = [o.strip() for o in rest.split(",")]
+ ops = [o for o in ops if o] # remove empty
+ if len(ops) >= 1:
+ dest = ops[0]
+ if len(ops) >= 2:
+ op1 = ops[1]
+ if len(ops) >= 3:
+ op2 = ops[2]
+ if len(ops) > 3:
+ raise ValueError(f"Too many operands: {rest}")
+
+ # normalize no-dest
+ if dest == "":
+ dest = DEST_NONE
+
+ return mnemonic, dest, op1, op2, jump
+
+
+def assemble_file(in_filename: str, out_filename: str):
+ with open(in_filename, "r") as fin, open(out_filename, "wb") as fout:
+ lineno = 0
+ for line in fin:
+ lineno += 1
+ try:
+ parsed = parse_line(line)
+ if parsed is None:
+ continue
+ mnemonic, dest, op1, op2, jump = parsed
+ ins = encode_instruction(mnemonic, dest, op1, op2, jump)
+ fout.write(ins.to_bytes(2, byteorder=ENDIANNESS))
+ except Exception as e:
+ raise SystemExit(f"{in_filename}:{lineno}: {e}")
+
+
+def main():
+ if len(sys.argv) != 3:
+ print(f"Usage: {sys.argv[0]} input.asm output.bin")
+ sys.exit(1)
+
+ assemble_file(sys.argv[1], sys.argv[2])
+
+
+if __name__ == "__main__":
+ main()