mirror of
https://github.com/catseye/SixtyPical.git
synced 2024-06-18 03:29:32 +00:00
commit
53074bc224
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
*.pyc
|
||||
vicerc
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
History of SixtyPical
|
||||
=====================
|
||||
|
||||
0.9
|
||||
---
|
||||
|
||||
* Add word (constant or memory location) to word memory location.
|
||||
* Add word to pointer (unchecked for now).
|
||||
* Added `word table` type.
|
||||
* Can `copy` from word storage location to word table and back.
|
||||
* A `vector` can name itself in its `inputs` and `outputs` or `trashes` sets.
|
||||
* Implementation: `--debug` shows some extra info during analysis.
|
||||
* Fixed bug where `copy`ing literal word into word storage used wrong endianness.
|
||||
* Fixed bug where every memory location was allocated 2 bytes of storage, regardless of type.
|
||||
* Tests: use https://github.com/tcarmelveilleux/dcc6502 to disassemble code for comparison.
|
||||
|
||||
0.8
|
||||
---
|
||||
|
||||
|
|
|
@ -22,9 +22,8 @@ based on common machine-language programming idioms, such as
|
|||
The reference implementation can execute, analyze, and compile SixtyPical
|
||||
programs to 6502 machine code.
|
||||
|
||||
It is a **work in progress**, currently at the **proof-of-concept** stage.
|
||||
|
||||
The current development version of SixtyPical is 0.8.
|
||||
SixtyPical is a work in progress. The current released version of SixtyPical
|
||||
is 0.9.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
@ -41,38 +40,55 @@ Documentation
|
|||
TODO
|
||||
----
|
||||
|
||||
### Add 16 bit values.
|
||||
### Demo game
|
||||
|
||||
I guess this means making `add` a bit more like `copy`.
|
||||
Finish the little demo "game" where you can move a block around the screen with
|
||||
the joystick (i.e. bring it up to par with the original demo game that was written
|
||||
for SixtyPical)
|
||||
|
||||
And then: add to pointer. (Not necessarily range-checked yet though.)
|
||||
### `call` routines that are defined further down in the source code
|
||||
|
||||
And then write a little demo "game" where you can move a block around the screen with
|
||||
the joystick.
|
||||
We might have a graph of states that refer to each other and that want to `goto`
|
||||
each other. Thus we need this. We have it for vectors, but we need it for `call`.
|
||||
|
||||
### `word table` and `vector table` types
|
||||
### Allow branches to diverge in what they touch
|
||||
|
||||
For example, if the routine inputs and outputs `foo`, and one branch of an `if`
|
||||
sets `foo` and the other does not touch it, that should be OK.
|
||||
|
||||
### `vector table` type
|
||||
|
||||
### `low` and `high` address operators
|
||||
|
||||
To turn `word` type into `byte`.
|
||||
|
||||
### save registers on stack
|
||||
### Save registers on stack
|
||||
|
||||
This preserves them, so semantically, they can be used even though they
|
||||
This preserves them, so that, semantically, they can be used later even though they
|
||||
are trashed inside the block.
|
||||
|
||||
### Range checking in the abstract interpretation
|
||||
|
||||
If you copy the address of a buffer (say it is size N) to a pointer, it is valid.
|
||||
If you add a value from 0 to N-1 to the pointer, it is still valid.
|
||||
But if you add a value ≥ N to it, it becomes invalid.
|
||||
This should be tracked in the abstract interpretation.
|
||||
(If only because abstract interpretation is the major point of this project!)
|
||||
|
||||
### And at some point...
|
||||
|
||||
* Compare word (constant or memory location) with memory location or pointer. (Maybe?)
|
||||
* `copy x, [ptr] + y`
|
||||
* Maybe even `copy [ptra] + y, [ptrb] + y`, which can be compiled to indirect LDA then indirect STA!
|
||||
* Check that the buffer being read or written to through pointer, appears in approporiate inputs or outputs set.
|
||||
* `byte table` and `word table` of sizes other than 256
|
||||
* initialized `byte table` memory locations
|
||||
* always analyze before executing or compiling, unless told not to
|
||||
* `trash` instruction.
|
||||
* `interrupt` routines.
|
||||
* 6502-mnemonic aliases (`sec`, `clc`)
|
||||
* other handy aliases (`eq` for `z`, etc.)
|
||||
* have `copy` instruction able to copy a constant to a user-def mem loc, etc.
|
||||
* `interrupt` routines -- to indicate that "the supervisor" has stored values on the stack, so we can trash them.
|
||||
* pre-initialized `word` variables
|
||||
* error messages that include the line number of the source code
|
||||
* have `copy` instruction able to copy a byte to a user-def mem loc, etc.
|
||||
* add absolute addressing in shl/shr, absolute-indexed for add, sub, etc.
|
||||
* check and disallow recursion.
|
||||
* automatic tail-call optimization (could be tricky, w/constraints?)
|
||||
|
|
|
@ -56,7 +56,7 @@ if __name__ == '__main__':
|
|||
|
||||
if options.analyze:
|
||||
try:
|
||||
analyzer = Analyzer()
|
||||
analyzer = Analyzer(debug=options.debug)
|
||||
analyzer.analyze_program(program)
|
||||
except Exception as e:
|
||||
if options.traceback:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
SixtyPical
|
||||
==========
|
||||
|
||||
This document describes the SixtyPical programming language version 0.8,
|
||||
This document describes the SixtyPical programming language version 0.9,
|
||||
both its execution aspect and its static analysis aspect (even though
|
||||
these are, technically speaking, separate concepts.)
|
||||
|
||||
|
@ -25,7 +25,8 @@ There are six *primitive types* in SixtyPical:
|
|||
|
||||
There are also two *type constructors*:
|
||||
|
||||
* X table (256 entries, each holding a value of type X, where X is `byte`)
|
||||
* T table (256 entries, each holding a value of type T, where T is either
|
||||
`byte` or `word`)
|
||||
* buffer[N] (N entries; each entry is a byte; N is a power of 2, ≤ 64K)
|
||||
|
||||
Memory locations
|
||||
|
@ -302,6 +303,12 @@ and initializing them afterwards.
|
|||
|
||||
dest and src continue to be initialized afterwards.
|
||||
|
||||
In addition, if dest is of `word` type, then src must also be of `word`
|
||||
type, and in this case this instruction trashes the `a` register.
|
||||
|
||||
NOTE: If dest is a pointer, the addition does not check if the result of
|
||||
the pointer arithmetic continues to be valid (within a buffer) or not.
|
||||
|
||||
### inc ###
|
||||
|
||||
inc <dest-memory-location>
|
||||
|
|
9
eg/add-word.60p
Normal file
9
eg/add-word.60p
Normal file
|
@ -0,0 +1,9 @@
|
|||
word score
|
||||
routine main
|
||||
inputs score
|
||||
outputs score
|
||||
trashes a, c, z, v, n
|
||||
{
|
||||
st off, c
|
||||
add score, 1999
|
||||
}
|
250
eg/proto-game.60p
Normal file
250
eg/proto-game.60p
Normal file
|
@ -0,0 +1,250 @@
|
|||
// ****************************
|
||||
// * Demo Game for SixtyPical *
|
||||
// ****************************
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// System Locations
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
byte vic_border @ 53280
|
||||
byte vic_bg @ 53281
|
||||
|
||||
byte table screen1 @ 1024
|
||||
byte table screen2 @ 1274
|
||||
byte table screen3 @ 1524
|
||||
byte table screen4 @ 1774
|
||||
|
||||
byte table colormap1 @ 55296
|
||||
byte table colormap2 @ 55546
|
||||
byte table colormap3 @ 55796
|
||||
byte table colormap4 @ 56046
|
||||
|
||||
buffer[2048] screen @ 1024
|
||||
byte joy2 @ $dc00
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Global Variables
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
pointer ptr @ 254
|
||||
word pos
|
||||
word delta
|
||||
byte button_down : 0 // effectively static-local to check_button
|
||||
|
||||
//
|
||||
// Points to the routine that implements the current game state.
|
||||
//
|
||||
|
||||
vector dispatch_game_state
|
||||
inputs joy2, pos, button_down, dispatch_game_state
|
||||
outputs delta, pos, screen, screen1, button_down, dispatch_game_state
|
||||
trashes a, x, y, c, z, n, v, ptr
|
||||
|
||||
//
|
||||
// The constraints on these 2 vectors are kind-of sort-of big fibs.
|
||||
// They're only written this way so they can be compatible with our
|
||||
// routine. In fact, CINV is an interrupt routine where it doesn't
|
||||
// really matter what you trash anyway, because all registers were
|
||||
/// saved by the caller (the KERNAL) and will be restored by the end
|
||||
// of the code of the saved origin cinv routine that we goto.
|
||||
//
|
||||
// I wonder if this could be arranged somehow to be less fibby, in
|
||||
// a future version of SixtyPical.
|
||||
//
|
||||
|
||||
vector cinv
|
||||
inputs joy2, pos, button_down, dispatch_game_state
|
||||
outputs delta, pos, screen, screen1, button_down, dispatch_game_state
|
||||
trashes a, x, y, c, z, n, v, ptr
|
||||
@ 788
|
||||
|
||||
vector save_cinv
|
||||
inputs joy2, pos, button_down, dispatch_game_state
|
||||
outputs delta, pos, screen, screen1, button_down, dispatch_game_state
|
||||
trashes a, x, y, c, z, n, v, ptr
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Utility Routines
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
routine read_stick
|
||||
inputs joy2
|
||||
outputs delta
|
||||
trashes a, x, z, n
|
||||
{
|
||||
ld x, joy2
|
||||
ld a, x
|
||||
and a, 1 // up
|
||||
if z {
|
||||
copy $ffd8, delta // -40
|
||||
} else {
|
||||
ld a, x
|
||||
and a, 2 // down
|
||||
if z {
|
||||
copy word 40, delta
|
||||
} else {
|
||||
ld a, x
|
||||
and a, 4 // left
|
||||
if z {
|
||||
copy $ffff, delta // -1
|
||||
} else {
|
||||
ld a, x
|
||||
and a, 8 // right
|
||||
if z {
|
||||
copy word 1, delta
|
||||
} else {
|
||||
copy word 0, delta
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// You can repeatedly (i.e. as part of actor logic or an IRQ handler)
|
||||
// call this routine.
|
||||
// Upon return, if carry is set, the button was pressed then released.
|
||||
|
||||
routine check_button
|
||||
inputs joy2, button_down
|
||||
outputs c, button_down
|
||||
trashes a, z, n
|
||||
{
|
||||
ld a, button_down
|
||||
if z {
|
||||
ld a, joy2
|
||||
and a, $10
|
||||
if z {
|
||||
ld a, 1
|
||||
st a, button_down
|
||||
}
|
||||
st off, c
|
||||
} else {
|
||||
ld a, joy2
|
||||
and a, $10
|
||||
if not z {
|
||||
ld a, 0
|
||||
st a, button_down
|
||||
st on, c
|
||||
} else {
|
||||
st off, c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
routine clear_screen
|
||||
outputs screen1, screen2, screen3, screen4, colormap1, colormap2, colormap3, colormap4
|
||||
trashes a, y, c, n, z
|
||||
{
|
||||
ld y, 0
|
||||
repeat {
|
||||
ld a, 1
|
||||
st a, colormap1 + y
|
||||
st a, colormap2 + y
|
||||
st a, colormap3 + y
|
||||
st a, colormap4 + y
|
||||
|
||||
ld a, 32
|
||||
st a, screen1 + y
|
||||
st a, screen2 + y
|
||||
st a, screen3 + y
|
||||
st a, screen4 + y
|
||||
|
||||
inc y
|
||||
cmp y, 250
|
||||
} until z
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Game States
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
//
|
||||
// Because these all `goto save_cinv` at the end, they must have the same signature as that routine.
|
||||
//
|
||||
|
||||
routine game_state_play
|
||||
inputs joy2, pos, button_down, dispatch_game_state
|
||||
outputs delta, pos, screen, screen1, button_down, dispatch_game_state
|
||||
trashes a, x, y, c, z, n, v, ptr
|
||||
{
|
||||
call read_stick
|
||||
|
||||
st off, c
|
||||
add pos, delta
|
||||
|
||||
copy ^screen, ptr
|
||||
st off, c
|
||||
add ptr, pos
|
||||
|
||||
ld y, 0
|
||||
copy 81, [ptr] + y
|
||||
|
||||
goto save_cinv
|
||||
}
|
||||
|
||||
routine game_state_title_screen
|
||||
inputs joy2, pos, button_down, dispatch_game_state
|
||||
outputs delta, pos, screen, screen1, button_down, dispatch_game_state
|
||||
trashes a, x, y, c, z, n, v, ptr
|
||||
{
|
||||
ld y, 10
|
||||
repeat {
|
||||
ld a, 90
|
||||
st a, screen1 + y
|
||||
inc y
|
||||
cmp y, 20
|
||||
} until z
|
||||
|
||||
st off, c
|
||||
call check_button
|
||||
|
||||
if c {
|
||||
// call clear_screen
|
||||
// call init_game
|
||||
copy game_state_play, dispatch_game_state
|
||||
} else {
|
||||
// This is sort of a hack. FIXME: let `if` branches diverge this much.
|
||||
copy dispatch_game_state, dispatch_game_state
|
||||
}
|
||||
|
||||
goto save_cinv
|
||||
}
|
||||
|
||||
// *************************
|
||||
// * Main Game Loop Driver *
|
||||
// *************************
|
||||
|
||||
routine our_cinv
|
||||
inputs joy2, pos, button_down, dispatch_game_state
|
||||
outputs delta, pos, screen, screen1, button_down, dispatch_game_state
|
||||
trashes a, x, y, c, z, n, v, ptr
|
||||
{
|
||||
goto dispatch_game_state
|
||||
}
|
||||
|
||||
routine main
|
||||
inputs cinv
|
||||
outputs cinv, save_cinv, pos, dispatch_game_state,
|
||||
screen1, screen2, screen3, screen4, colormap1, colormap2, colormap3, colormap4
|
||||
trashes a, y, n, c, z, vic_border, vic_bg
|
||||
{
|
||||
ld a, 5
|
||||
st a, vic_border
|
||||
ld a, 0
|
||||
st a, vic_bg
|
||||
ld y, 0
|
||||
|
||||
call clear_screen
|
||||
|
||||
copy game_state_title_screen, dispatch_game_state
|
||||
|
||||
copy word 0, pos
|
||||
with interrupts off {
|
||||
copy cinv, save_cinv
|
||||
copy our_cinv, cinv
|
||||
}
|
||||
// FIXME: find out why `repeat { } forever` does not analyze OK
|
||||
repeat {
|
||||
ld a, 0
|
||||
} until not z
|
||||
}
|
16
eg/word-table.60p
Normal file
16
eg/word-table.60p
Normal file
|
@ -0,0 +1,16 @@
|
|||
word one
|
||||
word table many
|
||||
|
||||
routine main
|
||||
inputs one, many
|
||||
outputs one, many
|
||||
trashes a, x, y, n, z
|
||||
{
|
||||
ld x, 0
|
||||
ld y, 0
|
||||
copy 777, one
|
||||
copy one, many + x
|
||||
copy one, many + y
|
||||
copy many + x, one
|
||||
copy many + y, one
|
||||
}
|
12
loadngo.sh
12
loadngo.sh
|
@ -1,7 +1,15 @@
|
|||
#!/bin/sh
|
||||
|
||||
SRC=$1
|
||||
if [ "X$1" = "X" ]; then
|
||||
echo "Usage: ./loadngo.sh <source.60p>"
|
||||
exit 1
|
||||
fi
|
||||
OUT=/tmp/a-out.prg
|
||||
bin/sixtypical --analyze --compile --basic-prelude $SRC > $OUT || exit 1
|
||||
x64 $OUT
|
||||
bin/sixtypical --traceback --analyze --compile --basic-prelude $SRC > $OUT || exit 1
|
||||
if [ -e vicerc ]; then
|
||||
x64 -config vicerc $OUT
|
||||
else
|
||||
x64 $OUT
|
||||
fi
|
||||
rm -f $OUT
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
from sixtypical.ast import Program, Routine, Block, Instr
|
||||
from sixtypical.model import (
|
||||
TYPE_BYTE, TYPE_BYTE_TABLE, BufferType, PointerType, VectorType, ExecutableType,
|
||||
ConstantRef, LocationRef, IndirectRef, AddressRef,
|
||||
TYPE_BYTE, TYPE_WORD, TYPE_BYTE_TABLE, TYPE_WORD_TABLE, BufferType, PointerType, VectorType, ExecutableType,
|
||||
ConstantRef, LocationRef, IndirectRef, IndexedRef, AddressRef,
|
||||
REG_A, REG_Y, FLAG_Z, FLAG_N, FLAG_V, FLAG_C
|
||||
)
|
||||
|
||||
|
@ -88,6 +88,20 @@ class Context(object):
|
|||
raise InconsistentConstraintsError('%s in %s' % (ref.name, routine.name))
|
||||
self._writeable.add(ref)
|
||||
|
||||
def __str__(self):
|
||||
def locstr(loc):
|
||||
if isinstance(loc, LocationRef):
|
||||
return "{}:{}".format(loc.name, loc.type)
|
||||
else:
|
||||
return str(loc)
|
||||
|
||||
def locsetstr(s):
|
||||
return '{' + ', '.join([locstr(loc) for loc in list(s)]) + '}'
|
||||
|
||||
return "Context(\n _touched={},\n _meaningful={},\n _writeable={}\n)".format(
|
||||
locsetstr(self._touched), locsetstr(self._meaningful), locsetstr(self._writeable)
|
||||
)
|
||||
|
||||
def clone(self):
|
||||
c = Context(self.routines, self.routine, [], [], [])
|
||||
c._touched = set(self._touched)
|
||||
|
@ -157,10 +171,11 @@ class Context(object):
|
|||
|
||||
class Analyzer(object):
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, debug=False):
|
||||
self.current_routine = None
|
||||
self.has_encountered_goto = False
|
||||
self.routines = {}
|
||||
self.debug = debug
|
||||
|
||||
def assert_type(self, type, *locations):
|
||||
for location in locations:
|
||||
|
@ -169,6 +184,26 @@ class Analyzer(object):
|
|||
(location.name, self.current_routine.name)
|
||||
)
|
||||
|
||||
def assert_affected_within(self, name, affected, limited_to):
|
||||
# We reduce the set of LocationRefs to a set of strings (their labels).
|
||||
# This is necessary because currently, two LocationRefs that refer to the
|
||||
# same location are not considered euqal. (But two LocationRefs with the
|
||||
# same label should always be the same type.)
|
||||
|
||||
affected = set([loc.name for loc in affected])
|
||||
limited_to = set([loc.name for loc in limited_to])
|
||||
|
||||
def loc_list(label_set):
|
||||
return ', '.join(sorted(label_set))
|
||||
|
||||
overage = affected - limited_to
|
||||
if not overage:
|
||||
return
|
||||
message = 'in %s: %s are {%s} but affects {%s} which exceeds by: {%s} ' % (
|
||||
self.current_routine.name, name, loc_list(limited_to), loc_list(affected), loc_list(overage)
|
||||
)
|
||||
raise IncompatibleConstraintsError(message)
|
||||
|
||||
def analyze_program(self, program):
|
||||
assert isinstance(program, Program)
|
||||
self.routines = {r.location: r for r in program.routines}
|
||||
|
@ -184,7 +219,13 @@ class Analyzer(object):
|
|||
return
|
||||
type = routine.location.type
|
||||
context = Context(self.routines, routine, type.inputs, type.outputs, type.trashes)
|
||||
if self.debug:
|
||||
print "at start of routine `{}`:".format(routine.name)
|
||||
print context
|
||||
self.analyze_block(routine.block, context)
|
||||
if self.debug:
|
||||
print "at end of routine `{}`:".format(routine.name)
|
||||
print context
|
||||
if not self.has_encountered_goto:
|
||||
for ref in type.outputs:
|
||||
context.assert_meaningful(ref, exception_class=UnmeaningfulOutputError)
|
||||
|
@ -233,7 +274,24 @@ class Analyzer(object):
|
|||
)
|
||||
context.assert_meaningful(src)
|
||||
context.set_written(dest)
|
||||
elif opcode in ('add', 'sub'):
|
||||
elif opcode == 'add':
|
||||
context.assert_meaningful(src, dest, FLAG_C)
|
||||
if src.type == TYPE_BYTE:
|
||||
self.assert_type(TYPE_BYTE, src, dest)
|
||||
context.set_written(dest, FLAG_Z, FLAG_N, FLAG_C, FLAG_V)
|
||||
else:
|
||||
self.assert_type(TYPE_WORD, src)
|
||||
if dest.type == TYPE_WORD:
|
||||
context.set_written(dest, FLAG_Z, FLAG_N, FLAG_C, FLAG_V)
|
||||
context.set_touched(REG_A)
|
||||
context.set_unmeaningful(REG_A)
|
||||
elif isinstance(dest.type, PointerType):
|
||||
context.set_written(dest, FLAG_Z, FLAG_N, FLAG_C, FLAG_V)
|
||||
context.set_touched(REG_A)
|
||||
context.set_unmeaningful(REG_A)
|
||||
else:
|
||||
self.assert_type(TYPE_WORD, dest)
|
||||
elif opcode == 'sub':
|
||||
self.assert_type(TYPE_BYTE, src, dest)
|
||||
context.assert_meaningful(src, dest, FLAG_C)
|
||||
context.set_written(dest, FLAG_Z, FLAG_N, FLAG_C, FLAG_V)
|
||||
|
@ -310,19 +368,26 @@ class Analyzer(object):
|
|||
pass
|
||||
else:
|
||||
raise TypeMismatchError((src, dest))
|
||||
|
||||
elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef):
|
||||
if src.type == TYPE_WORD and dest.ref.type == TYPE_WORD_TABLE:
|
||||
pass
|
||||
else:
|
||||
raise TypeMismatchError((src, dest))
|
||||
|
||||
elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef):
|
||||
if src.ref.type == TYPE_WORD_TABLE and dest.type == TYPE_WORD:
|
||||
pass
|
||||
else:
|
||||
raise TypeMismatchError((src, dest))
|
||||
|
||||
elif isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, LocationRef):
|
||||
if src.type == dest.type:
|
||||
pass
|
||||
elif isinstance(src.type, ExecutableType) and \
|
||||
isinstance(dest.type, VectorType):
|
||||
# if dealing with routines and vectors,
|
||||
# check that they're not incompatible
|
||||
if not (src.type.inputs <= dest.type.inputs):
|
||||
raise IncompatibleConstraintsError(src.type.inputs - dest.type.inputs)
|
||||
if not (src.type.outputs <= dest.type.outputs):
|
||||
raise IncompatibleConstraintsError(src.type.outputs - dest.type.outputs)
|
||||
if not (src.type.trashes <= dest.type.trashes):
|
||||
raise IncompatibleConstraintsError(src.type.trashes - dest.type.trashes)
|
||||
elif isinstance(src.type, ExecutableType) and isinstance(dest.type, VectorType):
|
||||
self.assert_affected_within('inputs', src.type.inputs, dest.type.inputs)
|
||||
self.assert_affected_within('outputs', src.type.outputs, dest.type.outputs)
|
||||
self.assert_affected_within('trashes', src.type.trashes, dest.type.trashes)
|
||||
else:
|
||||
raise TypeMismatchError((src, dest))
|
||||
else:
|
||||
|
@ -337,7 +402,15 @@ class Analyzer(object):
|
|||
elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef):
|
||||
context.assert_meaningful(src.ref, REG_Y)
|
||||
# TODO this will need to be more sophisticated. the thing ref points to is touched, as well.
|
||||
context.set_touched(src.ref)
|
||||
context.set_touched(src.ref) # TODO and REG_Y? if not, why not?
|
||||
context.set_written(dest)
|
||||
elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef):
|
||||
context.assert_meaningful(src, dest.ref, dest.index)
|
||||
context.set_touched(src) # TODO and dest.index?
|
||||
context.set_written(dest.ref)
|
||||
elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef):
|
||||
context.assert_meaningful(src.ref, src.index, dest)
|
||||
context.set_touched(dest) # TODO and src.index?
|
||||
context.set_written(dest)
|
||||
else:
|
||||
context.assert_meaningful(src)
|
||||
|
@ -350,22 +423,21 @@ class Analyzer(object):
|
|||
self.analyze_block(instr.block, context)
|
||||
elif opcode == 'goto':
|
||||
location = instr.location
|
||||
type = location.type
|
||||
type_ = location.type
|
||||
|
||||
if not isinstance(type, ExecutableType):
|
||||
if not isinstance(type_, ExecutableType):
|
||||
raise TypeMismatchError(location)
|
||||
|
||||
# assert that the dest routine's inputs are all initialized
|
||||
for ref in type.inputs:
|
||||
for ref in type_.inputs:
|
||||
context.assert_meaningful(ref)
|
||||
|
||||
# and that this routine's trashes and output constraints are a
|
||||
# superset of the called routine's
|
||||
current_type = self.current_routine.location.type
|
||||
if not (type.outputs <= current_type.outputs):
|
||||
raise IncompatibleConstraintsError(type.outputs - current_type.outputs)
|
||||
if not (type.trashes <= current_type.trashes):
|
||||
raise IncompatibleConstraintsError(type.trashes - current_type.trashes)
|
||||
self.assert_affected_within('outputs', type_.outputs, current_type.outputs)
|
||||
self.assert_affected_within('trashes', type_.trashes, current_type.trashes)
|
||||
|
||||
self.has_encountered_goto = True
|
||||
else:
|
||||
raise NotImplementedError(opcode)
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
from sixtypical.ast import Program, Routine, Block, Instr
|
||||
from sixtypical.model import (
|
||||
ConstantRef, LocationRef, IndirectRef, AddressRef,
|
||||
TYPE_BIT, TYPE_BYTE, TYPE_WORD, BufferType, PointerType, RoutineType, VectorType,
|
||||
ConstantRef, LocationRef, IndexedRef, IndirectRef, AddressRef,
|
||||
TYPE_BIT, TYPE_BYTE, TYPE_BYTE_TABLE, TYPE_WORD, TYPE_WORD_TABLE, BufferType, PointerType, RoutineType, VectorType,
|
||||
REG_A, REG_X, REG_Y, FLAG_C
|
||||
)
|
||||
from sixtypical.emitter import Byte, Label, Offset, LowAddressByte, HighAddressByte
|
||||
|
@ -35,10 +35,22 @@ class Compiler(object):
|
|||
assert isinstance(program, Program)
|
||||
|
||||
for defn in program.defns:
|
||||
label = Label(defn.name)
|
||||
if defn.addr is not None:
|
||||
label.set_addr(defn.addr)
|
||||
self.labels[defn.name] = label
|
||||
# compute length of memory pointed to. this is awful.
|
||||
length = None
|
||||
type_ = defn.location.type
|
||||
if type_ == TYPE_BYTE:
|
||||
length = 1
|
||||
elif type_ == TYPE_WORD or isinstance(type_, (PointerType, VectorType)):
|
||||
length = 2
|
||||
elif type_ == TYPE_BYTE_TABLE:
|
||||
length = 256
|
||||
elif type_ == TYPE_WORD_TABLE:
|
||||
length = 512
|
||||
elif isinstance(type_, BufferType):
|
||||
length = type_.size
|
||||
if length is None:
|
||||
raise NotImplementedError("Need size for type {}".format(type_))
|
||||
self.labels[defn.name] = Label(defn.name, addr=defn.addr, length=length)
|
||||
|
||||
for routine in program.routines:
|
||||
self.routines[routine.name] = routine
|
||||
|
@ -60,8 +72,10 @@ class Compiler(object):
|
|||
for defn in program.defns:
|
||||
if defn.initial is not None:
|
||||
label = self.labels[defn.name]
|
||||
initial_data = Byte(defn.initial) # TODO: support other types than Byte
|
||||
label.set_length(initial_data.size())
|
||||
self.emitter.resolve_label(label)
|
||||
self.emitter.emit(Byte(defn.initial))
|
||||
self.emitter.emit(initial_data)
|
||||
|
||||
# uninitialized, "BSS" data
|
||||
for defn in program.defns:
|
||||
|
@ -147,6 +161,46 @@ class Compiler(object):
|
|||
self.emitter.emit(ADC(Immediate(Byte(src.value))))
|
||||
else:
|
||||
self.emitter.emit(ADC(Absolute(self.labels[src.name])))
|
||||
elif isinstance(dest, LocationRef) and src.type == TYPE_WORD and dest.type == TYPE_WORD:
|
||||
if isinstance(src, ConstantRef):
|
||||
dest_label = self.labels[dest.name]
|
||||
self.emitter.emit(LDA(Absolute(dest_label)))
|
||||
self.emitter.emit(ADC(Immediate(Byte(src.low_byte()))))
|
||||
self.emitter.emit(STA(Absolute(dest_label)))
|
||||
self.emitter.emit(LDA(Absolute(Offset(dest_label, 1))))
|
||||
self.emitter.emit(ADC(Immediate(Byte(src.high_byte()))))
|
||||
self.emitter.emit(STA(Absolute(Offset(dest_label, 1))))
|
||||
elif isinstance(src, LocationRef):
|
||||
src_label = self.labels[src.name]
|
||||
dest_label = self.labels[dest.name]
|
||||
self.emitter.emit(LDA(Absolute(dest_label)))
|
||||
self.emitter.emit(ADC(Absolute(src_label)))
|
||||
self.emitter.emit(STA(Absolute(dest_label)))
|
||||
self.emitter.emit(LDA(Absolute(Offset(dest_label, 1))))
|
||||
self.emitter.emit(ADC(Absolute(Offset(src_label, 1))))
|
||||
self.emitter.emit(STA(Absolute(Offset(dest_label, 1))))
|
||||
else:
|
||||
raise UnsupportedOpcodeError(instr)
|
||||
elif isinstance(dest, LocationRef) and src.type == TYPE_WORD and isinstance(dest.type, PointerType):
|
||||
if isinstance(src, ConstantRef):
|
||||
dest_label = self.labels[dest.name]
|
||||
self.emitter.emit(LDA(ZeroPage(dest_label)))
|
||||
self.emitter.emit(ADC(Immediate(Byte(src.low_byte()))))
|
||||
self.emitter.emit(STA(ZeroPage(dest_label)))
|
||||
self.emitter.emit(LDA(ZeroPage(Offset(dest_label, 1))))
|
||||
self.emitter.emit(ADC(Immediate(Byte(src.high_byte()))))
|
||||
self.emitter.emit(STA(ZeroPage(Offset(dest_label, 1))))
|
||||
elif isinstance(src, LocationRef):
|
||||
src_label = self.labels[src.name]
|
||||
dest_label = self.labels[dest.name]
|
||||
self.emitter.emit(LDA(ZeroPage(dest_label)))
|
||||
self.emitter.emit(ADC(Absolute(src_label)))
|
||||
self.emitter.emit(STA(ZeroPage(dest_label)))
|
||||
self.emitter.emit(LDA(ZeroPage(Offset(dest_label, 1))))
|
||||
self.emitter.emit(ADC(Absolute(Offset(src_label, 1))))
|
||||
self.emitter.emit(STA(ZeroPage(Offset(dest_label, 1))))
|
||||
else:
|
||||
raise UnsupportedOpcodeError(instr)
|
||||
else:
|
||||
raise UnsupportedOpcodeError(instr)
|
||||
elif opcode == 'sub':
|
||||
|
@ -305,6 +359,41 @@ class Compiler(object):
|
|||
self.emitter.emit(STA(ZeroPage(dest_label)))
|
||||
self.emitter.emit(LDA(Immediate(LowAddressByte(src_label))))
|
||||
self.emitter.emit(STA(ZeroPage(Offset(dest_label, 1))))
|
||||
elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef):
|
||||
if src.type == TYPE_WORD and dest.ref.type == TYPE_WORD_TABLE:
|
||||
src_label = self.labels[src.name]
|
||||
dest_label = self.labels[dest.ref.name]
|
||||
addressing_mode = None
|
||||
if dest.index == REG_X:
|
||||
addressing_mode = AbsoluteX
|
||||
elif dest.index == REG_Y:
|
||||
addressing_mode = AbsoluteY
|
||||
else:
|
||||
raise NotImplementedError(dest)
|
||||
self.emitter.emit(LDA(Absolute(src_label)))
|
||||
self.emitter.emit(STA(addressing_mode(dest_label)))
|
||||
self.emitter.emit(LDA(Absolute(Offset(src_label, 1))))
|
||||
self.emitter.emit(STA(addressing_mode(Offset(dest_label, 256))))
|
||||
else:
|
||||
raise NotImplementedError
|
||||
elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef):
|
||||
if src.ref.type == TYPE_WORD_TABLE and dest.type == TYPE_WORD:
|
||||
src_label = self.labels[src.ref.name]
|
||||
dest_label = self.labels[dest.name]
|
||||
addressing_mode = None
|
||||
if src.index == REG_X:
|
||||
addressing_mode = AbsoluteX
|
||||
elif src.index == REG_Y:
|
||||
addressing_mode = AbsoluteY
|
||||
else:
|
||||
raise NotImplementedError(src)
|
||||
self.emitter.emit(LDA(addressing_mode(src_label)))
|
||||
self.emitter.emit(STA(Absolute(dest_label)))
|
||||
self.emitter.emit(LDA(addressing_mode(Offset(src_label, 256))))
|
||||
self.emitter.emit(STA(Absolute(Offset(dest_label, 1))))
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
elif not isinstance(src, (ConstantRef, LocationRef)) or not isinstance(dest, LocationRef):
|
||||
raise NotImplementedError((src, dest))
|
||||
elif src.type == TYPE_BYTE and dest.type == TYPE_BYTE:
|
||||
|
@ -318,11 +407,9 @@ class Compiler(object):
|
|||
elif src.type == TYPE_WORD and dest.type == TYPE_WORD:
|
||||
if isinstance(src, ConstantRef):
|
||||
dest_label = self.labels[dest.name]
|
||||
hi = (src.value >> 8) & 255
|
||||
lo = src.value & 255
|
||||
self.emitter.emit(LDA(Immediate(Byte(hi))))
|
||||
self.emitter.emit(LDA(Immediate(Byte(src.low_byte()))))
|
||||
self.emitter.emit(STA(Absolute(dest_label)))
|
||||
self.emitter.emit(LDA(Immediate(Byte(lo))))
|
||||
self.emitter.emit(LDA(Immediate(Byte(src.high_byte()))))
|
||||
self.emitter.emit(STA(Absolute(Offset(dest_label, 1))))
|
||||
else:
|
||||
src_label = self.labels[src.name]
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
"""Binary machine code emitter. Used in SixtyPical to emit 6502 machine code,
|
||||
but not specific to SixtyPical, or 6502. Not even necessarily machine code -
|
||||
though some parts are written around the assumptions of 8-bit architectures."""
|
||||
|
||||
|
||||
class Emittable(object):
|
||||
def size(self):
|
||||
raise NotImplementedError
|
||||
|
@ -42,14 +47,29 @@ class Word(Emittable):
|
|||
return "%s(%r)" % (self.__class__.__name__, self.value)
|
||||
|
||||
|
||||
class Table(Emittable):
|
||||
def size(self):
|
||||
return 256
|
||||
|
||||
def serialize(self, addr=None):
|
||||
return chr(0) * self.size()
|
||||
|
||||
def __repr__(self):
|
||||
return "%s()" % (self.__class__.__name__)
|
||||
|
||||
|
||||
class Label(Emittable):
|
||||
def __init__(self, name, addr=None):
|
||||
def __init__(self, name, addr=None, length=None):
|
||||
self.name = name
|
||||
self.addr = addr
|
||||
self.length = length
|
||||
|
||||
def set_addr(self, addr):
|
||||
self.addr = addr
|
||||
|
||||
def set_length(self, length):
|
||||
self.length = length
|
||||
|
||||
def size(self):
|
||||
return 2
|
||||
|
||||
|
@ -66,8 +86,9 @@ class Label(Emittable):
|
|||
return Byte(self.addr + offset).serialize()
|
||||
|
||||
def __repr__(self):
|
||||
addrs = ', addr=%r' % self.addr if self.addr is not None else ''
|
||||
return "%s(%r%s)" % (self.__class__.__name__, self.name, addrs)
|
||||
addr_s = ', addr=%r' % self.addr if self.addr is not None else ''
|
||||
length_s = ', length=%r' % self.length if self.length is not None else ''
|
||||
return "%s(%r%s%s)" % (self.__class__.__name__, self.name, addr_s, length_s)
|
||||
|
||||
|
||||
class Offset(Emittable):
|
||||
|
@ -154,4 +175,4 @@ class Emitter(object):
|
|||
"""Set the given label to be at the current address and
|
||||
advance the address for the next label, but don't emit anything."""
|
||||
self.resolve_label(label)
|
||||
self.addr += label.size()
|
||||
self.addr += label.length
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Data/storage model for SixtyPical."""
|
||||
|
||||
|
||||
class Type(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
@ -106,7 +107,7 @@ class IndirectRef(Ref):
|
|||
self.ref = ref
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.ref == other.ref
|
||||
return isinstance(other, self.__class__) and self.ref == other.ref
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__class__.name) ^ hash(self.ref)
|
||||
|
@ -122,6 +123,28 @@ class IndirectRef(Ref):
|
|||
return False
|
||||
|
||||
|
||||
class IndexedRef(Ref):
|
||||
def __init__(self, ref, index):
|
||||
self.ref = ref
|
||||
self.index = index
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__) and self.ref == other.ref and self.index == other.index
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__class__.name) ^ hash(self.ref) ^ hash(self.index)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r, %r)' % (self.__class__.__name__, self.ref, self.index)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return '{}+{}'.format(self.ref.name, self.index.name)
|
||||
|
||||
def is_constant(self):
|
||||
return False
|
||||
|
||||
|
||||
class AddressRef(Ref):
|
||||
def __init__(self, ref):
|
||||
self.ref = ref
|
||||
|
@ -191,6 +214,12 @@ class ConstantRef(Ref):
|
|||
def is_constant(self):
|
||||
return True
|
||||
|
||||
def high_byte(self):
|
||||
return (self.value >> 8) & 255
|
||||
|
||||
def low_byte(self):
|
||||
return self.value & 255
|
||||
|
||||
|
||||
REG_A = LocationRef(TYPE_BYTE, 'a')
|
||||
REG_X = LocationRef(TYPE_BYTE, 'x')
|
||||
|
|
|
@ -4,7 +4,7 @@ from sixtypical.ast import Program, Defn, Routine, Block, Instr
|
|||
from sixtypical.model import (
|
||||
TYPE_BIT, TYPE_BYTE, TYPE_BYTE_TABLE, TYPE_WORD, TYPE_WORD_TABLE,
|
||||
RoutineType, VectorType, ExecutableType, BufferType, PointerType,
|
||||
LocationRef, ConstantRef, IndirectRef, AddressRef,
|
||||
LocationRef, ConstantRef, IndirectRef, IndexedRef, AddressRef,
|
||||
)
|
||||
from sixtypical.scanner import Scanner
|
||||
|
||||
|
@ -29,6 +29,8 @@ class Parser(object):
|
|||
raise SyntaxError('Undefined symbol "%s"' % name)
|
||||
return self.symbols[name].model
|
||||
|
||||
# --- grammar productions
|
||||
|
||||
def program(self):
|
||||
defns = []
|
||||
routines = []
|
||||
|
@ -47,6 +49,19 @@ class Parser(object):
|
|||
self.symbols[name] = SymEntry(routine, routine.location)
|
||||
routines.append(routine)
|
||||
self.scanner.check_type('EOF')
|
||||
# now backpatch the executable types.
|
||||
for defn in defns:
|
||||
if isinstance(defn.location.type, VectorType):
|
||||
t = defn.location.type
|
||||
t.inputs = set([self.lookup(w) for w in t.inputs])
|
||||
t.outputs = set([self.lookup(w) for w in t.outputs])
|
||||
t.trashes = set([self.lookup(w) for w in t.trashes])
|
||||
for routine in routines:
|
||||
if isinstance(routine.location.type, ExecutableType):
|
||||
t = routine.location.type
|
||||
t.inputs = set([self.lookup(w) for w in t.inputs])
|
||||
t.outputs = set([self.lookup(w) for w in t.outputs])
|
||||
t.trashes = set([self.lookup(w) for w in t.trashes])
|
||||
return Program(defns=defns, routines=routines)
|
||||
|
||||
def defn(self):
|
||||
|
@ -108,11 +123,11 @@ class Parser(object):
|
|||
outputs = set()
|
||||
trashes = set()
|
||||
if self.scanner.consume('inputs'):
|
||||
inputs = set(self.locexprs())
|
||||
inputs = set(self.labels())
|
||||
if self.scanner.consume('outputs'):
|
||||
outputs = set(self.locexprs())
|
||||
outputs = set(self.labels())
|
||||
if self.scanner.consume('trashes'):
|
||||
trashes = set(self.locexprs())
|
||||
trashes = set(self.labels())
|
||||
return (inputs, outputs, trashes)
|
||||
|
||||
def routine(self):
|
||||
|
@ -137,6 +152,20 @@ class Parser(object):
|
|||
location=location
|
||||
)
|
||||
|
||||
def labels(self):
|
||||
accum = []
|
||||
accum.append(self.label())
|
||||
while self.scanner.consume(','):
|
||||
accum.append(self.label())
|
||||
return accum
|
||||
|
||||
def label(self):
|
||||
"""Like a locexpr, but does not allow literal values, and the labels do not
|
||||
need to be defined yet. They will be resolved at the end of parsing."""
|
||||
loc = self.scanner.token
|
||||
self.scanner.scan()
|
||||
return loc
|
||||
|
||||
def locexprs(self):
|
||||
accum = []
|
||||
accum.append(self.locexpr())
|
||||
|
@ -175,7 +204,12 @@ class Parser(object):
|
|||
loc = self.locexpr()
|
||||
return AddressRef(loc)
|
||||
else:
|
||||
return self.locexpr()
|
||||
loc = self.locexpr()
|
||||
index = None
|
||||
if self.scanner.consume('+'):
|
||||
index = self.locexpr()
|
||||
loc = IndexedRef(loc, index)
|
||||
return loc
|
||||
|
||||
def block(self):
|
||||
instrs = []
|
||||
|
|
|
@ -207,6 +207,21 @@ Can't `st` to a memory location that doesn't appear in (outputs ∪ trashes).
|
|||
| }
|
||||
? ForbiddenWriteError: lives in main
|
||||
|
||||
Can't `st` a `word` type.
|
||||
|
||||
| word foo
|
||||
|
|
||||
| routine main
|
||||
| outputs foo
|
||||
| trashes a, n, z
|
||||
| {
|
||||
| ld a, 0
|
||||
| st a, foo
|
||||
| }
|
||||
? TypeMismatchError: a and foo in main
|
||||
|
||||
### tables ###
|
||||
|
||||
Storing to a table, you must use an index, and vice-versa.
|
||||
|
||||
| byte one
|
||||
|
@ -313,18 +328,47 @@ Reading from a table, you must use an index, and vice-versa.
|
|||
| }
|
||||
= ok
|
||||
|
||||
Can't `st` a `word` type.
|
||||
Copying to and from a word table.
|
||||
|
||||
| word foo
|
||||
| word one
|
||||
| word table many
|
||||
|
|
||||
| routine main
|
||||
| outputs foo
|
||||
| trashes a, n, z
|
||||
| inputs one, many
|
||||
| outputs one, many
|
||||
| trashes a, x, n, z
|
||||
| {
|
||||
| ld a, 0
|
||||
| st a, foo
|
||||
| ld x, 0
|
||||
| copy one, many + x
|
||||
| copy many + x, one
|
||||
| }
|
||||
? TypeMismatchError: a and foo in main
|
||||
= ok
|
||||
|
||||
| word one
|
||||
| word table many
|
||||
|
|
||||
| routine main
|
||||
| inputs one, many
|
||||
| outputs one, many
|
||||
| trashes a, x, n, z
|
||||
| {
|
||||
| ld x, 0
|
||||
| copy one, many
|
||||
| }
|
||||
? TypeMismatchError
|
||||
|
||||
| word one
|
||||
| word table many
|
||||
|
|
||||
| routine main
|
||||
| inputs one, many
|
||||
| outputs one, many
|
||||
| trashes a, x, n, z
|
||||
| {
|
||||
| ld x, 0
|
||||
| copy one + x, many
|
||||
| }
|
||||
? TypeMismatchError
|
||||
|
||||
### add ###
|
||||
|
||||
|
@ -373,6 +417,103 @@ Can't `add` to a memory location that isn't writeable.
|
|||
| }
|
||||
? ForbiddenWriteError: a in main
|
||||
|
||||
You can `add` a word constant to a word memory location.
|
||||
|
||||
| word score
|
||||
| routine main
|
||||
| inputs a, score
|
||||
| outputs score
|
||||
| trashes a, c, z, v, n
|
||||
| {
|
||||
| st off, c
|
||||
| add score, 1999
|
||||
| }
|
||||
= ok
|
||||
|
||||
`add`ing a word constant to a word memory location trashes `a`.
|
||||
|
||||
| word score
|
||||
| routine main
|
||||
| inputs a, score
|
||||
| outputs score, a
|
||||
| trashes c, z, v, n
|
||||
| {
|
||||
| st off, c
|
||||
| add score, 1999
|
||||
| }
|
||||
? UnmeaningfulOutputError: a in main
|
||||
|
||||
To be sure, `add`ing a word constant to a word memory location trashes `a`.
|
||||
|
||||
| word score
|
||||
| routine main
|
||||
| inputs score
|
||||
| outputs score
|
||||
| trashes c, z, v, n
|
||||
| {
|
||||
| st off, c
|
||||
| add score, 1999
|
||||
| }
|
||||
? ForbiddenWriteError: a in main
|
||||
|
||||
You can `add` a word memory location to another word memory location.
|
||||
|
||||
| word score
|
||||
| word delta
|
||||
| routine main
|
||||
| inputs score, delta
|
||||
| outputs score
|
||||
| trashes a, c, z, v, n
|
||||
| {
|
||||
| st off, c
|
||||
| add score, delta
|
||||
| }
|
||||
= ok
|
||||
|
||||
`add`ing a word memory location to a word memory location trashes `a`.
|
||||
|
||||
| word score
|
||||
| word delta
|
||||
| routine main
|
||||
| inputs score, delta
|
||||
| outputs score
|
||||
| trashes c, z, v, n
|
||||
| {
|
||||
| st off, c
|
||||
| add score, delta
|
||||
| }
|
||||
? ForbiddenWriteError: a in main
|
||||
|
||||
You can `add` a word memory location, or a constant, to a pointer.
|
||||
|
||||
| pointer ptr
|
||||
| word delta
|
||||
| routine main
|
||||
| inputs ptr, delta
|
||||
| outputs ptr
|
||||
| trashes a, c, z, v, n
|
||||
| {
|
||||
| st off, c
|
||||
| add ptr, delta
|
||||
| add ptr, word 1
|
||||
| }
|
||||
= ok
|
||||
|
||||
`add`ing a word memory location, or a constant, to a pointer, trashes `a`.
|
||||
|
||||
| pointer ptr
|
||||
| word delta
|
||||
| routine main
|
||||
| inputs ptr, delta
|
||||
| outputs ptr
|
||||
| trashes c, z, v, n
|
||||
| {
|
||||
| st off, c
|
||||
| add ptr, delta
|
||||
| add ptr, word 1
|
||||
| }
|
||||
? ForbiddenWriteError: a in main
|
||||
|
||||
### sub ###
|
||||
|
||||
Can't `sub` from or to a memory location that isn't initialized.
|
||||
|
@ -1372,6 +1513,30 @@ But not if the vector is declared inappropriately.
|
|||
| }
|
||||
? IncompatibleConstraintsError
|
||||
|
||||
"Appropriately" means, if the routine affects no more than what is named
|
||||
in the input/output sets of the vector.
|
||||
|
||||
| vector vec
|
||||
| inputs a, x
|
||||
| outputs x
|
||||
| trashes a, z, n
|
||||
|
|
||||
| routine foo
|
||||
| inputs x
|
||||
| outputs x
|
||||
| trashes z, n
|
||||
| {
|
||||
| inc x
|
||||
| }
|
||||
|
|
||||
| routine main
|
||||
| outputs vec
|
||||
| trashes a, z, n
|
||||
| {
|
||||
| copy foo, vec
|
||||
| }
|
||||
= ok
|
||||
|
||||
Routines are read-only.
|
||||
|
||||
| vector vec
|
||||
|
|
|
@ -7,7 +7,7 @@ SixtyPical to 6502 machine code.
|
|||
[Falderal]: http://catseye.tc/node/Falderal
|
||||
|
||||
-> Functionality "Compile SixtyPical program" is implemented by
|
||||
-> shell command "bin/sixtypical --compile %(test-body-file) | fa-bin-to-hex"
|
||||
-> shell command "bin/sixtypical --basic-prelude --compile %(test-body-file) | tests/appliances/bin/dcc6502-adapter"
|
||||
|
||||
-> Tests for functionality "Compile SixtyPical program"
|
||||
|
||||
|
@ -16,7 +16,7 @@ Null program.
|
|||
| routine main
|
||||
| {
|
||||
| }
|
||||
= 00c060
|
||||
= $080D RTS
|
||||
|
||||
Rudimentary program.
|
||||
|
||||
|
@ -28,7 +28,9 @@ Rudimentary program.
|
|||
| st off, c
|
||||
| add a, 4
|
||||
| }
|
||||
= 00c018690460
|
||||
= $080D CLC
|
||||
= $080E ADC #$04
|
||||
= $0810 RTS
|
||||
|
||||
Call extern.
|
||||
|
||||
|
@ -44,7 +46,9 @@ Call extern.
|
|||
| ld a, 65
|
||||
| call chrout
|
||||
| }
|
||||
= 00c0a94120d2ff60
|
||||
= $080D LDA #$41
|
||||
= $080F JSR $FFD2
|
||||
= $0812 RTS
|
||||
|
||||
Call defined routine.
|
||||
|
||||
|
@ -62,7 +66,12 @@ Call defined routine.
|
|||
| {
|
||||
| call foo
|
||||
| }
|
||||
= 00c02004c060a900a200a00060
|
||||
= $080D JSR $0811
|
||||
= $0810 RTS
|
||||
= $0811 LDA #$00
|
||||
= $0813 LDX #$00
|
||||
= $0815 LDY #$00
|
||||
= $0817 RTS
|
||||
|
||||
Access a defined memory location.
|
||||
|
||||
|
@ -75,7 +84,10 @@ Access a defined memory location.
|
|||
| st y, foo
|
||||
| ld a, foo
|
||||
| }
|
||||
= 00c0a0008c09c0ad09c060
|
||||
= $080D LDY #$00
|
||||
= $080F STY $0816
|
||||
= $0812 LDA $0816
|
||||
= $0815 RTS
|
||||
|
||||
Memory location with explicit address.
|
||||
|
||||
|
@ -87,7 +99,9 @@ Memory location with explicit address.
|
|||
| ld a, 100
|
||||
| st a, screen
|
||||
| }
|
||||
= 00c0a9648d000460
|
||||
= $080D LDA #$64
|
||||
= $080F STA $0400
|
||||
= $0812 RTS
|
||||
|
||||
Memory location with initial value.
|
||||
|
||||
|
@ -99,7 +113,9 @@ Memory location with initial value.
|
|||
| {
|
||||
| ld a, lives
|
||||
| }
|
||||
= 00c0ad04c06003
|
||||
= $080D LDA $0811
|
||||
= $0810 RTS
|
||||
= $0811 .byte $03
|
||||
|
||||
Some instructions.
|
||||
|
||||
|
@ -141,7 +157,39 @@ Some instructions.
|
|||
| shl a
|
||||
| shr a
|
||||
| }
|
||||
= 00c0a900a200a0008d46c08e46c08c46c0381869016d46c0e901ed46c0ee46c0e8c8ce46c0ca8829ff2d46c009ff0d46c049ff4d46c0c901cd46c0e001ec46c0c001cc46c02a6a60
|
||||
= $080D LDA #$00
|
||||
= $080F LDX #$00
|
||||
= $0811 LDY #$00
|
||||
= $0813 STA $0853
|
||||
= $0816 STX $0853
|
||||
= $0819 STY $0853
|
||||
= $081C SEC
|
||||
= $081D CLC
|
||||
= $081E ADC #$01
|
||||
= $0820 ADC $0853
|
||||
= $0823 SBC #$01
|
||||
= $0825 SBC $0853
|
||||
= $0828 INC $0853
|
||||
= $082B INX
|
||||
= $082C INY
|
||||
= $082D DEC $0853
|
||||
= $0830 DEX
|
||||
= $0831 DEY
|
||||
= $0832 AND #$FF
|
||||
= $0834 AND $0853
|
||||
= $0837 ORA #$FF
|
||||
= $0839 ORA $0853
|
||||
= $083C EOR #$FF
|
||||
= $083E EOR $0853
|
||||
= $0841 CMP #$01
|
||||
= $0843 CMP $0853
|
||||
= $0846 CPX #$01
|
||||
= $0848 CPX $0853
|
||||
= $084B CPY #$01
|
||||
= $084D CPY $0853
|
||||
= $0850 ROL A
|
||||
= $0851 ROR A
|
||||
= $0852 RTS
|
||||
|
||||
Compiling `if`.
|
||||
|
||||
|
@ -155,7 +203,12 @@ Compiling `if`.
|
|||
| ld y, 2
|
||||
| }
|
||||
| }
|
||||
= 00c0a900d005a0014c0bc0a00260
|
||||
= $080D LDA #$00
|
||||
= $080F BNE $0816
|
||||
= $0811 LDY #$01
|
||||
= $0813 JMP $0818
|
||||
= $0816 LDY #$02
|
||||
= $0818 RTS
|
||||
|
||||
Compiling `if not`.
|
||||
|
||||
|
@ -169,7 +222,12 @@ Compiling `if not`.
|
|||
| ld y, 2
|
||||
| }
|
||||
| }
|
||||
= 00c0a900f005a0014c0bc0a00260
|
||||
= $080D LDA #$00
|
||||
= $080F BEQ $0816
|
||||
= $0811 LDY #$01
|
||||
= $0813 JMP $0818
|
||||
= $0816 LDY #$02
|
||||
= $0818 RTS
|
||||
|
||||
Compiling `if` without `else`.
|
||||
|
||||
|
@ -181,7 +239,10 @@ Compiling `if` without `else`.
|
|||
| ld y, 1
|
||||
| }
|
||||
| }
|
||||
= 00c0a900d002a00160
|
||||
= $080D LDA #$00
|
||||
= $080F BNE $0813
|
||||
= $0811 LDY #$01
|
||||
= $0813 RTS
|
||||
|
||||
Compiling `repeat`.
|
||||
|
||||
|
@ -195,7 +256,12 @@ Compiling `repeat`.
|
|||
| cmp y, 91
|
||||
| } until z
|
||||
| }
|
||||
= 00c0a04198c8c05bd0fa60
|
||||
= $080D LDY #$41
|
||||
= $080F TYA
|
||||
= $0810 INY
|
||||
= $0811 CPY #$5B
|
||||
= $0813 BNE $080F
|
||||
= $0815 RTS
|
||||
|
||||
Compiling `repeat until not`.
|
||||
|
||||
|
@ -209,7 +275,12 @@ Compiling `repeat until not`.
|
|||
| cmp y, 91
|
||||
| } until not z
|
||||
| }
|
||||
= 00c0a04198c8c05bf0fa60
|
||||
= $080D LDY #$41
|
||||
= $080F TYA
|
||||
= $0810 INY
|
||||
= $0811 CPY #$5B
|
||||
= $0813 BEQ $080F
|
||||
= $0815 RTS
|
||||
|
||||
Compiling `repeat forever`.
|
||||
|
||||
|
@ -221,7 +292,10 @@ Compiling `repeat forever`.
|
|||
| inc y
|
||||
| } forever
|
||||
| }
|
||||
= 00c0a041c84c02c060
|
||||
= $080D LDY #$41
|
||||
= $080F INY
|
||||
= $0810 JMP $080F
|
||||
= $0813 RTS
|
||||
|
||||
Indexed access.
|
||||
|
||||
|
@ -237,7 +311,48 @@ Indexed access.
|
|||
| st a, many + x
|
||||
| ld a, many + x
|
||||
| }
|
||||
= 00c0a200a9009d0dc0bd0dc060
|
||||
= $080D LDX #$00
|
||||
= $080F LDA #$00
|
||||
= $0811 STA $0819,X
|
||||
= $0814 LDA $0819,X
|
||||
= $0817 RTS
|
||||
|
||||
Byte tables take up 256 bytes in memory.
|
||||
|
||||
| byte table tab1
|
||||
| byte table tab2
|
||||
|
|
||||
| routine main
|
||||
| inputs tab1
|
||||
| outputs tab2
|
||||
| trashes a, x, n, z
|
||||
| {
|
||||
| ld x, 0
|
||||
| ld a, tab1 + x
|
||||
| st a, tab2 + x
|
||||
| }
|
||||
= $080D LDX #$00
|
||||
= $080F LDA $0816,X
|
||||
= $0812 STA $0916,X
|
||||
= $0815 RTS
|
||||
|
||||
Byte storage locations take up only 1 byte in memory.
|
||||
|
||||
| byte one
|
||||
| byte two
|
||||
|
|
||||
| routine main
|
||||
| outputs one, two
|
||||
| trashes a, x, n, z
|
||||
| {
|
||||
| ld a, 0
|
||||
| st a, one
|
||||
| st a, two
|
||||
| }
|
||||
= $080D LDA #$00
|
||||
= $080F STA $0816
|
||||
= $0812 STA $0817
|
||||
= $0815 RTS
|
||||
|
||||
Copy byte to byte.
|
||||
|
||||
|
@ -251,7 +366,9 @@ Copy byte to byte.
|
|||
| {
|
||||
| copy baz, bar
|
||||
| }
|
||||
= 00c0ad09c08d07c060
|
||||
= $080D LDA $0815
|
||||
= $0810 STA $0814
|
||||
= $0813 RTS
|
||||
|
||||
Copy word to word.
|
||||
|
||||
|
@ -265,7 +382,11 @@ Copy word to word.
|
|||
| {
|
||||
| copy baz, bar
|
||||
| }
|
||||
= 00c0ad0fc08d0dc0ad10c08d0ec060
|
||||
= $080D LDA $081C
|
||||
= $0810 STA $081A
|
||||
= $0813 LDA $081D
|
||||
= $0816 STA $081B
|
||||
= $0819 RTS
|
||||
|
||||
Copy literal word to word.
|
||||
|
||||
|
@ -275,9 +396,13 @@ Copy literal word to word.
|
|||
| outputs bar
|
||||
| trashes a, n, z
|
||||
| {
|
||||
| copy 65535, bar
|
||||
| copy 2000, bar
|
||||
| }
|
||||
= 00c0a9ff8d0bc0a9ff8d0cc060
|
||||
= $080D LDA #$D0
|
||||
= $080F STA $0818
|
||||
= $0812 LDA #$07
|
||||
= $0814 STA $0819
|
||||
= $0817 RTS
|
||||
|
||||
Copy vector to vector.
|
||||
|
||||
|
@ -291,7 +416,11 @@ Copy vector to vector.
|
|||
| {
|
||||
| copy baz, bar
|
||||
| }
|
||||
= 00c0ad0fc08d0dc0ad10c08d0ec060
|
||||
= $080D LDA $081C
|
||||
= $0810 STA $081A
|
||||
= $0813 LDA $081D
|
||||
= $0816 STA $081B
|
||||
= $0819 RTS
|
||||
|
||||
Copy routine to vector, inside an `interrupts off` block.
|
||||
|
||||
|
@ -314,7 +443,57 @@ Copy routine to vector, inside an `interrupts off` block.
|
|||
| copy foo, bar
|
||||
| }
|
||||
| }
|
||||
= 00c078a90d8d0fc0a9c08d10c05860e860
|
||||
= $080D SEI
|
||||
= $080E LDA #$1A
|
||||
= $0810 STA $081C
|
||||
= $0813 LDA #$08
|
||||
= $0815 STA $081D
|
||||
= $0818 CLI
|
||||
= $0819 RTS
|
||||
= $081A INX
|
||||
= $081B RTS
|
||||
|
||||
Copy word to word table and back, with both `x` and `y` as indexes.
|
||||
|
||||
| word one
|
||||
| word table many
|
||||
|
|
||||
| routine main
|
||||
| inputs one, many
|
||||
| outputs one, many
|
||||
| trashes a, x, y, n, z
|
||||
| {
|
||||
| ld x, 0
|
||||
| ld y, 0
|
||||
| copy 777, one
|
||||
| copy one, many + x
|
||||
| copy one, many + y
|
||||
| copy many + x, one
|
||||
| copy many + y, one
|
||||
| }
|
||||
= $080D LDX #$00
|
||||
= $080F LDY #$00
|
||||
= $0811 LDA #$09
|
||||
= $0813 STA $084C
|
||||
= $0816 LDA #$03
|
||||
= $0818 STA $084D
|
||||
= $081B LDA $084C
|
||||
= $081E STA $084E,X
|
||||
= $0821 LDA $084D
|
||||
= $0824 STA $094E,X
|
||||
= $0827 LDA $084C
|
||||
= $082A STA $084E,Y
|
||||
= $082D LDA $084D
|
||||
= $0830 STA $094E,Y
|
||||
= $0833 LDA $084E,X
|
||||
= $0836 STA $084C
|
||||
= $0839 LDA $094E,X
|
||||
= $083C STA $084D
|
||||
= $083F LDA $084E,Y
|
||||
= $0842 STA $084C
|
||||
= $0845 LDA $094E,Y
|
||||
= $0848 STA $084D
|
||||
= $084B RTS
|
||||
|
||||
Indirect call.
|
||||
|
||||
|
@ -328,7 +507,15 @@ Indirect call.
|
|||
| copy bar, foo
|
||||
| call foo
|
||||
| }
|
||||
= 00c0a90e8d14c0a9c08d15c02011c060a2c8606c14c0
|
||||
= $080D LDA #$1B
|
||||
= $080F STA $0821
|
||||
= $0812 LDA #$08
|
||||
= $0814 STA $0822
|
||||
= $0817 JSR $081E
|
||||
= $081A RTS
|
||||
= $081B LDX #$C8
|
||||
= $081D RTS
|
||||
= $081E JMP ($0821)
|
||||
|
||||
goto.
|
||||
|
||||
|
@ -340,7 +527,54 @@ goto.
|
|||
| ld y, 200
|
||||
| goto bar
|
||||
| }
|
||||
= 00c0a0c84c06c060a2c860
|
||||
= $080D LDY #$C8
|
||||
= $080F JMP $0813
|
||||
= $0812 RTS
|
||||
= $0813 LDX #$C8
|
||||
= $0815 RTS
|
||||
|
||||
### word operations
|
||||
|
||||
Adding a constant word to a word memory location.
|
||||
|
||||
| word score
|
||||
| routine main
|
||||
| inputs score
|
||||
| outputs score
|
||||
| trashes a, c, z, v, n
|
||||
| {
|
||||
| st off, c
|
||||
| add score, 1999
|
||||
| }
|
||||
= $080D CLC
|
||||
= $080E LDA $081F
|
||||
= $0811 ADC #$CF
|
||||
= $0813 STA $081F
|
||||
= $0816 LDA $0820
|
||||
= $0819 ADC #$07
|
||||
= $081B STA $0820
|
||||
= $081E RTS
|
||||
|
||||
Adding a word memory location to another word memory location.
|
||||
|
||||
| word score
|
||||
| word delta
|
||||
| routine main
|
||||
| inputs score, delta
|
||||
| outputs score
|
||||
| trashes a, c, z, v, n
|
||||
| {
|
||||
| st off, c
|
||||
| add score, delta
|
||||
| }
|
||||
= $080D CLC
|
||||
= $080E LDA $0821
|
||||
= $0811 ADC $0823
|
||||
= $0814 STA $0821
|
||||
= $0817 LDA $0822
|
||||
= $081A ADC $0824
|
||||
= $081D STA $0822
|
||||
= $0820 RTS
|
||||
|
||||
### Buffers and Pointers
|
||||
|
||||
|
@ -357,7 +591,12 @@ Load address into pointer.
|
|||
| ld y, 0
|
||||
| copy ^buf, ptr
|
||||
| }
|
||||
= 00c0a000a90b85fea9c085ff60
|
||||
= $080D LDY #$00
|
||||
= $080F LDA #$18
|
||||
= $0811 STA $FE
|
||||
= $0813 LDA #$08
|
||||
= $0815 STA $FF
|
||||
= $0817 RTS
|
||||
|
||||
Write literal through a pointer.
|
||||
|
||||
|
@ -373,7 +612,14 @@ Write literal through a pointer.
|
|||
| copy ^buf, ptr
|
||||
| copy 123, [ptr] + y
|
||||
| }
|
||||
= 00c0a000a90f85fea9c085ffa97b91fe60
|
||||
= $080D LDY #$00
|
||||
= $080F LDA #$1C
|
||||
= $0811 STA $FE
|
||||
= $0813 LDA #$08
|
||||
= $0815 STA $FF
|
||||
= $0817 LDA #$7B
|
||||
= $0819 STA ($FE),Y
|
||||
= $081B RTS
|
||||
|
||||
Write stored value through a pointer.
|
||||
|
||||
|
@ -390,7 +636,14 @@ Write stored value through a pointer.
|
|||
| copy ^buf, ptr
|
||||
| copy foo, [ptr] + y
|
||||
| }
|
||||
= 00c0a000a91085fea9c085ffad12c091fe60
|
||||
= $080D LDY #$00
|
||||
= $080F LDA #$1D
|
||||
= $0811 STA $FE
|
||||
= $0813 LDA #$08
|
||||
= $0815 STA $FF
|
||||
= $0817 LDA $101D
|
||||
= $081A STA ($FE),Y
|
||||
= $081C RTS
|
||||
|
||||
Read through a pointer.
|
||||
|
||||
|
@ -407,4 +660,56 @@ Read through a pointer.
|
|||
| copy ^buf, ptr
|
||||
| copy [ptr] + y, foo
|
||||
| }
|
||||
= 00c0a000a91085fea9c085ffb1fe8d12c060
|
||||
= $080D LDY #$00
|
||||
= $080F LDA #$1D
|
||||
= $0811 STA $FE
|
||||
= $0813 LDA #$08
|
||||
= $0815 STA $FF
|
||||
= $0817 LDA ($FE),Y
|
||||
= $0819 STA $101D
|
||||
= $081C RTS
|
||||
|
||||
Add a word memory location, and a literal word, to a pointer, and then read through it.
|
||||
Note that this is *not* range-checked. (Yet.)
|
||||
|
||||
| buffer[2048] buf
|
||||
| pointer ptr @ 254
|
||||
| byte foo
|
||||
| word delta
|
||||
|
|
||||
| routine main
|
||||
| inputs buf
|
||||
| outputs y, foo, delta
|
||||
| trashes a, z, n, ptr
|
||||
| {
|
||||
| copy 619, delta
|
||||
| ld y, 0
|
||||
| copy ^buf, ptr
|
||||
| add ptr, delta
|
||||
| add ptr, word 1
|
||||
| copy [ptr] + y, foo
|
||||
| }
|
||||
= $080D LDA #$6B
|
||||
= $080F STA $1042
|
||||
= $0812 LDA #$02
|
||||
= $0814 STA $1043
|
||||
= $0817 LDY #$00
|
||||
= $0819 LDA #$41
|
||||
= $081B STA $FE
|
||||
= $081D LDA #$08
|
||||
= $081F STA $FF
|
||||
= $0821 LDA $FE
|
||||
= $0823 ADC $1042
|
||||
= $0826 STA $FE
|
||||
= $0828 LDA $FF
|
||||
= $082A ADC $1043
|
||||
= $082D STA $FF
|
||||
= $082F LDA $FE
|
||||
= $0831 ADC #$01
|
||||
= $0833 STA $FE
|
||||
= $0835 LDA $FF
|
||||
= $0837 ADC #$00
|
||||
= $0839 STA $FF
|
||||
= $083B LDA ($FE),Y
|
||||
= $083D STA $1041
|
||||
= $0840 RTS
|
||||
|
|
|
@ -129,6 +129,7 @@ User-defined memory addresses of different types.
|
|||
| word wor
|
||||
| vector vec
|
||||
| byte table tab
|
||||
| word table wtab
|
||||
| buffer[2048] buf
|
||||
| pointer ptr
|
||||
|
|
||||
|
@ -250,7 +251,7 @@ Can't define two routines with the same name.
|
|||
| }
|
||||
? SyntaxError
|
||||
|
||||
Declaring a byte table memory location.
|
||||
Declaring byte and word table memory location.
|
||||
|
||||
| byte table tab
|
||||
|
|
||||
|
@ -262,6 +263,17 @@ Declaring a byte table memory location.
|
|||
| }
|
||||
= ok
|
||||
|
||||
| word one
|
||||
| word table many
|
||||
|
|
||||
| routine main {
|
||||
| ld x, 0
|
||||
| copy one, many + x
|
||||
| copy word 0, many + x
|
||||
| copy many + x, one
|
||||
| }
|
||||
= ok
|
||||
|
||||
Declaring and calling a vector.
|
||||
|
||||
| vector cinv
|
||||
|
@ -293,6 +305,44 @@ Only vectors can be decorated with constraints like that.
|
|||
| }
|
||||
? SyntaxError
|
||||
|
||||
Constraints set may only contain labels.
|
||||
|
||||
| vector cinv
|
||||
| inputs a
|
||||
| outputs 200
|
||||
| trashes a, x, z, n
|
||||
| @ 788
|
||||
|
|
||||
| routine foo {
|
||||
| ld a, 0
|
||||
| }
|
||||
| routine main {
|
||||
| with interrupts off {
|
||||
| copy foo, cinv
|
||||
| }
|
||||
| call cinv
|
||||
| }
|
||||
? SyntaxError
|
||||
|
||||
A vector can name itself in its inputs, outputs, and trashes.
|
||||
|
||||
| vector cinv
|
||||
| inputs cinv, a
|
||||
| outputs cinv, x
|
||||
| trashes a, x, z, n
|
||||
| @ 788
|
||||
|
|
||||
| routine foo {
|
||||
| ld a, 0
|
||||
| }
|
||||
| routine main {
|
||||
| with interrupts off {
|
||||
| copy foo, cinv
|
||||
| }
|
||||
| call cinv
|
||||
| }
|
||||
= ok
|
||||
|
||||
goto.
|
||||
|
||||
| routine foo {
|
||||
|
|
23
tests/appliances/bin/dcc6502-adapter
Executable file
23
tests/appliances/bin/dcc6502-adapter
Executable file
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# script that allows the binary output of sixtypical --basic-prelude --compile to be
|
||||
# disassembled by https://github.com/tcarmelveilleux/dcc6502
|
||||
|
||||
import sys
|
||||
import re
|
||||
from subprocess import check_output
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
bytes = sys.stdin.read()
|
||||
|
||||
bytes = bytes[14:]
|
||||
|
||||
f = NamedTemporaryFile(delete=False)
|
||||
filename = f.name
|
||||
f.write(bytes)
|
||||
f.close()
|
||||
|
||||
lines = [line for line in check_output("dcc6502 -o 2061 {}".format(filename), shell=True).split('\n') if line and not line.startswith(';')]
|
||||
lines = [re.sub(r'\s*\;.*$', '', line) for line in lines]
|
||||
lines.pop()
|
||||
sys.stdout.write('\n'.join(lines))
|
Loading…
Reference in New Issue
Block a user