1
0
mirror of https://github.com/catseye/SixtyPical.git synced 2024-06-07 06:29:32 +00:00

Collect exit contexts. Disturbs fallthru analysis, but otherwise?

This commit is contained in:
Chris Pressey 2018-11-27 16:59:16 +00:00
parent 0ca545c89a
commit 21e623f3ad
2 changed files with 104 additions and 22 deletions

View File

@ -101,6 +101,7 @@ class Context(object):
self._touched = set()
self._range = dict()
self._writeable = set()
self._terminated = False
self._gotos_encountered = set()
for ref in inputs:
@ -132,6 +133,13 @@ class Context(object):
c._writeable = set(self._writeable)
return c
def update_from(self, other):
self.routines = other.routines
self.routine = other.routine
self._touched = set(other._touched)
self._range = dict(other._range)
self._writeable = set(other._writeable)
def each_meaningful(self):
for ref in self._range.keys():
yield ref
@ -279,6 +287,13 @@ class Context(object):
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 assert_types_for_read_table(self, instr, src, dest, type_):
if (not TableType.is_a_table_type(src.ref.type, type_)) or (not dest.type == type_):
raise TypeMismatchError(instr, '{} and {}'.format(src.ref.name, dest.name))
@ -364,19 +379,20 @@ class Analyzer(object):
def analyze_routine(self, routine):
assert isinstance(routine, Routine)
self.current_routine = routine
if routine.block is None:
# it's an extern, that's fine
return
self.current_routine = routine
type_ = routine.location.type
context = Context(self.routines, routine, type_.inputs, type_.outputs, type_.trashes)
self.exit_contexts = []
if self.debug:
print("at start of routine `{}`:".format(routine.name))
print(context)
self.analyze_block(routine.block, context)
trashed = set(context.each_touched()) - set(context.each_meaningful())
if self.debug:
print("at end of routine `{}`:".format(routine.name))
@ -390,6 +406,20 @@ class Analyzer(object):
print('-' * 79)
print('')
if self.exit_contexts:
# check that they are all consistent
exit_context = self.exit_contexts[0]
exit_meaningful = set(exit_context.each_meaningful())
exit_touched = set(exit_context.each_touched())
for ex in self.exit_contexts[1:]:
if set(ex.each_meaningful()) != exit_meaningful:
raise InconsistentInitializationError('?')
if set(ex.each_touched()) != exit_touched:
raise InconsistentInitializationError('?')
context.update_from(exit_context)
trashed = set(context.each_touched()) - set(context.each_meaningful())
# these all apply whether we encountered goto(s) in this routine, or not...:
# can't trash an output.
@ -406,6 +436,7 @@ class Analyzer(object):
if ref not in type_.outputs and ref not in type_.trashes and not routine_has_static(routine, ref):
raise ForbiddenWriteError(routine, ref.name)
self.exit_contexts = None
self.current_routine = None
return context
@ -438,6 +469,9 @@ class Analyzer(object):
dest = instr.dest
src = instr.src
if context.has_terminated():
raise IllegalJumpError(instr, instr) # TODO: maybe a better name for this
if opcode == 'ld':
if isinstance(src, IndexedRef):
context.assert_types_for_read_table(instr, src, dest, TYPE_BYTE)
@ -677,26 +711,38 @@ class Analyzer(object):
context.encounter_gotos(set([instr.location]))
# now that we have encountered a goto here, we set the
# Now that we have encountered a goto, we update the
# context here to match what someone calling the goto'ed
# function directly, would expect. (which makes sense
# when you think about it; if this goto's F, then calling
# this is like calling F, from the perspective of what is
# returned.
# returned.)
#
# However, this isn't the current context anymore. This
# is an exit context of this routine.
exit_context = context.clone()
for ref in type_.outputs:
context.set_touched(ref) # ?
context.set_written(ref)
exit_context.set_touched(ref) # ?
exit_context.set_written(ref)
for ref in type_.trashes:
context.assert_writeable(ref)
context.set_touched(ref)
context.set_unmeaningful(ref)
exit_context.assert_writeable(ref)
exit_context.set_touched(ref)
exit_context.set_unmeaningful(ref)
# TODO is that... all we have to do? You'll note the above
# is a lot like call. We do rely on, if we are in a branch,
# the branch-merge to take care of... a lot? The fact that
# we don't actually continue on from here, I mean.
self.exit_contexts.append(exit_context)
# When we get to the end, we'll check that all the
# exit contexts are consistent with each other.
# We set the current context as having terminated.
# If we are in a branch, the merge will deal with
# having terminated. If we are at the end of the
# routine, the routine end will deal with that.
context.set_terminated()
elif opcode == 'trash':
context.set_touched(instr.dest)
@ -736,11 +782,21 @@ class Analyzer(object):
message='initialized in block 2 but not in block 1 of `if {}`'.format(instr.src)
)
# merge the contexts. this used to be a method called `set_from`
context._touched = set(context1._touched) | set(context2._touched)
context.set_meaningful(*list(outgoing_meaningful))
context._writeable = set(context1._writeable) | set(context2._writeable)
context.encounter_gotos(context1.encountered_gotos() | context2.encountered_gotos())
# merge the contexts.
# first, the easy case: if one of the contexts has terminated, just use the other one.
# if both have terminated, we return a terminated context, and that's OK.
if context1.has_terminated():
context.update_from(context2)
elif context2.has_terminated():
context.update_from(context1)
else:
# the more complicated case: merge the contents of the contexts.
context._touched = set(context1._touched) | set(context2._touched)
context.set_meaningful(*list(outgoing_meaningful))
context._writeable = set(context1._writeable) | set(context2._writeable)
context.encounter_gotos(context1.encountered_gotos() | context2.encountered_gotos())
for ref in outgoing_trashes:
context.set_touched(ref)
@ -753,15 +809,15 @@ class Analyzer(object):
if instr.src is not None: # None indicates 'repeat forever'
context.assert_meaningful(instr.src)
if context.encountered_gotos():
raise IllegalJumpError(instr, instr)
# now analyze it having been executed a second time, with the context
# of it having already been executed.
self.analyze_block(instr.block, context)
if instr.src is not None:
context.assert_meaningful(instr.src)
if context.encountered_gotos():
raise IllegalJumpError(instr, instr)
def analyze_for(self, instr, context):
context.assert_meaningful(instr.dest)
context.assert_writeable(instr.dest)

View File

@ -3174,7 +3174,33 @@ Here, we declare that main outputs a, but we goto a routine that does not output
| }
| ld a, 1
| }
? UnmeaningfulReadError
? UnmeaningfulOutputError: a
Here, we declare that main outputs a, and we goto a routine that outputs a so that's OK.
| define bar routine
| inputs x
| outputs a
| trashes x, z, n
| {
| ld x, 200
| ld a, 1
| }
|
| define main routine
| outputs a
| trashes x, z, n
| {
| ld x, 0
| if z {
| ld x, 1
| goto bar
| } else {
| ld x, 2
| }
| ld a, 1
| }
= ok
TODO: we should have a lot more test cases for the above, here.