This commit is contained in:
Neemek 2025-12-14 19:45:09 +01:00
commit d7091e7f36
Signed by: neemek
GPG key ID: 84FFE4D7D40AB25E
12 changed files with 757 additions and 0 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.13

0
README.md Normal file
View file

257
assembler.py Normal file
View file

@ -0,0 +1,257 @@
import sys
import re
binary_instructions = ["ADD", "ADC", "SUB", "XOR", "MUL", "AND", "SHR"]
unary_instructions = ["DRF", "SND"]
address_instructions = ["JMP", "CAL"]
conditional_instructions = ["JIZ", "JNZ", "GET", "PUT"]
mono_instructions = ["RET"]
immediate_instructions = ["IMM"]
instruction_map = {
"ADD": 0b0000,
"ADC": 0b0001,
"SUB": 0b0010,
"XOR": 0b0011,
"GET": 0b0100,
"PUT": 0b0101,
"JMP": 0b0110,
"JIZ": 0b0111,
"JNZ": 0b1000,
"CAL": 0b1001,
"RET": 0b1010,
"SND": 0b1011,
"DRF": 0b1100,
"AND": 0b1101,
"IMM": 0b1110,
"SHR": 0b1111,
}
def main():
inp = "test.ns" #sys.argv[1]
#out = sys.argv[2]
with open(inp, 'r') as infile:
data = infile.read()
rom = bytearray(2**9)
pc = 0
i = 0
labels = {}
put_labels = {}
line = 1
column = 1
def gobble(det: lambda c: bool, n: int=-1) -> str:
nonlocal line
nonlocal column
nonlocal i
start = i
while i < len(data) and (i - start) != n and det(data[i]):
column += 1
if data[i] == "\n":
line += 1
column = 1
i += 1
return data[start:i]
parse_name = lambda: gobble(lambda c: c.isalnum() or c == "_")
parse_hex = lambda n=-1: gobble(lambda c: (c.isdigit() or c.lower() in "abcdef"), n)
parse_decimal = lambda n=-1: gobble(lambda c: c.isdigit(), n or -1)
skip_whitespace = lambda: gobble(lambda c: c.isspace())
def error(msg: str):
raise ValueError(f"Error at {line}:{column}: {msg}")
def accept(s: str) -> bool:
nonlocal i
if data[i:i+len(s)] == s:
i += len(s)
return True
return False
def expect(s: str, msg: str = ""):
nonlocal i
if data[i:i+len(s)] != s:
error(f"Expected '{s}' (found {data[i:i+len(s)]})" + (": " + msg if msg else ""))
i += len(s)
def parse_register() -> int:
skip_whitespace()
expect("r", "registers must start with r")
token = parse_decimal(1)
if not token.isdigit() or int(token) < 0 or int(token) > 7:
error(f"Register out of range: {token}")
return int(token)
def parse_label() -> tuple[str, int]:
skip_whitespace()
expect("#", "label references must start with #")
label = parse_name()
offset = 0
if accept("."):
offset_str = parse_decimal()
offset = int(offset_str)
return label, offset
skip_whitespace()
while i < len(data):
if pc >= len(rom):
error("Program too large to fit in ROM")
c = data[i]
if c == "@":
i += 1
label = parse_name()
labels[label] = pc
elif c == "x":
i += 1
token = parse_hex()
value = int(token, 16)
if value < 0 or value > 255:
error(f"Value out of range: {token}")
rom[pc] = value
pc += 1
elif c == "+":
i += 1
token = parse_decimal()
value = int(token, 10)
if pc + value >= len(rom):
error(f"Offset value outside rom: {token}")
pc += value
elif c == ":":
i += 1
token = parse_decimal()
value = int(token, 10)
if value < 0 or value >= len(rom):
error(f"Address out of range: {token}")
pc = value
elif c == '"': # string literal
i += 1
while i < len(data) and data[i] != '"':
rom[pc] = ord(data[i]) & 0xFF
pc += 1
i += 1
if i >= len(data) or data[i] != '"':
error("Unterminated string literal")
i += 1
elif c == ";": # comment
i += 1
gobble(lambda c: c != "\n")
else: # instruction
token = parse_name().upper()
if token in binary_instructions:
rom[pc] = instruction_map[token] << 4
skip_whitespace()
r0 = parse_register()
r1 = parse_register()
r2 = parse_register()
rom[pc] |= (int(r0) & 0b111) << 1
rom[pc] |= (int(r1) >> 2) & 0b001
pc += 1
rom[pc] = 0
rom[pc] |= (int(r1) & 0b011) << 6
rom[pc] |= (int(r2) & 0b111) << 3
pc += 1
elif token in address_instructions:
rom[pc] = instruction_map[token] << 4
label, offset = parse_label()
put_labels[pc] = { "label": label, "offset": offset, "type": "address" }
pc += 2
elif token in conditional_instructions:
rom[pc] = instruction_map[token] << 4
skip_whitespace()
label, offset = parse_label()
put_labels[pc] = { "label": label, "offset": offset, "type": "address" }
pc += 1
r0 = parse_register()
rom[pc] = int(r0)
pc += 1
elif token in mono_instructions:
rom[pc] = instruction_map[token] << 4
pc += 1
elif token in immediate_instructions:
rom[pc] = instruction_map[token] << 4
r0 = parse_register()
rom[pc] |= (int(r0) & 0b111) << 1
pc += 1
skip_whitespace()
if data[i] == "#":
label, offset = parse_label()
put_labels[pc] = { "label": label, "offset": offset, "type": "immediate" }
imm_value = 0
else:
expect("x", "hexadecimal values must start with x")
imm_token = parse_hex()
imm_value = int(imm_token, 16)
if imm_value < 0 or imm_value > 255:
error(f"Immediate value out of range: {imm_token}")
rom[pc] = imm_value
pc += 1
elif token in unary_instructions:
rom[pc] = instruction_map[token] << 4
r0 = parse_register()
r1 = parse_register()
rom[pc] |= (int(r0) & 0b111) << 1
rom[pc] |= (int(r1) & 0b001) >> 2
pc += 1
rom[pc] = (int(r1) & 0b011) << 6
pc += 1
else:
error(f"Unknown instruction at position {i}: {token}")
skip_whitespace()
# put labels
for addr, label in put_labels.items():
if label["label"] not in labels:
error(f"Undefined label: {label['label']}")
target = labels[label["label"]] + label["offset"]
if target < 0 or target >= len(rom):
error(f"Label address out of range: {label['label']}")
if label["type"] == "address":
rom[addr] |= (target >> 5) & 0b1111
rom[addr + 1] |= (target & 0b11111) << 3
elif label["type"] == "immediate":
if target < 0 or target > 255:
error(f"Immediate label address out of range: {label['label']}")
rom[addr] = target & 0xFF
else:
error(f"Unknown label type for label: {label['label']}")
with open("out.rom", 'wb') as outfile:
outfile.write(rom)
if __name__ == "__main__":
main()

137
emulator.py Normal file
View file

@ -0,0 +1,137 @@
import sys
from assembler import instruction_map as instruction_to_number_map, binary_instructions, address_instructions, conditional_instructions, unary_instructions, mono_instructions, immediate_instructions
number_to_instruction_map = {v: k for k, v in instruction_to_number_map.items()}
class Emulator:
def __init__(self, ram: bytearray):
self.ram = ram
self.pc = 0
self.registers = bytearray(8)
self.carry = 0
self.call_stack = []
def step(self):
opcode = self.ram[self.pc] >> 4
op = number_to_instruction_map[opcode]
if op in unary_instructions or op in binary_instructions:
reg_a = (self.ram[self.pc] >> 1) & 0b111
reg_b = ((self.ram[self.pc] & 0b1) << 2) | ((self.ram[self.pc + 1] >> 6) & 0b11)
if op in unary_instructions:
match op:
case "DRF":
addr = self.registers[reg_a]
v = self.ram[addr]
self.registers[reg_b] = v
case "SND":
self.ram[self.registers[reg_a]] = self.registers[reg_b]
case _:
raise ValueError(f"Unknown unary opcode at PC={self.pc:03X}: {opcode:04b}")
elif op in binary_instructions:
reg_c = (self.ram[self.pc + 1] >> 3) & 0b111
if op in binary_instructions:
match op:
case "ADD" | "ADC":
v = self.registers[reg_a] + self.registers[reg_b] + (self.carry if op == "ADC" else 0)
if v > 0xFF:
self.carry = 1
else:
self.carry = 0
self.registers[reg_c] = v & 0xFF
case "SUB":
self.registers[reg_c] = (self.registers[reg_a] - self.registers[reg_b] + 0x100) & 0xFF
case "XOR":
self.registers[reg_c] = self.registers[reg_a] ^ self.registers[reg_b]
case "SHR":
self.registers[reg_c] = self.registers[reg_a] >> self.registers[reg_b]
case "AND":
self.registers[reg_c] = self.registers[reg_a] & self.registers[reg_b]
case _:
raise ValueError(f"Unknown binary opcode at PC={self.pc:03X}: {opcode:04b}")
elif op in address_instructions:
addr = ((self.ram[self.pc] & 0b1111) << 5) | (self.ram[self.pc + 1] >> 3)
match op:
case "JMP":
self.pc = addr
case "CAL":
self.call_stack.append(self.pc + 2)
self.pc = addr
case _:
raise ValueError(f"Unknown conditional opcode at PC={self.pc:03X}: {opcode:04b}")
return
elif op in conditional_instructions:
reg = self.ram[self.pc+1] & 0b111
addr = ((self.ram[self.pc] & 0b1111) << 5) | (self.ram[self.pc + 1] >> 3)
match op:
case "JIZ":
if self.registers[reg] == 0:
self.pc = addr
return
case "JNZ":
if self.registers[reg] != 0:
self.pc = addr
return
case "PUT":
self.ram[addr] = self.registers[reg]
case "GET":
self.registers[reg] = self.ram[addr]
case _:
raise ValueError(f"Unknown conditional opcode at PC={self.pc:03X}: {opcode:04b}")
elif op in mono_instructions:
match op:
case "RET":
if not self.call_stack:
raise ValueError(f"Call stack underflow at PC={self.pc:03X}")
self.pc = self.call_stack.pop()
return
case _:
raise ValueError(f"Unknown mono opcode at PC={self.pc:03X}: {opcode:04b}")
elif op in immediate_instructions:
reg = (self.ram[self.pc] >> 1) & 0b111
imm = self.ram[self.pc + 1]
match op:
case "IMM":
self.registers[reg] = imm
case _:
raise ValueError(f"Unknown immediate opcode at PC={self.pc:03X}: {opcode:04b}")
else:
raise ValueError(f"Unknown opcode at PC={self.pc:03X}: {opcode:04b}")
self.pc += 2
def state(self) -> str:
regs = ' '.join(f'r{i}={self.registers[i]:02X}' for i in range(8))
return f'PC={self.pc:03X} RAM[PC]={self.ram[self.pc]:08b} RAM[PC+1]={self.ram[self.pc+1]:08b} C={self.carry} \t' + regs
def main():
ram = bytearray(2**9)
with open("out.rom", 'rb') as infile:
rom_data = infile.read()
ram[0:len(rom_data)] = rom_data
emu = Emulator(ram)
prev_pc = -1
while emu.pc != prev_pc:
if emu.pc >= len(ram):
print("PC out of bounds!")
break
# handle output
if emu.ram[511] != 0:
print(f"{chr(emu.ram[-1])}", end='')
emu.ram[-1] = 0
prev_pc = emu.pc
#print(emu.state())
emu.step()
#print("End:\t " + emu.state())
if __name__ == "__main__":
main()

98
mul.ns Normal file
View file

@ -0,0 +1,98 @@
jmp #start
; ROM/RAM data would go here before start
@number x0
@string "Hello world!" x0A x00 ; Newline and null terminated
@start
imm r1 xF
imm r2 x7
cal #full_ladder_multiply
imm r0 #string ; put the address of #string into r0
cal #print ; print it!
jmp #loop ; limbo/endless loop
@multiply ; Multiply r1 by r2, putting the result in r0
imm r7 x1 ; We use r7 to decrement r2
@multiply_round ; Increment and decrement
add r0 r1 r0 ; Add r1 to counter
sub r2 r7 r2 ; Decrement r2
jnz #multiply_round r2 ; If r2 is not zero, again!
ret
@full_ladder_multiply ; Multiply r1 by r2, putting the result in r0 (r1 ends up being r1*r2 + r1)
imm r6 x1 ; let r6 be 1. it is used as a mask to select the first bit
imm r7 x80 ; let r7 be 0b1000_0000. it is used as a bit mask for the current bit
; Move r2 into r5
imm r5 x0
add r2 r5 r5
@full_ladder_multiply_find_high_bit ; go through each bit from low to high. if we encounter a 1, we have found the highest bit.
and r7 r2 r3 ; use r7 as a mask for r2, getting the current bit
shr r7 r6 r7 ; shift r7 right by one (r6 = 1)
jiz #full_ladder_multiply_find_high_bit r3 ; r7 ends up being 2^(l-2), where the bit at l-1 is 1 and is the highest bit
imm r0 x0 ; Let r0 be 0 (to reset it).
add r0 r1 r0 ; r0 starts with the value of r1
imm r1 x0
add r0 r0 r2 ; r1 starts with the value of two r1
imm r3 x0
adc r3 r3 r3 ; put the carry into r3
@full_ladder_multiply_round
and r5 r7 r3 ; let r3 be r5 & r7. We check if the bit that r7 is masking is high on r2 later
shr r7 r6 r7 ; Shift r7 right by 1 (r6 = 1)
jnz #full_ladder_multiply_round_1 r3 ; if r3 is 1, we jump. otherwise
@full_ladder_multiply_round_0
add r0 r2 r2 ; Let r2 be r0 + r2
adc r1 r3 r3 ; carry
add r0 r0 r0 ; Double r0
adc r1 r1 r1 ; Double r1 and add carry
jmp #ladder_multiply_round_end ; Skip the second alternative
@full_ladder_multiply_round_1
add r0 r2 r0 ; Let r0 be r0 + r2
adc r1 r3 r1 ; carry
add r2 r2 r2 ; Double r0
adc r3 r3 r3 ; Double r1 and add carry
@full_ladder_multiply_round_end
jnz #full_ladder_multiply_round r7 ; if the mask still hasn't been shifted out (r7 != 0), do another round
ret
@print ; Print a string to output, given r0 as the address of a null-terminated string
imm r2 x1
@print_loop
drf r0 r1 ; get the value that r0 points to, and put it in r1
jiz #print_loop_end r1 ; if the value of r1 is 0, we have reached the end and should exit
put #output r1 ; output the character
add r0 r2 r0 ; increment the pointer (r2 = 1)
jmp #print_loop ; again!
@print_loop_end
ret
@loop ; Infinite loop
jmp #loop
:511 @output ; I/O address for serial out

88
multiply.ns Normal file
View file

@ -0,0 +1,88 @@
jmp #start
; ROM/RAM data would go here before start
@number x0
@string "Hello world!" x0A x00 ; Newline and null terminated
@start
imm r1 xF
imm r2 x7
cal #ladder_multiply
put #number r0 ; keep the number
imm r1 #string
cal #number_to_string
imm r0 #string ; put the address of #string into r0
cal #print ; print it!
jmp #loop ; limbo/endless loop
@multiply ; Multiply r1 by r2, putting the result in r0
imm r7 x1 ; We use r7 to decrement r2
@multiply_round ; Increment and decrement
add r0 r1 r0 ; Add r1 to counter
sub r2 r7 r2 ; Decrement r2
jnz #multiply_round r2 ; If r2 is not zero, again!
ret
@ladder_multiply ; Multiply r1 by r2, putting the result in r0 (r1 ends up being r1*r2 + r1)
imm r0 x0 ; Let r0 be 0 (to reset it).
imm r6 x1 ; let r6 be 1. it is used as a mask to select the first bit
imm r7 x80 ; let r7 be 0b1000_0000. it is used as a bit mask for the current bit
@ladder_multiply_find_high_bit ; go through each bit from low to high. if we encounter a 1, we have found the highest bit.
and r7 r2 r3
shr r7 r6 r7
jiz #ladder_multiply_find_high_bit r3 ; r7 ends up being 2^(l-2), where the bit at l-1 is 1 and is the highest bit
add r0 r1 r0 ; r0 starts with the value of r1
add r1 r1 r1 ; r1 starts with the value of two r1
@ladder_multiply_round
and r2 r7 r3 ; let r3 be r2 & r7. We check if the bit that r7 is masking is high on r2 later
shr r7 r6 r7 ; Shift r7 right by 1 (r6 = 1)
jnz #ladder_multiply_round_1 r3 ; if r3 is 1, we jump. otherwise
@ladder_multiply_round_0
add r0 r1 r1 ; Let r1 be r0 + r1
add r0 r0 r0 ; Double r0
jmp #ladder_multiply_round_end ; Skip the second alternative
@ladder_multiply_round_1
add r0 r1 r0 ; Let r0 be r0 + r1
add r1 r1 r1 ; Double r1
@ladder_multiply_round_end
jnz #ladder_multiply_round r7 ; if the mask still hasn't been shifted out (r7 != 0), do another round
ret
@print ; Print a string to output, given r0 as the address of a null-terminated string
imm r2 x1
@print_loop
drf r0 r1 ; get the value that r0 points to, and put it in r1
jiz #print_loop_end r1 ; if the value of r1 is 0, we have reached the end and should exit
put #output r1 ; output the character
add r0 r2 r0 ; increment the pointer (r2 = 1)
jmp #print_loop ; again!
@print_loop_end
ret
@loop ; Infinite loop
jmp #loop
:511 @output ; I/O address for serial out

53
number.ns Normal file
View file

@ -0,0 +1,53 @@
; this was more complicated than i thought it would be
@number_to_string_len x0
@number_to_string_addr x0
@number_to_string ; convert a number in r0 into a null-terminated decimal ascii string stored at the address r1 of r2 digits
put #number_to_string_len r2
put #number_to_string_addr r1
; Converting a binary number to a decimal string requires using the double-dabble algorithm
@number_to_string_loop
; Shift everything to the left
add r0 r0 r0 ; Equivalent to doubling, which has the same effect as shifting to the left
imm r3 x0 ; let r3 be the offset from the start of the string of the current character
imm r4 x0 ; r4 is used to store the overflowed bit from the previous shift
; for each digit
@number_to_string_loop_digits
drf r7 r5 ; get the current character and put it into r5
add r5 r5 r5 ; shift to left
add r5 r4 r5 ; add the carry
imm r6 x1
add r3 r6 r3 ; increment r3 (r6 = 1)
; determine if overflowed
imm r6 x10
and r5 r6 r4
jiz #number_to_string_loop_digits_overflow r4
imm r4 x1 ; if it overflowed, i.e. r4 (the overflow register) is NOT zero, set it to a 1.
@number_to_string_loop_digits_overflow
snd r5 r7 ; put the current character back/update it
xor r3 r2 r5 ; if r3 == r2, then r5 would be 0
jnz #number_to_string_loop_digits r5 ; therefore, loop while r3 != r2
; add 3 to each digit greater than five
imm
@number_to_string_loop_add
drf r7 r5 ; get the current character and put it into r5
; while it is greater than
xor r3 r2 r5 ; if r3 == r2, then r5 would be 0
jnz #number_to_string_loop_add r5 ; therefore, loop while r3 != r2
jnz #number_to_string_loop r0 ; While there are still bits in r0, again!
ret

BIN
out.rom Normal file

Binary file not shown.

7
pyproject.toml Normal file
View file

@ -0,0 +1,7 @@
[project]
name = "nc2-assembler"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []

98
test.ns Normal file
View file

@ -0,0 +1,98 @@
jmp #start
; ROM/RAM data would go here before start
@number x00 x00 x00 ; The last byte is to terminate it
@string "Hello world!" x0A x00 ; Newline and null terminated
@start
imm r0 xFF
imm r1 xFF
cal #full_ladder_multiply
put #number.1 r0 ; low byte
put #number.0 r1 ; high byte
imm r0 #string ; put the address of #string into r0
cal #print ; print it!
;imm r0 #number
;cal #print
jmp #loop ; limbo/endless loop
@multiply ; Multiply r1 by r2, putting the result in r0. NB! very slow for big values of r2
imm r7 x1 ; We use r7 to decrement r2
@multiply_round ; Increment and decrement
add r0 r1 r0 ; Add r1 to counter
sub r2 r7 r2 ; Decrement r2
jnz #multiply_round r2 ; If r2 is not zero, again!
ret
@full_ladder_multiply ; Multiply r0 by r1, putting the result in r0 (r1 ends up being r1*r2 + r1)
imm r6 x1 ; let r6 be 1. it is used as a mask to select the first bit
imm r7 x80 ; let r7 be 0b1000_0000. it is used as a bit mask for the current bit
; Move r1 into r5
imm r5 x0
add r1 r5 r5
@full_ladder_multiply_find_high_bit ; go through each bit from low to high. if we encounter a 1, we have found the highest bit.
and r7 r1 r3
shr r7 r6 r7
jiz #full_ladder_multiply_find_high_bit r3 ; r7 ends up being 2^(l-2), where the bit at l-1 is 1 and is the highest bit
imm r1 x0
add r0 r0 r2 ; r2 starts with the value of two r0
imm r3 x0
adc r3 r3 r3 ; put the carry into r3
@full_ladder_multiply_round
and r5 r7 r4 ; let r3 be r2 & r7. We check if the bit that r7 is masking is high on r2 later
shr r7 r6 r7 ; Shift r7 right by 1 (r6 = 1)
jnz #full_ladder_multiply_round_1 r4 ; if r4 is 1, we jump. otherwise
@full_ladder_multiply_round_0
add r0 r2 r2 ; Let r2 be r0 + r2
adc r1 r3 r3 ; carry
add r0 r0 r0 ; Double r0
adc r1 r1 r1 ; Double r1 and add carry
jmp #full_ladder_multiply_round_end ; Skip the second alternative
@full_ladder_multiply_round_1
add r0 r2 r0 ; Let r0 be r0 + r2
adc r1 r3 r1 ; carry
add r2 r2 r2 ; Double r0
adc r3 r3 r3 ; Double r1 and add carry
@full_ladder_multiply_round_end
jnz #full_ladder_multiply_round r7 ; if the mask still hasn't been shifted out (r7 != 0), do another round
ret
@print ; Print a string to output, given r0 as the address of a null-terminated string
imm r2 x1
@print_loop
drf r0 r1 ; get the value that r0 points to, and put it in r1
jiz #print_loop_end r1 ; if the value of r1 is 0, we have reached the end and should exit
put #output r1 ; output the character
add r0 r2 r0 ; increment the pointer (r2 = 1)
jmp #print_loop ; again!
@print_loop_end
ret
@loop ; Infinite loop
jmp #loop
:511 @output ; I/O address for serial out

8
uv.lock generated Normal file
View file

@ -0,0 +1,8 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "nc2-assembler"
version = "0.1.0"
source = { virtual = "." }