Compare commits

..

22 commits
v1.0 ... master

Author SHA1 Message Date
a714a9d71a guard against SET PC, [SP++], and SET PC, [--SP] 2012-04-14 15:41:20 +02:00
d638ebe7ea docstring 2012-04-14 15:39:42 +02:00
d95072ceb4 doc + cosmetic 2012-04-09 16:17:30 +02:00
bf055e998e Isolate memory from CPU 2012-04-09 16:16:22 +02:00
7d6205cac9 all registers wrapped as 16 bits 2012-04-09 15:58:44 +02:00
9a11985cb6 simplified register declaration 2012-04-09 15:40:40 +02:00
4d2d914712 log outside of valcodes 2012-04-09 15:32:51 +02:00
39ac76aa4f exception isolation 2012-04-09 15:32:14 +02:00
6f493aab4e cosmetic 2012-04-09 15:13:56 +02:00
605790257f refactored opcode dispatching 2012-04-09 15:13:15 +02:00
037559160f a little doc 2012-04-09 11:48:36 +02:00
2fff97dd64 run(), and HLT on infinite loop 2012-04-09 11:47:14 +02:00
50ab21f3e5 skip feedback 2012-04-09 11:30:21 +02:00
f3acea29fd fix param order to match doc 2012-04-09 11:27:06 +02:00
6fcac3cf2e title changes 2012-04-08 20:19:55 +02:00
c3e7012f2d Updated doc 2012-04-08 20:16:57 +02:00
54e56b82db updated doc and brought back spec demo 2012-04-08 20:01:41 +02:00
ec4711d2af ignore pyc 2012-04-08 19:58:53 +02:00
726698b4ea cosmetic 2012-04-08 19:58:35 +02:00
a2c029f9cb spec demo program as a test 2012-04-08 19:58:24 +02:00
32a827b728 cosmetic 2012-04-08 19:57:50 +02:00
5fa919e35b uniform naming 2012-04-08 19:57:02 +02:00
4 changed files with 255 additions and 150 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.pyc

View file

@ -2,19 +2,19 @@
A DCPU-16 implementation in Python. See [the spec][0]. A DCPU-16 implementation in Python. See [the spec][0].
# But what's the goal of this? There's another one already! # Goal
Well, what's wrong with taking another stab at it? Besides, I personnally felt it was too C-ish, and not pythonic enough (whatever that means, but you should [read][1] [this][2]). I wanted to revive whatever low-level (admittedly limited) ASM knowledge I had (from 6800/68000) and sharpen my Python-fu. The spirit of the thing is to be educative for everyone (including me). Many high-level implementations looked like C-in-other-language, so let's have a pythonic enough (whatever that means, but you should [read][1] [this][2]) implementation. The spirit of the thing is to be educative for everyone.
# How do I use this? # Usage
It's meant to be used interactively via the Python REPL as well as programmatically. I might implement a specific ASM REPL at some point. It's meant to be used interactively via the Python REPL as well as programmatically. A specific ASM REPL might be implemented at some point.
An example of a Python REPL session: An example of a Python REPL session:
>>> from dcpu_16 import CPU >>> from dcpu_16 import CPU, spec_demo
>>> c = CPU(debug=True) >>> c = CPU(debug=True)
>>> c.load_m() # loads demo program >>> c.load_m(spec_demo) # loads demo program
>>> c.step() # step by one instruction >>> c.step() # step by one instruction
<< SET << SET
<< c.r[0x0] << c.r[0x0]
@ -31,9 +31,9 @@ An example of a Python REPL session:
>>> c.dump_r() # get CPU register state as string >>> c.dump_r() # get CPU register state as string
'A=0000 B=0000 C=0000 X=0000 Y=0000 Z=0000 I=0000 J=0000 PC=0000 SP=0000 O=0000' 'A=0000 B=0000 C=0000 X=0000 Y=0000 Z=0000 I=0000 J=0000 PC=0000 SP=0000 O=0000'
# What is the status of this? # Status
It's not bug-free yet, the implementation itself is still a WIP and the whole of the spec example does not pass yet. But hey, that's what you get in a few late hours. Fixes coming, I promise. As of v1.0 the CPU implementation ought to be complete according to DCPU-16 spec v1.1.
# Features # Features

View file

@ -1,32 +1,48 @@
"""DCPU-16 implementation. (c) 2012, Loic Nageleisen
Spec: http://0x10c.com/doc/dcpu-16.txt
See LICENSE for licensing information.
"""
from __future__ import print_function, division from __future__ import print_function, division
# DCPU-16 spec version
spec = '1.1'
# log tool
def log(string): def log(string):
print(" << %s" % string) print(" << %s" % string)
#http://0x10c.com/doc/dcpu-16.txt # width constants
spec = '1.1'
w = 16 w = 16
wmask = 2**w - 1 wmask = 2**w - 1
literals = [l for l in xrange(0, 0x20)]
opcode_map = {}
valcode_map = {}
_opcode_map = {}
class opcode(object): class opcode(object):
"""Opcode decorator"""
def __init__(self, *opcode): def __init__(self, *opcode):
self.opcode = opcode self.opcode = opcode
def __call__(self, f): def __call__(self, f):
_opcode_f = f _opcode_f = f
_opcode_f.opcode = self.opcode _opcode_f.opcode = self.opcode
opcode_map[self.opcode] = _opcode_f if len(self.opcode) == 1:
_opcode_map[self.opcode[0]] = _opcode_f
else:
if not self.opcode[0] in _opcode_map.keys():
_opcode_map[self.opcode[0]] = {}
_opcode_map[self.opcode[0]][self.opcode[1]] = _opcode_f
return _opcode_f return _opcode_f
_valcode_map = {}
class valcode(object): class valcode(object):
"""Valcode decorator"""
def __init__(self, valcode): def __init__(self, valcode):
self.valcode = valcode self.valcode = valcode
@ -36,17 +52,58 @@ class valcode(object):
try: try:
for code in self.valcode: for code in self.valcode:
# currify with (bound) code # currify with (bound) code
valcode_map[code] = lambda c, code=code: _valcode_f(c, code) _valcode_map[code] = lambda c, code=code: _valcode_f(c, code)
valcode_map[code].__name__ = '%s(code=0x%02X)' % (_valcode_f.__name__, code) _valcode_map[code].__name__ = '%s(code=0x%02X)' % (_valcode_f.__name__, code)
except TypeError: except TypeError:
valcode_map[self.valcode] = _valcode_f _valcode_map[self.valcode] = _valcode_f
return _valcode_f return _valcode_f
class Register(object):
"""Register descriptor"""
def __init__(self, regcode=None):
self.regcode = regcode
def __get__(self, c, type=None):
if self.regcode is not None:
return c.r[self.regcode]
else:
return self.value
def __set__(self, c, value):
if self.regcode is not None:
c.r[self.regcode] = value & wmask
else:
self.value = value & wmask
def make_pointer(c, codestr):
"""creates a pointer func that evaluates codestr"""
def getter(c=c):
return eval("%s" % codestr)
def setter(data, c=c):
exec("%s = %r" % (codestr, data)) in {}, {'c': c, 'data': data}
pointer = getter
pointer.codestr = codestr
pointer.set = setter
return pointer
def pointerize(f):
"""wraps a function that generates a codestr to create a pointer"""
if f.func_code.co_argcount == 1:
ptrz = lambda c: make_pointer(c, f(c))
elif f.func_code.co_argcount == 2:
ptrz = lambda c, code: make_pointer(c, f(c, code))
else:
raise Exception('%s has too many arguments' % f.__name__)
ptrz.__name__ = 'ptr_to_%s' % f.__name__
ptrz.__doc__ = f.__doc__
return ptrz
@opcode(0x0, 0x01) @opcode(0x0, 0x01)
def JSR(c, a): def JSR(c, a):
"""pushes the address of the next instruction to the stack, then sets PC to a""" """pushes the address of the next instruction to the stack, then sets PC to a"""
c.sp = (c.sp - 1) & wmask c.sp -= 1
c.m[c.sp] = c.pc c.m[c.sp] = c.pc
c.pc = a() c.pc = a()
@ -164,35 +221,11 @@ def IFB(c, a, b):
c.skip = True c.skip = True
def make_pointer(c, codestr):
"""creates a pointer func that evaluates codestr"""
def getter(c=c):
return eval("%s" % codestr)
def setter(data, c=c):
exec("%s = %r" % (codestr, data)) in {}, {'c': c, 'data': data}
pointer = getter
pointer.codestr = codestr
pointer.set = setter
return pointer
def pointerize(f):
"""wraps a function that generates a codestr to create a pointer"""
if f.func_code.co_argcount == 1:
ptrz = lambda c: make_pointer(c, f(c))
elif f.func_code.co_argcount == 2:
ptrz = lambda c, code: make_pointer(c, f(c, code))
else:
raise Exception('%s has too many arguments' % f.__name__)
ptrz.__name__ = 'ptr_to_%s' % f.__name__
ptrz.__doc__ = f.__doc__
return ptrz
@valcode(range(0x00, 0x08)) @valcode(range(0x00, 0x08))
@pointerize @pointerize
def register(c, code): def register(c, code):
"""register""" """register"""
v = "c.r[0x%01X]" % code v = "c.r[0x%01X]" % code
if c.debug: log(v)
return v return v
@valcode(range(0x08, 0x10)) @valcode(range(0x08, 0x10))
@ -200,7 +233,6 @@ def register(c, code):
def register_value(c, code): def register_value(c, code):
"""[register]""" """[register]"""
v = "c.m[c.r[0x%01X]]" % (code-0x07) v = "c.m[c.r[0x%01X]]" % (code-0x07)
if c.debug: log(v)
return v return v
@valcode(range(0x10, 0x18)) @valcode(range(0x10, 0x18))
@ -208,8 +240,7 @@ def register_value(c, code):
def next_word_plus_register_value(c, code): def next_word_plus_register_value(c, code):
"""[next word + register]""" """[next word + register]"""
v = "c.m[0x%04X + c.r[0x%01X]]" % (c.m[c.pc], code-0x0F) v = "c.m[0x%04X + c.r[0x%01X]]" % (c.m[c.pc], code-0x0F)
if c.debug: log(v) c.pc += 1
c.pc = (c.pc + 1) & wmask
return v return v
@valcode(0x18) @valcode(0x18)
@ -217,8 +248,7 @@ def next_word_plus_register_value(c, code):
def pop(c): def pop(c):
"""POP / [SP++]""" """POP / [SP++]"""
v = "c.m[0x%04X]" % c.sp v = "c.m[0x%04X]" % c.sp
if c.debug: log(v) c.sp += 1
c.sp = (c.sp + 1) & wmask
return v return v
@valcode(0x19) @valcode(0x19)
@ -226,16 +256,14 @@ def pop(c):
def peek(c): def peek(c):
"""PEEK / [SP]""" """PEEK / [SP]"""
v = "c.m[0x%04X]" % c.sp v = "c.m[0x%04X]" % c.sp
if c.debug: log(v)
return v return v
@valcode(0x1A) @valcode(0x1A)
@pointerize @pointerize
def push(c): def push(c):
"""PUSH / [--SP]""" """PUSH / [--SP]"""
c.sp = (c.sp - 1) & wmask c.sp -= 1
v = "c.m[0x%04X]" % c.sp v = "c.m[0x%04X]" % c.sp
if c.debug: log(v)
return v return v
@valcode(0x1B) @valcode(0x1B)
@ -243,7 +271,6 @@ def push(c):
def stack_pointer(c): def stack_pointer(c):
"""stack pointer""" """stack pointer"""
v = "c.sp" v = "c.sp"
if c.debug: log(v)
return v return v
@valcode(0x1C) @valcode(0x1C)
@ -251,7 +278,6 @@ def stack_pointer(c):
def program_counter(c): def program_counter(c):
"""program counter""" """program counter"""
v = "c.pc" v = "c.pc"
if c.debug: log(v)
return v return v
@valcode(0x1D) @valcode(0x1D)
@ -259,7 +285,6 @@ def program_counter(c):
def overflow(c): def overflow(c):
"""overflow""" """overflow"""
v = "c.o" v = "c.o"
if c.debug: log(v)
return v return v
@valcode(0x1E) @valcode(0x1E)
@ -267,8 +292,7 @@ def overflow(c):
def next_word_value(c): def next_word_value(c):
"""[next_word]""" """[next_word]"""
v = "c.m[0x%04X]" % c.m[c.pc] v = "c.m[0x%04X]" % c.m[c.pc]
c.pc = (c.pc + 1) & wmask c.pc += 1
if c.debug: log(v)
return v return v
@valcode(0x1F) @valcode(0x1F)
@ -276,8 +300,7 @@ def next_word_value(c):
def next_word(c): def next_word(c):
"""next_word (literal)""" """next_word (literal)"""
v = "c.m[0x%04X]" % c.pc v = "c.m[0x%04X]" % c.pc
c.pc = (c.pc + 1) & wmask c.pc += 1
if c.debug: log(v)
return v return v
@valcode(range(0x20, 0x40)) @valcode(range(0x20, 0x40))
@ -285,13 +308,34 @@ def next_word(c):
def literal(c, code): def literal(c, code):
"""literal value 0x00-0x1F (literal)""" """literal value 0x00-0x1F (literal)"""
v = "0x%04X" % (code - 0x20) v = "0x%04X" % (code - 0x20)
if c.debug: log(v)
return v return v
class Memory(object):
"""array of 16-bit words"""
def __init__(m, debug=False):
m.clear()
def clear(m):
"""clear memory"""
# TODO: (addr, len) memory range to clear
m.w = [0 for _ in xrange(0, 2**w)]
def __getitem__(m, addr):
"""get word at addr"""
return m.w[addr]
def __setitem__(m, addr, value):
"""assignment truncates values to 16-bit words"""
# TODO: multi-word assignment starting at addr when value is list
m.w[addr] = value & wmask
class CPU(object): class CPU(object):
def __init__(c, debug=False): """DCPU-16"""
def __init__(c, memory=Memory(), debug=False):
"""If you don't specify memory, CPUs will share the same, default memory object"""
c.m = memory
c.clear() c.clear()
c.reset() c.reset()
c.debug = debug c.debug = debug
@ -306,107 +350,78 @@ class CPU(object):
def clear(c): def clear(c):
"""clear memory""" """clear memory"""
c.m = [0 for _ in xrange(0, 2**w)] c.m.clear()
@property
def a(c):
return c.r[0]
@a.setter
def a(c, val):
c.r[0] = val
@property
def b(c):
return c.r[1]
@b.setter
def b(c, val):
c.r[1] = val
@property
def c(c):
return c.r[2]
@c.setter
def c(c, val):
c.r[2] = val
@property
def x(c):
return c.r[3]
@x.setter
def x(c, val):
c.r[3] = val
@property
def y(c):
return c.r[4]
@y.setter
def y(c, val):
c.r[4] = val
@property
def z(c):
return c.r[5]
@z.setter
def z(c, val):
c.r[5] = val
@property
def i(c):
return c.r[6]
@i.setter
def i(c, val):
c.r[6] = val
@property
def j(c):
return c.r[7]
@j.setter
def j(c, val):
c.r[7] = val
a = Register(0x0)
b = Register(0x1)
c = Register(0x2)
x = Register(0x3)
y = Register(0x4)
z = Register(0x5)
i = Register(0x6)
j = Register(0x7)
pc = Register()
sp = Register()
o = Register()
def _op(c, word):
"""dispatch word to op and args"""
opcode = word & 0xF
a_code = word >> 4 & 0x3F
b_code = word >> 10 & 0x3F
try:
op = _opcode_map[opcode]
try:
op = op[a_code]
except TypeError:
args = (c._pointer(a_code),
c._pointer(b_code))
else:
args = (c._pointer(b_code),)
finally:
if c.debug:
log(' '.join([op.__name__] + [arg.codestr for arg in args]))
except KeyError:
raise Exception('Invalid opcode %s at PC=%04X' % (["%02X"%x for x in opcode], c.pc))
return op, args
def _pointer(c, code): def _pointer(c, code):
"""get pointer to value code""" """get pointer to valcode"""
try: try:
return valcode_map[code](c) return _valcode_map[code](c)
except KeyError: except KeyError:
raise Exception("Invalid value code") raise Exception("Invalid valcode")
def __getitem__(c, code): def __getitem__(c, code):
"""get pointer to value""" """get value of valcode"""
return c._pointer(code)() return c._pointer(code)()
def __setitem__(c, code, value): def __setitem__(c, code, value):
"""set value at pointer""" """set value at valcode"""
c._pointer(code).set(value) c._pointer(code).set(value)
def step(c): def step(c):
"""start handling [PC]""" """start handling [PC]"""
word = c.m[c.pc] word = c.m[c.pc]
c.pc = (c.pc + 1) & wmask c.pc += 1
if (word & 0xF) > 0: op, args = c._op(word)
opcode = (word & 0xF,)
a = c._pointer(word >> 4 & 0x3F)
b = c._pointer(word >> 10 & 0x3F)
args = (a, b)
elif (word >> 4 & 0x3F) > 0:
opcode = (0x0, word >> 4 & 0x3F)
a = c._pointer(word >> 10 & 0x3F)
args = (a, )
else:
raise Exception('Invalid opcode %s at PC=%04X' % (["%02X"%x for x in opcode], c.pc))
try:
op = opcode_map[opcode]
if c.debug: log(op.__name__)
except KeyError:
raise Exception('Invalid opcode %s at PC=%04X' % (["%02X"%x for x in opcode], c.pc))
if c.skip: if c.skip:
c.skip = False c.skip = False
if c.debug: log("Skipped")
else: else:
op(c, *args) op(c, *args)
if c.debug: log(c.dump_r()) if c.debug: log(c.dump_r())
def run(c):
"""step until PC is constant"""
last_pc = 0xFFFF
last_sp = 0xFFFF
while c.pc != last_pc or c.sp != last_sp:
last_pc = c.pc
last_sp = c.sp
c.step()
log("Infinite loop")
def dump_r(c): def dump_r(c):
"""human-readable register status""" """human-readable register status"""
return " ".join( "%s=%04X" % return " ".join( "%s=%04X" %
@ -418,15 +433,9 @@ class CPU(object):
(c.r + [c.pc, c.sp, c.o])[i]) (c.r + [c.pc, c.sp, c.o])[i])
for i in range(11)) for i in range(11))
def load_m(c, io=None): def load_m(c, data=None, io=None):
"""load data in memory""" """load data in memory"""
# TODO: load from io object # TODO: load from io object
data = [
0x7c01, 0x0030, 0x7de1, 0x1000, 0x0020, 0x7803, 0x1000, 0xc00d,
0x7dc1, 0x001a, 0xa861, 0x7c01, 0x2000, 0x2161, 0x2000, 0x8463,
0x806d, 0x7dc1, 0x000d, 0x9031, 0x7c10, 0x0018, 0x7dc1, 0x001a,
0x9037, 0x61c1, 0x7dc1, 0x001a, 0x0000, 0x0000, 0x0000, 0x0000,
]
for i in xrange(len(data)): for i in xrange(len(data)):
c.m[i] = data[i] c.m[i] = data[i]
@ -436,3 +445,9 @@ class CPU(object):
pass pass
spec_demo = [
0x7c01, 0x0030, 0x7de1, 0x1000, 0x0020, 0x7803, 0x1000, 0xc00d,
0x7dc1, 0x001a, 0xa861, 0x7c01, 0x2000, 0x2161, 0x2000, 0x8463,
0x806d, 0x7dc1, 0x000d, 0x9031, 0x7c10, 0x0018, 0x7dc1, 0x001a,
0x9037, 0x61c1, 0x7dc1, 0x001a, 0x0000, 0x0000, 0x0000, 0x0000,
]

91
test.py
View file

@ -190,6 +190,7 @@ class TestInstructions(unittest.TestCase):
self.assertEqual(c.sp, 0xFFFF) self.assertEqual(c.sp, 0xFFFF)
self.assertEqual(c.m[0xFFFF], 0xBEEF) self.assertEqual(c.m[0xFFFF], 0xBEEF)
class TestCPU(unittest.TestCase): class TestCPU(unittest.TestCase):
"""CPU behavior""" """CPU behavior"""
@ -232,7 +233,95 @@ class TestCPU(unittest.TestCase):
for r in c.m: for r in c.m:
self.assertEqual(r, 0) self.assertEqual(r, 0)
class TestCPUWithPrograms(unittest.TestCase):
def setUP(self):
pass
def test_spec_demo(self):
c = CPU()
data = [
0x7c01, 0x0030, 0x7de1, 0x1000, 0x0020, 0x7803, 0x1000, 0xc00d,
0x7dc1, 0x001a, 0xa861, 0x7c01, 0x2000, 0x2161, 0x2000, 0x8463,
0x806d, 0x7dc1, 0x000d, 0x9031, 0x7c10, 0x0018, 0x7dc1, 0x001a,
0x9037, 0x61c1, 0x7dc1, 0x001a, 0x0000, 0x0000, 0x0000, 0x0000,
]
c.load_m(data=data)
self.assertEqual(c.pc, 0)
c.step() # SET A, 0x30
self.assertEqual(c.a, 0x30)
self.assertEqual(c.pc, 2)
c.step() # SET [0x1000], 0x20
self.assertEqual(c.m[0x1000], 0x20)
self.assertEqual(c.pc, 5)
c.step() # SUB A, [0x1000]
self.assertEqual(c.a, 0x10)
self.assertEqual(c.pc, 7)
c.step() # IFN A, 0x10
self.assertEqual(c.pc, 8)
self.assertEqual(c.skip, True)
c.step() # skip SET PC, crash
self.assertEqual(c.skip, False)
self.assertEqual(c.pc, 10)
c.step() # SET I, 10
self.assertEqual(c.i, 0x0A)
self.assertEqual(c.pc, 11)
c.step() # SET A, 0x2000
self.assertEqual(c.a, 0x2000)
for i in range(10, 0, -1):
self.assertEqual(c.pc, 13)
c.step() # SET [0x2000+I], [A]
self.assertEqual(c.m[0x2000+i], 0x0)
self.assertEqual(c.pc, 15)
c.step() # SUB I, 1
self.assertEqual(c.i, i-1)
self.assertEqual(c.pc, 16)
c.step() # IFN I, 0
self.assertEqual(c.skip, i-1==0)
self.assertEqual(c.pc, 17)
c.step() # SET PC, loop (with skip if c.i==0)
self.assertEqual(c.pc, 19)
c.step() # SET X, 0x4
self.assertEqual(c.x, 0x4)
self.assertEqual(c.pc, 20)
c.step() # JSR testsub
self.assertEqual(c.sp, 0xFFFF)
self.assertEqual(c.m[0xFFFF], 22)
self.assertEqual(c.pc, 24)
c.step() # SHL X, 4
self.assertEqual(c.x, 0x40)
self.assertEqual(c.pc, 25)
c.step() # SET PC, POP
self.assertEqual(c.pc, 22)
c.step() # SET PC, crash
self.assertEqual(c.pc, 26)
c.step() # SET PC, crash
self.assertEqual(c.pc, 26)
# endless loop
if __name__ == '__main__': if __name__ == '__main__':
suite = unittest.TestLoader().loadTestsFromNames(['test.TestInstructions', 'test.TestCPU']) cases = [
'test.TestInstructions',
'test.TestCPU',
'test.TestCPUWithPrograms'
]
suite = unittest.TestLoader().loadTestsFromNames(cases)
unittest.TextTestRunner(verbosity=2).run(suite) unittest.TextTestRunner(verbosity=2).run(suite)