summaryrefslogtreecommitdiff
path: root/nandgame
diff options
context:
space:
mode:
authoruvok2026-01-14 17:52:12 +0100
committeruvok2026-01-14 17:52:12 +0100
commitc8138da2dd4ad9a8d7aeebbd060b8e0820cbfbd5 (patch)
treec388e5d6285f27d90715bbc10f31afdb3c69ac40 /nandgame
parent4340b7d0096ec2bafa580363c654291dd01227a1 (diff)
assembler: Implement
no use in doing this in nice small commits
Diffstat (limited to 'nandgame')
-rw-r--r--nandgame/assembler/assembler.py188
1 files changed, 157 insertions, 31 deletions
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)