1
0
mirror of https://github.com/catseye/SixtyPical.git synced 2024-05-31 21:41:29 +00:00

Add reset instruction; much refactoring.

This commit is contained in:
Chris Pressey 2019-05-14 08:56:35 +01:00
parent dd29b6fd4a
commit 3f666f4385
16 changed files with 4260 additions and 3864 deletions

View File

@ -4,10 +4,18 @@ History of SixtyPical
0.20
----
* A `point ... into` block no longer initializes the pointer
by default. A subequent `reset` instruction must be used
to initialize the pointer. The pointer may be reset to any
valid offset within the table (not only 0) and it may be
reset multiple times inside the block.
* Local locations need no longer be static. If they are not
static, they are considered uninitialized until assigned,
and they can be declared with an explicit fixed address.
* Fixed a bug where two local statics could be declared with
the same name.
* Local locations need no longer be static. If they are not
static, they are considered uninitialized until assigned.
* Split context off from analyzer and put it in its own module.
* Split the SixtyPical Analysis tests across three files.
0.19
----

20
TODO.md
View File

@ -29,15 +29,7 @@ inner block has finished -- even if there is no `call`.)
These holes need to be plugged.
### Reset pointer in `point into` blocks
We have `point into` blocks, but maybe the action when entering such a
block shouldn't always be to set the given pointer to the start of the given table.
That is, sometimes we would like to start at some fixed offset. And
sometimes we want to (re)set the pointer, without closing and starting a new block.
### Pointers associated globally with a table
### Pointers associated globally with a table(?)
We have `point into` blocks, but we would also like to sometimes pass a pointer
around to different routines, and have them all "know" what table it operates on.
@ -62,16 +54,6 @@ be allocated at the same space.
This is more an impressive trick than a really useful feature, but still.
Impressive tricks are impressive.
### Locals with explicit addresses
A local could also be given an explicit address. In this case, two locals in
different routines could be given the same address, and as long as the condition
in the above paragraph holds, that's okay. (If it doesn't, the analyzer should
detect it.)
This would permit local pointers, which would be one way of addressing the
"same pointer to different tables" problem.
### Copy byte to/from table
Do we want a `copy bytevar, table + x` instruction? We don't currently have one.

View File

@ -265,6 +265,7 @@ define player_logic logic_routine
if c {
point ptr into screen {
reset ptr 0
st off, c
add ptr, new_pos
ld y, 0
@ -280,6 +281,7 @@ define player_logic logic_routine
cmp a, 32
if z {
point ptr into screen {
reset ptr 0
st off, c
add ptr, pos
copy 32, [ptr] + y
@ -288,6 +290,7 @@ define player_logic logic_routine
copy new_pos, pos
point ptr into screen {
reset ptr 0
st off, c
add ptr, pos
copy 81, [ptr] + y
@ -307,6 +310,7 @@ define enemy_logic logic_routine
if c {
point ptr into screen {
reset ptr 0
st off, c
add ptr, new_pos
ld y, 0
@ -321,6 +325,7 @@ define enemy_logic logic_routine
cmp a, 32
if z {
point ptr into screen {
reset ptr 0
st off, c
add ptr, pos
copy 32, [ptr] + y
@ -329,6 +334,7 @@ define enemy_logic logic_routine
copy new_pos, pos
point ptr into screen {
reset ptr 0
st off, c
add ptr, pos
copy 82, [ptr] + y

View File

@ -11,10 +11,12 @@ define main routine
trashes a, z, n, c, ptr
{
ld y, 0
copy ^buf, ptr
copy 123, [ptr] + y
copy [ptr] + y, foo
copy foo, [ptr] + y
point ptr into buf {
reset ptr 0
copy 123, [ptr] + y
copy [ptr] + y, foo
copy foo, [ptr] + y
}
// TODO: support saying `cmp foo, 123`, maybe
ld a, foo

View File

@ -1,8 +1,9 @@
# encoding: UTF-8
from sixtypical.ast import (
Program, Routine, Block, SingleOp, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
Program, Routine, Block, SingleOp, Reset, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
)
from sixtypical.context import AnalysisContext
from sixtypical.model import (
TYPE_BYTE, TYPE_WORD,
TableType, PointerType, VectorType, RoutineType,
@ -80,321 +81,6 @@ class IncompatibleConstraintsError(ConstraintsError):
pass
class AnalysisContext(object):
"""
A location is touched if it was changed (or even potentially
changed) during this routine, or some routine called by this routine.
A location is meaningful if it was an input to this routine,
or if it was set to a meaningful value by some operation in this
routine (or some routine called by this routine).
If a location is meaningful, it has a range. This range represents
the lowest and highest values that it might possibly be (i.e. we know
it cannot possibly be below the lowest or above the highest.) In the
absence of any usage information, the range of a byte, is 0..255 and
the range of a word is 0..65535.
A location is writeable if it was listed in the outputs and trashes
lists of this routine. A location can also be temporarily marked
unwriteable in certain contexts, such as `for` loops.
"""
def __init__(self, symtab, routine, inputs, outputs, trashes):
self.symtab = symtab
self.routine = routine # Routine (AST node)
self._touched = set() # {LocationRef}
self._range = dict() # LocationRef -> (Int, Int)
self._writeable = set() # {LocationRef}
self._terminated = False
self._gotos_encountered = set()
self._pointer_assoc = dict()
for ref in inputs:
if self.is_constant(ref):
raise ConstantConstraintError(self.routine, ref.name)
self._range[ref] = self.max_range(ref)
output_names = set()
for ref in outputs:
if self.is_constant(ref):
raise ConstantConstraintError(self.routine, ref.name)
output_names.add(ref.name)
self._writeable.add(ref)
for ref in trashes:
if self.is_constant(ref):
raise ConstantConstraintError(self.routine, ref.name)
if ref.name in output_names:
raise InconsistentConstraintsError(self.routine, ref.name)
self._writeable.add(ref)
def __str__(self):
return "{}(\n _touched={},\n _range={},\n _writeable={}\n)".format(
self.__class__.__name__,
LocationRef.format_set(self._touched), LocationRef.format_set(self._range), LocationRef.format_set(self._writeable)
)
def to_json_data(self):
type_ = self.symtab.fetch_global_type(self.routine.name)
return {
'routine_inputs': ','.join(sorted(loc.name for loc in type_.inputs)),
'routine_outputs': ','.join(sorted(loc.name for loc in type_.outputs)),
'routine_trashes': ','.join(sorted(loc.name for loc in type_.trashes)),
'touched': ','.join(sorted(loc.name for loc in self._touched)),
'range': dict((loc.name, '{}-{}'.format(rng[0], rng[1])) for (loc, rng) in self._range.items()),
'writeable': ','.join(sorted(loc.name for loc in self._writeable)),
'terminated': self._terminated,
'gotos_encountered': ','.join(sorted(loc.name for loc in self._gotos_encountered)),
}
def clone(self):
c = AnalysisContext(self.symtab, self.routine, [], [], [])
c._touched = set(self._touched)
c._range = dict(self._range)
c._writeable = set(self._writeable)
c._pointer_assoc = dict(self._pointer_assoc)
c._gotos_encountered = set(self._gotos_encountered)
return c
def update_from(self, other):
"""Replaces the information in this context, with the information from the other context.
This is an overwriting action - it does not attempt to merge the contexts.
We do not replace the gotos_encountered for technical reasons. (In `analyze_if`,
we merge those sets afterwards; at the end of `analyze_routine`, they are not distinct in the
set of contexts we are updating from, and we want to retain our own.)"""
self.routine = other.routine
self._touched = set(other._touched)
self._range = dict(other._range)
self._writeable = set(other._writeable)
self._terminated = other._terminated
self._pointer_assoc = dict(other._pointer_assoc)
def each_meaningful(self):
for ref in self._range.keys():
yield ref
def each_touched(self):
for ref in self._touched:
yield ref
def each_writeable(self):
for ref in self._writeable:
yield ref
def assert_meaningful(self, *refs, **kwargs):
exception_class = kwargs.get('exception_class', UnmeaningfulReadError)
for ref in refs:
if self.symtab.has_local(self.routine.name, ref.name):
if ref not in self._range:
message = ref.name
if kwargs.get('message'):
message += ' (%s)' % kwargs['message']
raise exception_class(self.routine, message)
else:
continue
if self.is_constant(ref):
pass
elif isinstance(ref, LocationRef):
if ref not in self._range:
message = ref.name
if kwargs.get('message'):
message += ' (%s)' % kwargs['message']
raise exception_class(self.routine, message)
elif isinstance(ref, IndexedRef):
self.assert_meaningful(ref.ref, **kwargs)
self.assert_meaningful(ref.index, **kwargs)
else:
raise NotImplementedError(ref)
def assert_writeable(self, *refs, **kwargs):
exception_class = kwargs.get('exception_class', ForbiddenWriteError)
for ref in refs:
# locals are always writeable
if self.symtab.has_local(self.routine.name, ref.name):
continue
if ref not in self._writeable:
message = ref.name
if kwargs.get('message'):
message += ' (%s)' % kwargs['message']
raise exception_class(self.routine, message)
def assert_in_range(self, inside, outside, offset):
"""Given two locations, assert that the first location, offset by the given offset,
is contained 'inside' the second location."""
assert isinstance(inside, LocationRef)
assert isinstance(outside, LocationRef)
# inside should always be meaningful
inside_range = self._range[inside]
# outside might not be meaningful, so default to max range if necessary
if outside in self._range:
outside_range = self._range[outside]
else:
outside_range = self.max_range(outside)
if (inside_range[0] + offset.value) < outside_range[0] or (inside_range[1] + offset.value) > outside_range[1]:
raise RangeExceededError(self.routine,
"Possible range of {} {} (+{}) exceeds acceptable range of {} {}".format(
inside, inside_range, offset, outside, outside_range
)
)
def set_touched(self, *refs):
for ref in refs:
self._touched.add(ref)
# TODO: it might be possible to invalidate the range here
def set_meaningful(self, *refs):
for ref in refs:
if ref not in self._range:
self._range[ref] = self.max_range(ref)
def set_top_of_range(self, ref, top):
self.assert_meaningful(ref)
(bottom, _) = self._range[ref]
self._range[ref] = (bottom, top)
def set_bottom_of_range(self, ref, bottom):
self.assert_meaningful(ref)
(top, _) = self._range[ref]
self._range[ref] = (bottom, top)
def set_range(self, ref, bottom, top):
self.assert_meaningful(ref)
self._range[ref] = (bottom, top)
def get_top_of_range(self, ref):
if isinstance(ref, ConstantRef):
return ref.value
self.assert_meaningful(ref)
(_, top) = self._range[ref]
return top
def get_bottom_of_range(self, ref):
if isinstance(ref, ConstantRef):
return ref.value
self.assert_meaningful(ref)
(bottom, _) = self._range[ref]
return bottom
def get_range(self, ref):
if isinstance(ref, ConstantRef):
return (ref.value, ref.value)
self.assert_meaningful(ref)
(bottom, top) = self._range[ref]
return bottom, top
def copy_range(self, src, dest):
self.assert_meaningful(src)
if src in self._range:
src_range = self._range[src]
else:
src_range = self.max_range(src)
self._range[dest] = src_range
def invalidate_range(self, ref):
self.assert_meaningful(ref)
self._range[ref] = self.max_range(ref)
def set_unmeaningful(self, *refs):
for ref in refs:
if ref in self._range:
del self._range[ref]
def set_written(self, *refs):
"""A "helper" method which does the following common sequence for
the given refs: asserts they're all writable, and sets them all
as touched and meaningful."""
self.assert_writeable(*refs)
self.set_touched(*refs)
self.set_meaningful(*refs)
def set_unwriteable(self, *refs):
"""Intended to be used for implementing analyzing `for`."""
for ref in refs:
self._writeable.remove(ref)
def set_writeable(self, *refs):
"""Intended to be used for implementing analyzing `for`, but also used in `save`."""
for ref in refs:
self._writeable.add(ref)
def encounter_gotos(self, gotos):
self._gotos_encountered |= gotos
def encountered_gotos(self):
return self._gotos_encountered
def set_terminated(self):
# Having a terminated context and having encountered gotos is not the same thing.
self._terminated = True
def has_terminated(self):
return self._terminated
def extract(self, location):
"""Sets the given location as writeable in the context, and returns a 'baton' representing
the previous state of context for that location. This 'baton' can be used to later restore
this state of context."""
# Used in `save`.
baton = (
location,
location in self._touched,
self._range.get(location, None),
location in self._writeable,
)
self.set_writeable(location)
return baton
def re_introduce(self, baton):
"""Given a 'baton' produced by `extract()`, restores the context for that saved location
to what it was before `extract()` was called."""
# Used in `save`.
location, was_touched, was_range, was_writeable = baton
if was_touched:
self._touched.add(location)
elif location in self._touched:
self._touched.remove(location)
if was_range is not None:
self._range[location] = was_range
elif location in self._range:
del self._range[location]
if was_writeable:
self._writeable.add(location)
elif location in self._writeable:
self._writeable.remove(location)
def get_assoc(self, pointer):
return self._pointer_assoc.get(pointer)
def set_assoc(self, pointer, table):
self._pointer_assoc[pointer] = table
def is_constant(self, ref):
"""read-only means that the program cannot change the value
of a location. constant means that the value of the location
will not change during the lifetime of the program."""
if isinstance(ref, ConstantRef):
return True
if isinstance(ref, (IndirectRef, IndexedRef)):
return False
if isinstance(ref, LocationRef):
type_ = self.symtab.fetch_global_type(ref.name)
return isinstance(type_, RoutineType)
raise NotImplementedError
def max_range(self, ref):
if isinstance(ref, ConstantRef):
return (ref.value, ref.value)
elif self.symtab.has_local(self.routine.name, ref.name):
return self.symtab.fetch_local_type(self.routine.name, ref.name).max_range
else:
return self.symtab.fetch_global_type(ref.name).max_range
class Analyzer(object):
def __init__(self, symtab, debug=False):
@ -414,7 +100,7 @@ class Analyzer(object):
if isinstance(ref, ConstantRef):
return ref.type
if not isinstance(ref, LocationRef):
raise NotImplementedError
raise NotImplementedError(str(ref))
return self.get_type_for_name(ref.name)
def assert_type(self, type_, *locations):
@ -551,8 +237,10 @@ class Analyzer(object):
self.analyze_save(instr, context)
elif isinstance(instr, PointInto):
self.analyze_point_into(instr, context)
elif isinstance(instr, Reset):
self.analyze_reset(instr, context)
else:
raise NotImplementedError
raise NotImplementedError(str(instr))
def analyze_single_op(self, instr, context):
@ -772,11 +460,12 @@ class Analyzer(object):
# 2. check that the context is meaningful
if isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndirectRef):
context.assert_meaningful(src, REG_Y)
context.assert_meaningful(src, dest.ref, REG_Y)
target = context.get_assoc(dest.ref)
if not target:
raise ForbiddenWriteError(instr, dest.ref)
context.assert_writeable(target)
context.set_touched(target)
context.set_written(target)
@ -788,10 +477,11 @@ class Analyzer(object):
raise UnmeaningfulReadError(instr, src.ref)
context.assert_meaningful(origin)
context.assert_writeable(dest)
context.set_touched(dest)
context.set_written(dest)
elif isinstance(src, IndirectRef) and isinstance(dest, IndirectRef):
context.assert_meaningful(src.ref, REG_Y)
context.assert_meaningful(src.ref, dest.ref, REG_Y)
origin = context.get_assoc(src.ref)
if not origin:
@ -801,6 +491,7 @@ class Analyzer(object):
target = context.get_assoc(dest.ref)
if not target:
raise ForbiddenWriteError(instr, dest.ref)
context.assert_writeable(target)
context.set_touched(target)
context.set_written(target)
@ -1020,11 +711,11 @@ class Analyzer(object):
if context.get_assoc(instr.pointer):
raise ForbiddenWriteError(instr, instr.pointer)
# associate pointer with table, mark it as meaningful.
# associate pointer with table
# (do not mark it as meaningful yet - that's reset's job.)
context.set_assoc(instr.pointer, instr.table)
context.set_meaningful(instr.pointer)
context.set_touched(instr.pointer)
context.set_unmeaningful(instr.pointer)
self.analyze_block(instr.block, context)
if context.encountered_gotos():
@ -1034,3 +725,21 @@ class Analyzer(object):
context.set_assoc(instr.pointer, None)
context.set_unmeaningful(instr.pointer)
def analyze_reset(self, instr, context):
type = self.get_type(instr.pointer)
if not isinstance(type, (PointerType)):
raise TypeMismatchError(instr, instr.pointer.name)
table = context.get_assoc(instr.pointer)
if not table:
raise ForbiddenWriteError(instr, '{} is not associated with any table'.format(instr.pointer.name))
context.assert_meaningful(table)
low_limit, high_limit = context.get_range(table)
assert isinstance(instr.offset, ConstantRef)
if instr.offset.value < low_limit or instr.offset.value > high_limit:
raise RangeExceededError(instr, instr.pointer.name)
context.set_meaningful(instr.pointer)
context.set_touched(instr.pointer)

View File

@ -75,6 +75,10 @@ class SingleOp(Instr):
value_attrs = ('opcode', 'dest', 'src',)
class Reset(Instr):
value_attrs = ('pointer', 'offset',)
class Call(Instr):
value_attrs = ('location',)

View File

@ -1,7 +1,7 @@
# encoding: UTF-8
from sixtypical.ast import (
Program, Routine, Block, SingleOp, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
Program, Routine, Block, SingleOp, Reset, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
)
from sixtypical.model import (
ConstantRef, LocationRef, IndexedRef, IndirectRef,
@ -37,6 +37,7 @@ class Compiler(object):
self.routine_locals = {} # routine.name -> { local.name -> Label }
self.labels = {} # global.name -> Label ("global" includes routines)
self.trampolines = {} # Location -> Label
self.pointer_assoc = {} # pointer name -> table name (I'm not entirely happy about this)
self.current_routine = None
# - - - - helper methods - - - -
@ -191,6 +192,8 @@ class Compiler(object):
return self.compile_save(instr)
elif isinstance(instr, PointInto):
return self.compile_point_into(instr)
elif isinstance(instr, Reset):
return self.compile_reset(instr)
else:
raise NotImplementedError
@ -741,12 +744,16 @@ class Compiler(object):
self.emitter.emit(STA(Absolute(src_label)))
def compile_point_into(self, instr):
src_label = self.get_label(instr.table.name)
self.pointer_assoc[instr.pointer.name] = instr.table.name
self.compile_block(instr.block)
del self.pointer_assoc[instr.pointer.name]
def compile_reset(self, instr):
table_name = self.pointer_assoc[instr.pointer.name]
src_label = Offset(self.get_label(table_name), instr.offset.value)
dest_label = self.get_label(instr.pointer.name)
self.emitter.emit(LDA(Immediate(HighAddressByte(src_label))))
self.emitter.emit(STA(ZeroPage(dest_label)))
self.emitter.emit(LDA(Immediate(LowAddressByte(src_label))))
self.emitter.emit(STA(ZeroPage(Offset(dest_label, 1))))
self.compile_block(instr.block)

328
src/sixtypical/context.py Normal file
View File

@ -0,0 +1,328 @@
# encoding: UTF-8
from sixtypical.model import (
RoutineType, ConstantRef, LocationRef, IndirectRef, IndexedRef,
)
class AnalysisContext(object):
"""
A location is touched if it was changed (or even potentially
changed) during this routine, or some routine called by this routine.
A location is meaningful if it was an input to this routine,
or if it was set to a meaningful value by some operation in this
routine (or some routine called by this routine).
If a location is meaningful, it has a range. This range represents
the lowest and highest values that it might possibly be (i.e. we know
it cannot possibly be below the lowest or above the highest.) In the
absence of any usage information, the range of a byte, is 0..255 and
the range of a word is 0..65535.
A location is writeable if it was listed in the outputs and trashes
lists of this routine. A location can also be temporarily marked
unwriteable in certain contexts, such as `for` loops.
"""
def __init__(self, symtab, routine, inputs, outputs, trashes):
from sixtypical.analyzer import ConstantConstraintError, InconsistentConstraintsError
self.symtab = symtab
self.routine = routine # Routine (AST node)
self._touched = set() # {LocationRef}
self._range = dict() # LocationRef -> (Int, Int)
self._writeable = set() # {LocationRef}
self._terminated = False
self._gotos_encountered = set()
self._pointer_assoc = dict()
for ref in inputs:
if self.is_constant(ref):
raise ConstantConstraintError(self.routine, ref.name)
self._range[ref] = self.max_range(ref)
output_names = set()
for ref in outputs:
if self.is_constant(ref):
raise ConstantConstraintError(self.routine, ref.name)
output_names.add(ref.name)
self._writeable.add(ref)
for ref in trashes:
if self.is_constant(ref):
raise ConstantConstraintError(self.routine, ref.name)
if ref.name in output_names:
raise InconsistentConstraintsError(self.routine, ref.name)
self._writeable.add(ref)
def __str__(self):
return "{}(\n _touched={},\n _range={},\n _writeable={}\n)".format(
self.__class__.__name__,
LocationRef.format_set(self._touched), LocationRef.format_set(self._range), LocationRef.format_set(self._writeable)
)
def to_json_data(self):
type_ = self.symtab.fetch_global_type(self.routine.name)
return {
'routine_inputs': ','.join(sorted(loc.name for loc in type_.inputs)),
'routine_outputs': ','.join(sorted(loc.name for loc in type_.outputs)),
'routine_trashes': ','.join(sorted(loc.name for loc in type_.trashes)),
'touched': ','.join(sorted(loc.name for loc in self._touched)),
'range': dict((loc.name, '{}-{}'.format(rng[0], rng[1])) for (loc, rng) in self._range.items()),
'writeable': ','.join(sorted(loc.name for loc in self._writeable)),
'terminated': self._terminated,
'gotos_encountered': ','.join(sorted(loc.name for loc in self._gotos_encountered)),
}
def clone(self):
c = AnalysisContext(self.symtab, self.routine, [], [], [])
c._touched = set(self._touched)
c._range = dict(self._range)
c._writeable = set(self._writeable)
c._pointer_assoc = dict(self._pointer_assoc)
c._gotos_encountered = set(self._gotos_encountered)
return c
def update_from(self, other):
"""Replaces the information in this context, with the information from the other context.
This is an overwriting action - it does not attempt to merge the contexts.
We do not replace the gotos_encountered for technical reasons. (In `analyze_if`,
we merge those sets afterwards; at the end of `analyze_routine`, they are not distinct in the
set of contexts we are updating from, and we want to retain our own.)"""
self.routine = other.routine
self._touched = set(other._touched)
self._range = dict(other._range)
self._writeable = set(other._writeable)
self._terminated = other._terminated
self._pointer_assoc = dict(other._pointer_assoc)
def each_meaningful(self):
for ref in self._range.keys():
yield ref
def each_touched(self):
for ref in self._touched:
yield ref
def each_writeable(self):
for ref in self._writeable:
yield ref
def assert_meaningful(self, *refs, **kwargs):
from sixtypical.analyzer import UnmeaningfulReadError
exception_class = kwargs.get('exception_class', UnmeaningfulReadError)
for ref in refs:
if self.symtab.has_local(self.routine.name, ref.name):
if ref not in self._range:
message = ref.name
if kwargs.get('message'):
message += ' (%s)' % kwargs['message']
raise exception_class(self.routine, message)
else:
continue
if self.is_constant(ref):
pass
elif isinstance(ref, LocationRef):
if ref not in self._range:
message = ref.name
if kwargs.get('message'):
message += ' (%s)' % kwargs['message']
raise exception_class(self.routine, message)
elif isinstance(ref, IndexedRef):
self.assert_meaningful(ref.ref, **kwargs)
self.assert_meaningful(ref.index, **kwargs)
else:
raise NotImplementedError(ref)
def assert_writeable(self, *refs, **kwargs):
from sixtypical.analyzer import ForbiddenWriteError
exception_class = kwargs.get('exception_class', ForbiddenWriteError)
for ref in refs:
# locals are always writeable
if self.symtab.has_local(self.routine.name, ref.name):
continue
if ref not in self._writeable:
message = ref.name
if kwargs.get('message'):
message += ' (%s)' % kwargs['message']
raise exception_class(self.routine, message)
def assert_in_range(self, inside, outside, offset):
"""Given two locations, assert that the first location, offset by the given offset,
is contained 'inside' the second location."""
from sixtypical.analyzer import RangeExceededError
assert isinstance(inside, LocationRef)
assert isinstance(outside, LocationRef)
# inside should always be meaningful
inside_range = self._range[inside]
# outside might not be meaningful, so default to max range if necessary
if outside in self._range:
outside_range = self._range[outside]
else:
outside_range = self.max_range(outside)
if (inside_range[0] + offset.value) < outside_range[0] or (inside_range[1] + offset.value) > outside_range[1]:
raise RangeExceededError(self.routine,
"Possible range of {} {} (+{}) exceeds acceptable range of {} {}".format(
inside, inside_range, offset, outside, outside_range
)
)
def set_touched(self, *refs):
for ref in refs:
self._touched.add(ref)
# TODO: it might be possible to invalidate the range here
def set_meaningful(self, *refs):
for ref in refs:
if ref not in self._range:
self._range[ref] = self.max_range(ref)
def set_top_of_range(self, ref, top):
self.assert_meaningful(ref)
(bottom, _) = self._range[ref]
self._range[ref] = (bottom, top)
def set_bottom_of_range(self, ref, bottom):
self.assert_meaningful(ref)
(top, _) = self._range[ref]
self._range[ref] = (bottom, top)
def set_range(self, ref, bottom, top):
self.assert_meaningful(ref)
self._range[ref] = (bottom, top)
def get_top_of_range(self, ref):
if isinstance(ref, ConstantRef):
return ref.value
self.assert_meaningful(ref)
(_, top) = self._range[ref]
return top
def get_bottom_of_range(self, ref):
if isinstance(ref, ConstantRef):
return ref.value
self.assert_meaningful(ref)
(bottom, _) = self._range[ref]
return bottom
def get_range(self, ref):
if isinstance(ref, ConstantRef):
return (ref.value, ref.value)
self.assert_meaningful(ref)
(bottom, top) = self._range[ref]
return bottom, top
def copy_range(self, src, dest):
self.assert_meaningful(src)
if src in self._range:
src_range = self._range[src]
else:
src_range = self.max_range(src)
self._range[dest] = src_range
def invalidate_range(self, ref):
self.assert_meaningful(ref)
self._range[ref] = self.max_range(ref)
def set_unmeaningful(self, *refs):
for ref in refs:
if ref in self._range:
del self._range[ref]
def set_written(self, *refs):
"""A "helper" method which does the following common sequence for
the given refs: asserts they're all writable, and sets them all
as touched and meaningful."""
self.assert_writeable(*refs)
self.set_touched(*refs)
self.set_meaningful(*refs)
def set_unwriteable(self, *refs):
"""Intended to be used for implementing analyzing `for`."""
for ref in refs:
self._writeable.remove(ref)
def set_writeable(self, *refs):
"""Intended to be used for implementing analyzing `for`, but also used in `save`."""
for ref in refs:
self._writeable.add(ref)
def encounter_gotos(self, gotos):
self._gotos_encountered |= gotos
def encountered_gotos(self):
return self._gotos_encountered
def set_terminated(self):
# Having a terminated context and having encountered gotos is not the same thing.
self._terminated = True
def has_terminated(self):
return self._terminated
def extract(self, location):
"""Sets the given location as writeable in the context, and returns a 'baton' representing
the previous state of context for that location. This 'baton' can be used to later restore
this state of context."""
# Used in `save`.
baton = (
location,
location in self._touched,
self._range.get(location, None),
location in self._writeable,
)
self.set_writeable(location)
return baton
def re_introduce(self, baton):
"""Given a 'baton' produced by `extract()`, restores the context for that saved location
to what it was before `extract()` was called."""
# Used in `save`.
location, was_touched, was_range, was_writeable = baton
if was_touched:
self._touched.add(location)
elif location in self._touched:
self._touched.remove(location)
if was_range is not None:
self._range[location] = was_range
elif location in self._range:
del self._range[location]
if was_writeable:
self._writeable.add(location)
elif location in self._writeable:
self._writeable.remove(location)
def get_assoc(self, pointer):
return self._pointer_assoc.get(pointer)
def set_assoc(self, pointer, table):
self._pointer_assoc[pointer] = table
def is_constant(self, ref):
"""read-only means that the program cannot change the value
of a location. constant means that the value of the location
will not change during the lifetime of the program."""
if isinstance(ref, ConstantRef):
return True
if isinstance(ref, (IndirectRef, IndexedRef)):
return False
if isinstance(ref, LocationRef):
type_ = self.symtab.fetch_global_type(ref.name)
return isinstance(type_, RoutineType)
raise NotImplementedError
def max_range(self, ref):
if isinstance(ref, ConstantRef):
return (ref.value, ref.value)
elif self.symtab.has_local(self.routine.name, ref.name):
return self.symtab.fetch_local_type(self.routine.name, ref.name).max_range
else:
return self.symtab.fetch_global_type(ref.name).max_range

View File

@ -128,7 +128,7 @@ class Offset(Emittable):
class HighAddressByte(Emittable):
def __init__(self, label):
assert isinstance(label, Label)
assert isinstance(label, (Label, Offset))
self.label = label
def size(self):
@ -143,7 +143,7 @@ class HighAddressByte(Emittable):
class LowAddressByte(Emittable):
def __init__(self, label):
assert isinstance(label, Label)
assert isinstance(label, (Label, Offset))
self.label = label
def size(self):

View File

@ -1,7 +1,7 @@
# encoding: UTF-8
from sixtypical.ast import (
Program, Defn, Routine, Block, SingleOp, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
Program, Defn, Routine, Block, SingleOp, Reset, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
)
from sixtypical.model import (
TYPE_BIT, TYPE_BYTE, TYPE_WORD,
@ -432,6 +432,10 @@ class Parser(object):
final = self.const()
block = self.block()
return For(self.scanner.line_number, dest=dest, direction=direction, final=final, block=block)
elif self.scanner.consume('reset'):
pointer = self.locexpr()
offset = self.const()
return Reset(self.scanner.line_number, pointer=pointer, offset=offset)
elif self.scanner.token in ("ld",):
# the same as add, sub, cmp etc below, except supports an indlocexpr for the src
opcode = self.scanner.token

10
test.sh
View File

@ -1,7 +1,9 @@
#!/bin/sh
falderal --substring-error \
tests/SixtyPical\ Syntax.md \
tests/SixtyPical\ Analysis.md \
tests/SixtyPical\ Fallthru.md \
tests/SixtyPical\ Compilation.md
"tests/SixtyPical Syntax.md" \
"tests/SixtyPical Analysis.md" \
"tests/SixtyPical Storage.md" \
"tests/SixtyPical Control Flow.md" \
"tests/SixtyPical Fallthru.md" \
"tests/SixtyPical Compilation.md"

File diff suppressed because it is too large Load Diff

View File

@ -1400,7 +1400,7 @@ Subtracting a word memory location from another word memory location.
### Tables and Pointers
Load address of table into pointer.
Associate pointer with table. Does nothing by itself.
| byte table[256] tab
| pointer ptr @ 254
@ -1415,6 +1415,24 @@ Load address of table into pointer.
| }
| }
= $080D LDY #$00
= $080F RTS
Reset pointer to table.
| byte table[256] tab
| pointer ptr @ 254
|
| define main routine
| inputs tab
| outputs tab, y
| trashes a, z, n, ptr
| {
| ld y, 0
| point ptr into tab {
| reset ptr 0
| }
| }
= $080D LDY #$00
= $080F LDA #$18
= $0811 STA $FE
= $0813 LDA #$08
@ -1433,6 +1451,7 @@ Write literal through a pointer.
| {
| ld y, 0
| point ptr into tab {
| reset ptr 0
| copy 123, [ptr] + y
| }
| }
@ -1458,6 +1477,7 @@ Write stored value through a pointer.
| {
| ld y, 0
| point ptr into tab {
| reset ptr 0
| copy foo, [ptr] + y
| }
| }
@ -1483,6 +1503,7 @@ Read through a pointer, into a byte storage location, or the `a` register.
| {
| ld y, 0
| point ptr into tab {
| reset ptr 0
| copy [ptr] + y, foo
| ld a, [ptr] + y
| }
@ -1497,6 +1518,39 @@ Read through a pointer, into a byte storage location, or the `a` register.
= $081C LDA ($FE),Y
= $081E RTS
Multiple `reset`s may occur inside the same block.
| byte table[256] tab @ 1024
| pointer ptr @ 254
| byte foo
|
| define main routine
| inputs tab
| outputs y, foo
| trashes a, z, n, ptr
| {
| ld y, 0
| point ptr into tab {
| reset ptr 16
| copy [ptr] + y, foo
| reset ptr 18
| ld a, [ptr] + y
| }
| }
= $080D LDY #$00
= $080F LDA #$10
= $0811 STA $FE
= $0813 LDA #$04
= $0815 STA $FF
= $0817 LDA ($FE),Y
= $0819 STA $0827
= $081C LDA #$12
= $081E STA $FE
= $0820 LDA #$04
= $0822 STA $FF
= $0824 LDA ($FE),Y
= $0826 RTS
Read and write through two pointers.
| byte table[256] tab
@ -1510,7 +1564,9 @@ Read and write through two pointers.
| {
| ld y, 0
| point ptra into tab {
| reset ptra 0
| point ptrb into tab {
| reset ptrb 0
| copy [ptra] + y, [ptrb] + y
| }
| }
@ -1541,6 +1597,7 @@ Write the `a` register through a pointer.
| {
| ld y, 0
| point ptr into tab {
| reset ptr 0
| ld a, 255
| st a, [ptr] + y
| }
@ -1571,6 +1628,7 @@ Note that this is *not* range-checked. (Yet.)
| ld y, 0
| st off, c
| point ptr into tab {
| reset ptr 0
| add ptr, delta
| add ptr, word 1
| copy [ptr] + y, foo
@ -1619,9 +1677,9 @@ Trash does nothing except indicate that we do not care about the value anymore.
= $080E LDA #$00
= $0810 RTS
### static ###
### locals ###
Memory locations defined static to a routine are allocated
Memory locations defined local static to a routine are allocated
just the same as initialized global storage locations are.
| define foo routine
@ -1651,3 +1709,66 @@ just the same as initialized global storage locations are.
= $081D RTS
= $081E .byte $FF
= $081F .byte $07
Memory locations defined local dynamic to a routine are allocated
just the same as uninitialized global storage locations are.
| define foo routine
| inputs x
| outputs x
| trashes z, n
| local byte t
| {
| st x, t
| inc t
| ld x, t
| }
|
| define main routine
| trashes a, x, z, n
| local byte t
| {
| ld x, 0
| st x, t
| call foo
| }
= $080D LDX #$00
= $080F STX $0821
= $0812 JSR $0816
= $0815 RTS
= $0816 STX $0820
= $0819 INC $0820
= $081C LDX $0820
= $081F RTS
Memory locations defined local dynamic to a routine are allocated
just the same as uninitialized global storage locations are, even
when declared with a fixed address.
| define foo routine
| inputs x
| outputs x
| trashes z, n
| local byte t @ 1024
| {
| st x, t
| inc t
| ld x, t
| }
|
| define main routine
| trashes a, x, z, n
| local byte t @ 1025
| {
| ld x, 0
| st x, t
| call foo
| }
= $080D LDX #$00
= $080F STX $0401
= $0812 JSR $0816
= $0815 RTS
= $0816 STX $0400
= $0819 INC $0400
= $081C LDX $0400
= $081F RTS

File diff suppressed because it is too large Load Diff

1922
tests/SixtyPical Storage.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -176,6 +176,7 @@ Other blocks.
| ld a, 0
| }
| point ptr into tab {
| reset ptr 0
| ld a, [ptr] + y
| }
| }
@ -681,6 +682,7 @@ Tables and pointers.
|
| define main routine {
| point ptr into buf {
| reset ptr 0
| copy 123, [ptr] + y
| copy [ptr] + y, foo
| copy [ptr] + y, [ptrb] + y
@ -784,6 +786,20 @@ Local static memory locations must always be given an initial value.
| }
? SyntaxError
Local static memory locations may not be given an address.
| define main routine
| inputs x
| outputs x
| trashes z, n
| static byte t @ 1024
| {
| st x, t
| inc t
| ld x, t
| }
? SyntaxError
Local dynamic memory locations may not be given an initial value.
| define main routine
@ -798,6 +814,20 @@ Local dynamic memory locations may not be given an initial value.
| }
? SyntaxError
Local dynamic memory locations may be given an address.
| define main routine
| inputs x
| outputs x
| trashes z, n
| local byte t @ 1024
| {
| st x, t
| inc t
| ld x, t
| }
= ok
Name of a local cannot shadow an existing global or local.
| byte t