From c8138da2dd4ad9a8d7aeebbd060b8e0820cbfbd5 Mon Sep 17 00:00:00 2001 From: uvok Date: Wed, 14 Jan 2026 17:52:12 +0100 Subject: assembler: Implement no use in doing this in nice small commits --- nandgame/assembler/assembler.py | 188 +++++++++++++++++++++++++++++++++------- 1 file changed, 157 insertions(+), 31 deletions(-) (limited to 'nandgame') diff --git a/nandgame/assembler/assembler.py b/nandgame/assembler/assembler.py index a622891..34b228c 100644 --- a/nandgame/assembler/assembler.py +++ b/nandgame/assembler/assembler.py @@ -1,57 +1,183 @@ #!/usr/bin/env python3 +from dataclasses import dataclass import sys -from typing import Iterable, Tuple +from typing import Callable, Iterable from parser import parser -from parsetypes import * +import parser_types as pt +from simple_assembler import encode_instruction -def check_num_args(instruction: Instruction) -> Tuple[int, int]: - return (0, 20) +@dataclass +class MnemonicInfo: + opcode: str + num_args: int + supports_jmp: bool -def check_supports_jump(instruction: Instruction) -> bool: - return True +class Program: + + def __init__(self): + self.labels: dict[str, int] = {} + self.instructions: bytes = b"" + self.pc: int = 0 + + def encode(self, ins: pt.Instruction): + arg1 = self._resolve(ins, lambda: ins.arg1) + arg2 = self._resolve(ins, lambda: ins.arg2) + + op = encode_instruction( + ins.opcode, + ins.dest.name if ins.dest else "", + arg1, + arg2, + (ins.jumptarget or ""), + ) + self.instructions += op.to_bytes(length=2, byteorder="little") + + def _resolve( + self, + ins: pt.Instruction, + get_prop: Callable[[], pt.Symbol | pt.Register | pt.Immediate | str | None], + ): + arg = get_prop() + if isinstance(arg, pt.Symbol): + ret = self.labels.get(arg.name, None) + if ret is None: + raise ValueError(f"Line {ins.lineno}: Label {arg.name} not defined") + elif isinstance(arg, pt.Register): + ret = arg.name + elif isinstance(arg, pt.Immediate): + ret = arg.value + else: + ret = arg + + return ret + + def write_to_file(self, filename: str) -> None: + with open(filename, "wb") as outfile: + outfile.write(self.instructions) + print(f"Output written to {filename}") + + +opcode_infos: dict[str, MnemonicInfo] = { + "and": MnemonicInfo("and", 3, True), + "or": MnemonicInfo("or", 3, True), + "xor": MnemonicInfo("xor", 3, True), + "not": MnemonicInfo("not", 2, True), + "mov": MnemonicInfo("mov", 2, True), + "add": MnemonicInfo("add", 3, True), + "inc": MnemonicInfo("inc", 2, True), + "sub": MnemonicInfo("sub", 3, True), + "dec": MnemonicInfo("dec", 2, True), + "cmp": MnemonicInfo("cmp", 2, True), + "neg": MnemonicInfo("neg", 2, True), + "hlt": MnemonicInfo("hlt", 0, False), + "nop": MnemonicInfo("nop", 0, False), +} + + +def get_op_info(instruction: pt.Instruction) -> MnemonicInfo | None: + """Get information about a given opcode in a instruction.""" + return opcode_infos.get(instruction.opcode, None) def check_instructions( - instructions: Iterable[Instruction], -) -> Iterable[ErrorInstruction]: - for i in instructions: - if isinstance(i, ErrorInstruction): - yield i + instructions: Iterable[pt.AsmLine], +) -> Iterable[pt.ErrorInstruction]: + """Given an iterable of assembler lines, check for errors.""" + for ins in instructions: + # If instruction already is an error generated by the parser, just return that. + if isinstance(ins, pt.ErrorInstruction): + yield ins continue - (min, max) = check_num_args(i) - if not (min <= i.num_args <= max): - yield ErrorInstruction.from_instruction( - i, f"Expected between {min} and {max} args, got {i.num_args}." + if not isinstance(ins, pt.Instruction): + continue + + if ( + ins.arg1 is not None + and ins.arg2 is not None + and not isinstance(ins.arg1, pt.Register) + and not isinstance(ins.arg2, pt.Register) + ): + yield pt.ErrorInstruction( + lineno=ins.lineno, + opcode=ins.opcode, + error_message="At least one argument must be a register.", + ) + + opinfo = get_op_info(ins) + if opinfo is None: + yield pt.ErrorInstruction( + lineno=ins.lineno, + opcode=ins.opcode, + error_message="Unknown instruction", + ) + continue + + if opinfo.num_args != ins.num_args: + yield pt.ErrorInstruction( + lineno=ins.lineno, + opcode=ins.opcode, + error_message=f"Expected {opinfo.num_args} args, got {ins.num_args}.", ) - if not check_supports_jump(i) and i.jumptarget: - yield ErrorInstruction.from_instruction( - i, f"OPcode got jump, but it's not supported here." + if not opinfo.supports_jmp and ins.jumptarget: + yield pt.ErrorInstruction( + lineno=ins.lineno, + opcode=ins.opcode, + error_message="OPcode got a jump, but it's not supported here.", ) -labels: dict[str,int] = {} -def assemble(program: Iterable[Instruction|JumpTarget]) -> None: - pc = 0 - for ins in program: - if isinstance(ins, JumpTarget): - pass +def assemble(instructions: Iterable[pt.AsmLine]) -> Program: + prog = Program() + + prog.pc = 0 + # first pass: populate symbols + for ins in instructions: + match ins: + case pt.JumpTarget(): + lblname = ins.label.name + if lblname in prog.labels: + print( + f"WARNING: Label {lblname} redefined on line {ins.lineno}. Using previous definition.", + file=sys.stderr, + ) + else: + prog.labels[lblname] = prog.pc + case pt.Instruction(): + prog.pc += 1 + + prog.pc = 0 + # second pass: assemble with resolve + for ins in instructions: + match ins: + case pt.Instruction(): + prog.encode(ins) + prog.pc += 1 + return prog -with open(sys.argv[1], "rb") as f: - data = f.read() + +with open(sys.argv[1], "rb") as infile: + data = infile.read() data2 = data.decode("ascii") - result: list[Instruction | JumpTarget] + result: list[pt.AsmLine] result = parser.parse(data2, tracking=True) - errors = check_instructions(ins for ins in result if isinstance(ins, Instruction)) + errors = check_instructions(result) + errors = list(errors) + if errors: for e in errors: - print(f"On line {e.lineno}: {e.opcode} : {e.error_message}") + print(f"ERROR: On line {e.lineno}: {e.opcode} : {e.error_message}") sys.exit(1) - assemble(result) - pass + print("Instruction checks passed") + p = assemble(result) + if len(sys.argv) >= 3: + dest = sys.argv[2] + else: + dest = sys.argv[1] + ".bin" + p.write_to_file(dest) -- cgit v1.2.3