1
0
mirror of https://github.com/catseye/SixtyPical.git synced 2025-02-16 15:30:26 +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._touched = set()
self._range = dict() self._range = dict()
self._writeable = set() self._writeable = set()
self._terminated = False
self._gotos_encountered = set() self._gotos_encountered = set()
for ref in inputs: for ref in inputs:
@ -132,6 +133,13 @@ class Context(object):
c._writeable = set(self._writeable) c._writeable = set(self._writeable)
return c 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): def each_meaningful(self):
for ref in self._range.keys(): for ref in self._range.keys():
yield ref yield ref
@ -279,6 +287,13 @@ class Context(object):
def encountered_gotos(self): def encountered_gotos(self):
return self._gotos_encountered 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_): 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_): 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)) raise TypeMismatchError(instr, '{} and {}'.format(src.ref.name, dest.name))
@ -364,19 +379,20 @@ class Analyzer(object):
def analyze_routine(self, routine): def analyze_routine(self, routine):
assert isinstance(routine, Routine) assert isinstance(routine, Routine)
self.current_routine = routine
if routine.block is None: if routine.block is None:
# it's an extern, that's fine # it's an extern, that's fine
return return
self.current_routine = routine
type_ = routine.location.type type_ = routine.location.type
context = Context(self.routines, routine, type_.inputs, type_.outputs, type_.trashes) context = Context(self.routines, routine, type_.inputs, type_.outputs, type_.trashes)
self.exit_contexts = []
if self.debug: if self.debug:
print("at start of routine `{}`:".format(routine.name)) print("at start of routine `{}`:".format(routine.name))
print(context) print(context)
self.analyze_block(routine.block, context) self.analyze_block(routine.block, context)
trashed = set(context.each_touched()) - set(context.each_meaningful())
if self.debug: if self.debug:
print("at end of routine `{}`:".format(routine.name)) print("at end of routine `{}`:".format(routine.name))
@ -390,6 +406,20 @@ class Analyzer(object):
print('-' * 79) print('-' * 79)
print('') 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...: # these all apply whether we encountered goto(s) in this routine, or not...:
# can't trash an output. # 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): if ref not in type_.outputs and ref not in type_.trashes and not routine_has_static(routine, ref):
raise ForbiddenWriteError(routine, ref.name) raise ForbiddenWriteError(routine, ref.name)
self.exit_contexts = None
self.current_routine = None self.current_routine = None
return context return context
@ -438,6 +469,9 @@ class Analyzer(object):
dest = instr.dest dest = instr.dest
src = instr.src src = instr.src
if context.has_terminated():
raise IllegalJumpError(instr, instr) # TODO: maybe a better name for this
if opcode == 'ld': if opcode == 'ld':
if isinstance(src, IndexedRef): if isinstance(src, IndexedRef):
context.assert_types_for_read_table(instr, src, dest, TYPE_BYTE) context.assert_types_for_read_table(instr, src, dest, TYPE_BYTE)
@ -677,26 +711,38 @@ class Analyzer(object):
context.encounter_gotos(set([instr.location])) 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 # context here to match what someone calling the goto'ed
# function directly, would expect. (which makes sense # function directly, would expect. (which makes sense
# when you think about it; if this goto's F, then calling # when you think about it; if this goto's F, then calling
# this is like calling F, from the perspective of what is # 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: for ref in type_.outputs:
context.set_touched(ref) # ? exit_context.set_touched(ref) # ?
context.set_written(ref) exit_context.set_written(ref)
for ref in type_.trashes: for ref in type_.trashes:
context.assert_writeable(ref) exit_context.assert_writeable(ref)
context.set_touched(ref) exit_context.set_touched(ref)
context.set_unmeaningful(ref) exit_context.set_unmeaningful(ref)
# TODO is that... all we have to do? You'll note the above self.exit_contexts.append(exit_context)
# 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 # When we get to the end, we'll check that all the
# we don't actually continue on from here, I mean. # 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': elif opcode == 'trash':
context.set_touched(instr.dest) 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) 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` # merge the contexts.
context._touched = set(context1._touched) | set(context2._touched)
context.set_meaningful(*list(outgoing_meaningful)) # first, the easy case: if one of the contexts has terminated, just use the other one.
context._writeable = set(context1._writeable) | set(context2._writeable) # if both have terminated, we return a terminated context, and that's OK.
context.encounter_gotos(context1.encountered_gotos() | context2.encountered_gotos())
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: for ref in outgoing_trashes:
context.set_touched(ref) context.set_touched(ref)
@ -753,15 +809,15 @@ class Analyzer(object):
if instr.src is not None: # None indicates 'repeat forever' if instr.src is not None: # None indicates 'repeat forever'
context.assert_meaningful(instr.src) 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 # now analyze it having been executed a second time, with the context
# of it having already been executed. # of it having already been executed.
self.analyze_block(instr.block, context) self.analyze_block(instr.block, context)
if instr.src is not None: if instr.src is not None:
context.assert_meaningful(instr.src) context.assert_meaningful(instr.src)
if context.encountered_gotos():
raise IllegalJumpError(instr, instr)
def analyze_for(self, instr, context): def analyze_for(self, instr, context):
context.assert_meaningful(instr.dest) context.assert_meaningful(instr.dest)
context.assert_writeable(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 | 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. TODO: we should have a lot more test cases for the above, here.