1
0
mirror of https://github.com/catseye/SixtyPical.git synced 2024-11-25 23:49:17 +00:00

Merge branch 'develop-0.17' into remove-legacy-syntax

This commit is contained in:
Chris Pressey 2018-09-07 17:49:03 +01:00
commit 0c63de9351
9 changed files with 148 additions and 130 deletions

View File

@ -5,6 +5,11 @@ History of SixtyPical
----
* `save X, Y, Z { }` now allowed as a shortcut for nested `save`s.
* If the name in a location expression isn't found in the symbol
table, a forward reference will _always_ be generated; and forward
references in _all_ operations will be resolved after parsing.
* As a consequence, trying to call or goto a non-routine-typed symbol
is now an analysis error, not a syntax error.
* Split TODO off into own file.
* `sixtypical` no longer writes the compiled binary to standard
output. The `--output` command-line argument should be given

34
TODO.md
View File

@ -1,20 +1,16 @@
TODO for SixtyPical
===================
### `low` and `high` address operators
### 16-bit `cmp`
To turn `word` type into `byte`.
This is because we don't actually want `low` and `high` address operators
that turn `word` type into `byte`.
Trying to remember if we have a compelling case for this or now. The best I can think
of is for implementing 16-bit `cmp` in an efficient way. Maybe we should see if we
can get by with 16-bit `cmp` instead though.
This is because this immediately makes things harder (that is, effectively
impossible) to analyze.
The problem is that once a byte is extracted, putting it back into a word is awkward.
The address operators have to modify a destination in a special way. That is, when
you say `st a, >word`, you are updating `word` to be `word & $ff | a << 8`, somelike.
Is that consistent with `st`? Well, probably it is, but we have to explain it.
It might make more sense, then, for it to be "part of the operation" instead of "part of
the reference"; something like `st.hi x, word`; `st.lo y, word`. Dunno.
16-bit `cmp` also benefits from some special differences between `cmp`
and `sub` on 6502, so it would be nice to capture them.
### Save values to other-than-the-stack
@ -27,17 +23,12 @@ Allow
Which uses some other storage location instead of the stack. A local static
would be a good candidate for such.
### Make all symbols forward-referencable
Basically, don't do symbol-table lookups when parsing, but do have a more formal
"symbol resolution" (linking) phase right after parsing.
### Associate each pointer with the buffer it points into
Check that the buffer being read or written to through pointer, appears in appropriate
inputs or outputs set.
In the analysis, when we obtain a pointer, we need to record, in contect, what buffer
In the analysis, when we obtain a pointer, we need to record, in context, what buffer
that pointer came from.
When we write through that pointer, we need to set that buffer as written.
@ -78,8 +69,8 @@ error.
More generally, define a block as having zero or one `goto`s at the end. (and `goto`s cannot
appear elsewhere.)
If a block ends in a `call` can that be converted to end in a `goto`? Why not? I think it can.
The constraints should iron out the same both ways.
If a block ends in a `call` can that be converted to end in a `goto`? Why not? I think it can,
if the block is in tail position. The constraints should iron out the same both ways.
And - once we have this - why do we need `goto` to be in tail position, strictly?
As long as the routine has consistent type context every place it exits, that should be fine.
@ -91,3 +82,8 @@ Search a searchlist of include paths. And use them to make libraries of routine
One such library routine might be an `interrupt routine` type for various architectures.
Since "the supervisor" has stored values on the stack, we should be able to trash them
with impunity, in such a routine.
### Line numbers in analysis error messages
For analysis errors, there is a line number, but it's the line of the routine
after the routine in which the analysis error occurred. Fix this.

View File

@ -33,9 +33,11 @@
typedef routine
inputs joy2, press_fire_msg, dispatch_game_state,
actor_pos, actor_delta, actor_logic,
player_died,
screen, screen1, screen2, screen3, screen4, colormap1, colormap2, colormap3, colormap4
outputs dispatch_game_state,
actor_pos, actor_delta, actor_logic,
player_died,
screen, screen1, screen2, screen3, screen4, colormap1, colormap2, colormap3, colormap4
trashes a, x, y, c, z, n, v, pos, new_pos, delta, ptr, dispatch_logic
game_state_routine
@ -45,13 +47,13 @@ typedef routine
//
// Routines that conform to this type also follow this convention:
//
// Set carry if the player perished. Carry clear otherwise.
// Set player_died to 1 if the player perished. Unchanged otherwise.
//
typedef routine
inputs pos, delta, joy2, screen
outputs pos, delta, new_pos, screen, c
trashes a, x, y, z, n, v, ptr
inputs pos, delta, joy2, screen, player_died
outputs pos, delta, new_pos, screen, player_died
trashes a, x, y, z, n, v, c, ptr
logic_routine
// ----------------------------------------------------------------
@ -87,6 +89,8 @@ word new_pos
word table[256] actor_delta
word delta
byte player_died
vector logic_routine table[256] actor_logic
vector logic_routine dispatch_logic
@ -241,7 +245,7 @@ define check_new_position_in_bounds routine
routine init_game
inputs actor_pos, actor_delta, actor_logic
outputs actor_pos, actor_delta, actor_logic
outputs actor_pos, actor_delta, actor_logic, player_died
trashes pos, a, y, z, n, c, v
{
ld y, 0
@ -259,9 +263,11 @@ routine init_game
} until z
ld y, 0
copy word 0, actor_pos + y
copy word 40, actor_pos + y
copy word 0, actor_delta + y
copy player_logic, actor_logic + y
st y, player_died
}
// ----------------------------------------------------------------
@ -301,10 +307,9 @@ define player_logic logic_routine
st off, c
add ptr, pos
copy 81, [ptr] + y
st off, c
} else {
st on, c
ld a, 1
st a, player_died
}
// FIXME these trashes, strictly speaking, probably shouldn't be needed,
@ -314,8 +319,6 @@ define player_logic logic_routine
trash ptr
trash y
trash v
} else {
st off, c
}
}
@ -351,10 +354,6 @@ define enemy_logic logic_routine
st off, c
add ptr, pos
copy 82, [ptr] + y
st off, c
} else {
st on, c
}
// FIXME these trashes, strictly speaking, probably shouldn't be needed,
@ -373,8 +372,6 @@ define enemy_logic logic_routine
copy $ffd8, delta
}
}
st off, c
}
// ----------------------------------------------------------------
@ -408,6 +405,7 @@ define game_state_title_screen game_state_routine
define game_state_play game_state_routine
{
ld x, 0
st x, player_died
for x up to 15 {
copy actor_pos + x, pos
copy actor_delta + x, delta
@ -420,18 +418,19 @@ define game_state_play game_state_routine
save x {
copy actor_logic + x, dispatch_logic
call dispatch_logic
if c {
// Player died! Want no dead!
call clear_screen
copy game_state_game_over, dispatch_game_state
}
}
copy pos, actor_pos + x
copy delta, actor_delta + x
}
ld a, player_died
if not z {
// Player died! Want no dead!
call clear_screen
copy game_state_game_over, dispatch_game_state
}
goto save_cinv
}

View File

@ -550,6 +550,8 @@ class Analyzer(object):
context.invalidate_range(dest)
elif opcode == 'call':
type = instr.location.type
if not isinstance(type, (RoutineType, VectorType)):
raise TypeMismatchError(instr, instr.location)
if isinstance(type, VectorType):
type = type.of_type
for ref in type.inputs:

View File

@ -37,14 +37,16 @@ class AST(object):
def all_children(self):
for attr in self.children_attrs:
for child in self.attrs[attr]:
if child is not None:
yield child
for subchild in child.all_children():
yield subchild
for attr in self.child_attrs:
child = self.attrs[attr]
if child is not None:
yield child
for subchild in child.all_children():
yield subchild
for attr in self.child_attrs:
child = self.attrs[attr]
yield child
for subchild in child.all_children():
yield subchild
class Program(AST):

View File

@ -18,20 +18,6 @@ class Type(object):
def __hash__(self):
return hash(self.name)
def backpatch_constraint_labels(self, resolver):
def resolve(w):
if not isinstance(w, str):
return w
return resolver(w)
if isinstance(self, TableType):
self.of_type.backpatch_constraint_labels(resolver)
elif isinstance(self, VectorType):
self.of_type.backpatch_constraint_labels(resolver)
elif isinstance(self, RoutineType):
self.inputs = set([resolve(w) for w in self.inputs])
self.outputs = set([resolve(w) for w in self.outputs])
self.trashes = set([resolve(w) for w in self.trashes])
TYPE_BIT = Type('bit', max_range=(0, 1))
TYPE_BYTE = Type('byte', max_range=(0, 255))
@ -41,11 +27,11 @@ TYPE_WORD = Type('word', max_range=(0, 65535))
class RoutineType(Type):
"""This memory location contains the code for a routine."""
def __init__(self, inputs=None, outputs=None, trashes=None):
def __init__(self, inputs, outputs, trashes):
self.name = 'routine'
self.inputs = inputs or set()
self.outputs = outputs or set()
self.trashes = trashes or set()
self.inputs = inputs
self.outputs = outputs
self.trashes = trashes
def __repr__(self):
return '%s(%r, inputs=%r, outputs=%r, trashes=%r)' % (

View File

@ -18,6 +18,14 @@ class SymEntry(object):
return "%s(%r, %r)" % (self.__class__.__name__, self.ast_node, self.model)
class ForwardReference(object):
def __init__(self, name):
self.name = name
def __repr__(self):
return "%s(%r)" % (self.__class__.__name__, self.name)
class ParsingContext(object):
def __init__(self):
self.symbols = {} # token -> SymEntry
@ -45,7 +53,6 @@ class Parser(object):
def __init__(self, context, text, filename):
self.context = context
self.scanner = Scanner(text, filename)
self.backpatch_instrs = []
def syntax_error(self, msg):
self.scanner.syntax_error(msg)
@ -67,6 +74,44 @@ class Parser(object):
def clear_statics(self):
self.context.statics = {}
# ---- symbol resolution
def resolve_symbols(self, program):
# This could stand to be better unified.
def backpatch_constraint_labels(type_):
def resolve(w):
if not isinstance(w, ForwardReference):
return w
return self.lookup(w.name)
if isinstance(type_, TableType):
backpatch_constraint_labels(type_.of_type)
elif isinstance(type_, VectorType):
backpatch_constraint_labels(type_.of_type)
elif isinstance(type_, RoutineType):
type_.inputs = set([resolve(w) for w in type_.inputs])
type_.outputs = set([resolve(w) for w in type_.outputs])
type_.trashes = set([resolve(w) for w in type_.trashes])
for defn in program.defns:
backpatch_constraint_labels(defn.location.type)
for routine in program.routines:
backpatch_constraint_labels(routine.location.type)
def resolve_fwd_reference(obj, field):
field_value = getattr(obj, field, None)
if isinstance(field_value, ForwardReference):
setattr(obj, field, self.lookup(field_value.name))
elif isinstance(field_value, IndexedRef):
if isinstance(field_value.ref, ForwardReference):
field_value.ref = self.lookup(field_value.ref.name)
for node in program.all_children():
if isinstance(node, SingleOp):
resolve_fwd_reference(node, 'location')
resolve_fwd_reference(node, 'src')
resolve_fwd_reference(node, 'dest')
# --- grammar productions
def program(self):
@ -91,28 +136,9 @@ class Parser(object):
routines.append(routine)
self.scanner.check_type('EOF')
# now backpatch the executable types.
#for type_name, type_ in self.context.typedefs.items():
# type_.backpatch_constraint_labels(lambda w: self.lookup(w))
for defn in defns:
defn.location.type.backpatch_constraint_labels(lambda w: self.lookup(w))
for routine in routines:
routine.location.type.backpatch_constraint_labels(lambda w: self.lookup(w))
for instr in self.backpatch_instrs:
if instr.opcode in ('call', 'goto'):
name = instr.location
model = self.lookup(name)
if not isinstance(model.type, (RoutineType, VectorType)):
self.syntax_error('Illegal call of non-executable "%s"' % name)
instr.location = model
if instr.opcode in ('copy',) and isinstance(instr.src, str):
name = instr.src
model = self.lookup(name)
if not isinstance(model.type, (RoutineType, VectorType)):
self.syntax_error('Illegal copy of non-executable "%s"' % name)
instr.src = model
return Program(self.scanner.line_number, defns=defns, routines=routines)
program = Program(self.scanner.line_number, defns=defns, routines=routines)
self.resolve_symbols(program)
return program
def typedef(self):
self.scanner.expect('typedef')
@ -252,7 +278,11 @@ class Parser(object):
outputs = set(self.labels())
if self.scanner.consume('trashes'):
trashes = set(self.labels())
return (inputs, outputs, trashes)
return (
set([ForwardReference(n) for n in inputs]),
set([ForwardReference(n) for n in outputs]),
set([ForwardReference(n) for n in trashes])
)
def routine(self, name):
type_ = self.defn_type()
@ -302,23 +332,19 @@ class Parser(object):
accum.append(self.locexpr())
return accum
def locexpr(self, forward=False):
def locexpr(self):
if self.scanner.token in ('on', 'off', 'word') or self.scanner.token in self.context.consts or self.scanner.on_type('integer literal'):
return self.const()
elif forward:
else:
name = self.scanner.token
self.scanner.scan()
loc = self.context.fetch(name)
if loc is not None:
if loc:
return loc
else:
return name
else:
loc = self.lookup(self.scanner.token)
self.scanner.scan()
return loc
return ForwardReference(name)
def indlocexpr(self, forward=False):
def indlocexpr(self):
if self.scanner.consume('['):
loc = self.locexpr()
self.scanner.expect(']')
@ -329,10 +355,10 @@ class Parser(object):
loc = self.locexpr()
return AddressRef(loc)
else:
return self.indexed_locexpr(forward=forward)
return self.indexed_locexpr()
def indexed_locexpr(self, forward=False):
loc = self.locexpr(forward=forward)
def indexed_locexpr(self):
loc = self.locexpr()
if not isinstance(loc, str):
index = None
if self.scanner.consume('+'):
@ -427,17 +453,15 @@ class Parser(object):
self.scanner.scan()
name = self.scanner.token
self.scanner.scan()
instr = SingleOp(self.scanner.line_number, opcode=opcode, location=name, dest=None, src=None)
self.backpatch_instrs.append(instr)
instr = SingleOp(self.scanner.line_number, opcode=opcode, location=ForwardReference(name), dest=None, src=None)
return instr
elif self.scanner.token in ("copy",):
opcode = self.scanner.token
self.scanner.scan()
src = self.indlocexpr(forward=True)
src = self.indlocexpr()
self.scanner.expect(',')
dest = self.indlocexpr()
instr = SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src)
self.backpatch_instrs.append(instr)
return instr
elif self.scanner.consume("with"):
self.scanner.expect("interrupts")

View File

@ -196,6 +196,35 @@ If a routine reads or writes a user-define memory location, it needs to declare
| }
= ok
### call ###
You can't call a non-routine.
| byte up
|
| routine main outputs x, y trashes z, n {
| ld x, 0
| ld y, 1
| call up
| }
? TypeMismatchError: up
| routine main outputs x, y trashes z, n {
| ld x, 0
| ld y, 1
| call x
| }
? TypeMismatchError: x
Nor can you goto a non-routine.
| byte foo
|
| routine main {
| goto foo
| }
? TypeMismatchError: foo
### ld ###
Can't `ld` from a memory location that isn't initialized.

View File

@ -404,24 +404,6 @@ Can't call routine that hasn't been defined.
| }
? SyntaxError
And you can't call a non-routine.
| byte up
|
| define main routine {
| ld x, 0
| ld y, 1
| call up
| }
? SyntaxError
| define main routine {
| ld x, 0
| ld y, 1
| call x
| }
? SyntaxError
But you can call a routine that is yet to be defined, further on.
| define main routine {
@ -589,13 +571,6 @@ goto.
| }
? SyntaxError
| byte foo
|
| define main routine {
| goto foo
| }
? SyntaxError
Buffers and pointers.
| buffer[2048] buf