diff options
| author | uvok | 2026-01-14 20:44:16 +0100 |
|---|---|---|
| committer | uvok | 2026-01-14 20:44:16 +0100 |
| commit | 1561eff8780dc15dc5ea46d7225cc49a46f709ca (patch) | |
| tree | 130d44ef295ff2113fc56c592a78780035449dff /nandgame/assembler/py_nand_ass/simple_assembler.py | |
| parent | 281414ea9b42e213b85b95b7072b73d1f1e3f240 (diff) | |
Restructure asembler as package
Diffstat (limited to 'nandgame/assembler/py_nand_ass/simple_assembler.py')
| -rwxr-xr-x | nandgame/assembler/py_nand_ass/simple_assembler.py | 324 |
1 files changed, 324 insertions, 0 deletions
diff --git a/nandgame/assembler/py_nand_ass/simple_assembler.py b/nandgame/assembler/py_nand_ass/simple_assembler.py new file mode 100755 index 0000000..d684635 --- /dev/null +++ b/nandgame/assembler/py_nand_ass/simple_assembler.py @@ -0,0 +1,324 @@ +#!/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 +from typing import Union + +from . import parser_types as pt + +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}") + +#Arg = Union[str, int, pt.Address, pt.Immediate, pt.Register] +Arg = Union[str, int, None] + +def encode_instruction( + mnemonic: str, dest: str, op1: Arg, op2: Arg, jump: str +) -> int: + """ + Encode a single instruction into a 16-bit integer. + """ + mnemonic = mnemonic.strip() + + if mnemonic == "hlt": + return 0xFFFF & ~0x4000 + + dest = dest.strip() + if op1 is None: + op1 = "" + elif isinstance(op1, int): + op1 = f"#{op1}" + else: + op1 = str(op1).strip() + + if op2 is None: + op2 = "" + elif isinstance(op2, int): + op2 = f"#{op2}" + else: + op2 = str(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("Invalid args to mov.") + + # C-instruction + if mnemonic in MNEMONICS: + pass + elif mnemonic == "nop": + mnemonic = "and" + dest = "_" + op1 = "#0" + op2 = "A" + jump = "" + else: + 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", encoding="ascii") 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}") from e |
