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].
# 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:
>>> from dcpu_16 import CPU
>>> from dcpu_16 import CPU, spec_demo
>>> c = CPU(debug=True)
>>> c.load_m() # loads demo program
>>> c.load_m(spec_demo) # loads demo program
>>> c.step() # step by one instruction
<< SET
<< c.r[0x0]
@ -31,9 +31,9 @@ An example of a Python REPL session:
>>> 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'
# 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

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
# DCPU-16 spec version
spec = '1.1'
# log tool
def log(string):
print(" << %s" % string)
#http://0x10c.com/doc/dcpu-16.txt
spec = '1.1'
# width constants
w = 16
wmask = 2**w-1
literals = [l for l in xrange(0, 0x20)]
opcode_map = {}
valcode_map = {}
wmask = 2**w - 1
_opcode_map = {}
class opcode(object):
"""Opcode decorator"""
def __init__(self, *opcode):
self.opcode = opcode
def __call__(self, f):
_opcode_f = f
_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
_valcode_map = {}
class valcode(object):
"""Valcode decorator"""
def __init__(self, valcode):
self.valcode = valcode
@ -36,17 +52,58 @@ class valcode(object):
try:
for code in self.valcode:
# currify with (bound) 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] = lambda c, code=code: _valcode_f(c, code)
_valcode_map[code].__name__ = '%s(code=0x%02X)' % (_valcode_f.__name__, code)
except TypeError:
valcode_map[self.valcode] = _valcode_f
_valcode_map[self.valcode] = _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)
def JSR(c, 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.pc = a()
@ -164,35 +221,11 @@ def IFB(c, a, b):
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))
@pointerize
def register(c, code):
"""register"""
v = "c.r[0x%01X]" % code
if c.debug: log(v)
return v
@valcode(range(0x08, 0x10))
@ -200,7 +233,6 @@ def register(c, code):
def register_value(c, code):
"""[register]"""
v = "c.m[c.r[0x%01X]]" % (code-0x07)
if c.debug: log(v)
return v
@valcode(range(0x10, 0x18))
@ -208,8 +240,7 @@ def register_value(c, code):
def next_word_plus_register_value(c, code):
"""[next word + register]"""
v = "c.m[0x%04X + c.r[0x%01X]]" % (c.m[c.pc], code-0x0F)
if c.debug: log(v)
c.pc = (c.pc + 1) & wmask
c.pc += 1
return v
@valcode(0x18)
@ -217,8 +248,7 @@ def next_word_plus_register_value(c, code):
def pop(c):
"""POP / [SP++]"""
v = "c.m[0x%04X]" % c.sp
if c.debug: log(v)
c.sp = (c.sp + 1) & wmask
c.sp += 1
return v
@valcode(0x19)
@ -226,16 +256,14 @@ def pop(c):
def peek(c):
"""PEEK / [SP]"""
v = "c.m[0x%04X]" % c.sp
if c.debug: log(v)
return v
@valcode(0x1A)
@pointerize
def push(c):
"""PUSH / [--SP]"""
c.sp = (c.sp - 1) & wmask
c.sp -= 1
v = "c.m[0x%04X]" % c.sp
if c.debug: log(v)
return v
@valcode(0x1B)
@ -243,7 +271,6 @@ def push(c):
def stack_pointer(c):
"""stack pointer"""
v = "c.sp"
if c.debug: log(v)
return v
@valcode(0x1C)
@ -251,7 +278,6 @@ def stack_pointer(c):
def program_counter(c):
"""program counter"""
v = "c.pc"
if c.debug: log(v)
return v
@valcode(0x1D)
@ -259,7 +285,6 @@ def program_counter(c):
def overflow(c):
"""overflow"""
v = "c.o"
if c.debug: log(v)
return v
@valcode(0x1E)
@ -267,8 +292,7 @@ def overflow(c):
def next_word_value(c):
"""[next_word]"""
v = "c.m[0x%04X]" % c.m[c.pc]
c.pc = (c.pc + 1) & wmask
if c.debug: log(v)
c.pc += 1
return v
@valcode(0x1F)
@ -276,8 +300,7 @@ def next_word_value(c):
def next_word(c):
"""next_word (literal)"""
v = "c.m[0x%04X]" % c.pc
c.pc = (c.pc + 1) & wmask
if c.debug: log(v)
c.pc += 1
return v
@valcode(range(0x20, 0x40))
@ -285,13 +308,34 @@ def next_word(c):
def literal(c, code):
"""literal value 0x00-0x1F (literal)"""
v = "0x%04X" % (code - 0x20)
if c.debug: log(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):
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.reset()
c.debug = debug
@ -306,107 +350,78 @@ class CPU(object):
def clear(c):
"""clear memory"""
c.m = [0 for _ in xrange(0, 2**w)]
@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
c.m.clear()
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):
"""get pointer to value code"""
"""get pointer to valcode"""
try:
return valcode_map[code](c)
return _valcode_map[code](c)
except KeyError:
raise Exception("Invalid value code")
raise Exception("Invalid valcode")
def __getitem__(c, code):
"""get pointer to value"""
"""get value of valcode"""
return c._pointer(code)()
def __setitem__(c, code, value):
"""set value at pointer"""
"""set value at valcode"""
c._pointer(code).set(value)
def step(c):
"""start handling [PC]"""
word = c.m[c.pc]
c.pc = (c.pc + 1) & wmask
if (word & 0xF) > 0:
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))
c.pc += 1
op, args = c._op(word)
if c.skip:
c.skip = False
if c.debug: log("Skipped")
else:
op(c, *args)
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):
"""human-readable register status"""
return " ".join( "%s=%04X" %
@ -418,15 +433,9 @@ class CPU(object):
(c.r + [c.pc, c.sp, c.o])[i])
for i in range(11))
def load_m(c, io=None):
def load_m(c, data=None, io=None):
"""load data in memory"""
# 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)):
c.m[i] = data[i]
@ -436,3 +445,9 @@ class CPU(object):
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.m[0xFFFF], 0xBEEF)
class TestCPU(unittest.TestCase):
"""CPU behavior"""
@ -232,7 +233,95 @@ class TestCPU(unittest.TestCase):
for r in c.m:
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__':
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)