diff --git a/HISTORY.md b/HISTORY.md index ea5938b..36bf490 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,30 @@ 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. +* Along with `goto`, `call` and `with interrupts off` are + now forbidden inside a `with interrupts off` block. +* More tests to assure that using `call` inside a `point into` + block or inside a `for` block does not cause trouble, + particularly when the routine being called also uses the + variable named in that block. +* Fixed a bug where two local statics could be declared with + the same name. +* Split analysis context support off from analyzer, and + symbol table support from parse, and it their own modules. +* Split the SixtyPical Analysis tests across three files, + and placed test appliances for `sixtypical` in own file. + 0.19 ---- diff --git a/LICENSE b/LICENSE index 9d5dd32..23aa2a4 100644 --- a/LICENSE +++ b/LICENSE @@ -7,7 +7,7 @@ covered by the following BSD-compatible license, modelled after the ----------------------------------------------------------------------------- - Copyright (c)2014-2018 Chris Pressey, Cat's Eye Technologies. + Copyright (c)2014-2019 Chris Pressey, Cat's Eye Technologies. The authors intend this Report to belong to the entire SixtyPical community, and so we grant permission to copy and distribute it for @@ -24,7 +24,7 @@ The source code for the reference implementation and supporting tools (in the ----------------------------------------------------------------------------- - Copyright (c)2014-2018, Chris Pressey, Cat's Eye Technologies. + Copyright (c)2014-2019, Chris Pressey, Cat's Eye Technologies. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 7be91d3..ab177fc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ SixtyPical ========== -_Version 0.19. Work-in-progress, everything is subject to change._ +_Version 0.20. Work-in-progress, everything is subject to change._ **SixtyPical** is a [low-level](#low-level) programming language supporting a sophisticated [static analysis](#static-analysis). @@ -109,7 +109,9 @@ In order to run the tests for compilation, [dcc6502][] needs to be installed. * [SixtyPical specification](doc/SixtyPical.md) * [Literate test suite for SixtyPical syntax](tests/SixtyPical%20Syntax.md) -* [Literate test suite for SixtyPical analysis](tests/SixtyPical%20Analysis.md) +* [Literate test suite for SixtyPical analysis (operations)](tests/SixtyPical%20Analysis.md) +* [Literate test suite for SixtyPical analysis (storage)](tests/SixtyPical%20Storage.md) +* [Literate test suite for SixtyPical analysis (control flow)](tests/SixtyPical%20Control%20Flow.md) * [Literate test suite for SixtyPical compilation](tests/SixtyPical%20Compilation.md) * [Literate test suite for SixtyPical fallthru optimization](tests/SixtyPical%20Fallthru.md) diff --git a/TODO.md b/TODO.md index 34ed5e5..1761bde 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,9 @@ TODO for SixtyPical =================== +Language +-------- + ### Save values to other-than-the-stack Allow @@ -9,35 +12,111 @@ Allow ... } -Which uses some other storage location instead of the stack. A local static -would be a good candidate for such. +Which uses some other storage location instead of the stack. A local non-static +would be a good candidate for such. At any rate, the location must not +be writeable by anything that is called from within the block. So, probably +just restrict this to local non-statics. -### Analyze `call` within blocks? +### Copy byte to/from table -What happens if you call another routine from inside a `with interrupts off` block? +Do we want a `copy bytevar, table + x` instruction? We don't currently have one. +You have to `ld a`, `st a`. I think maybe we should have one. -What happens if you call another routine from inside a `save` block? +### Character literals -What happens if you call another routine from inside a `point into` block? +For goodness sake, let the programmer say `'A'` instead of `65`. -What happens if you call another routine from inside a `for` block? +### Character set mapping -Remember that any of these may have a `goto` ... and they may have a second -instance of the same block (e.g. `with interrupts off` nested within -`with interrupts off` shouldn't be allowed to turn them back on after the -inner block has finished -- even if there is no `call`.) +Not all computers think `'A'` should be `65`. Allow the character set to be +mapped. Probably copy what Ophis does. -These holes need to be plugged. +### "Include" directives -### Reset pointer in `point into` blocks +Search a searchlist of include paths. And use them to make libraries of routines. -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. +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. -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 into non-byte tables -### Pointers associated globally with a table +Right now you cannot get a pointer into a non-byte (for instance, word or vector) table. + +Word and vector tables are stored as two byte tables in memory. This is useful for +indexed access, but makes pointer access more difficult. + +Laying them out for pointer access would make indexed access more difficult. + +### Saving non-byte values + +Right now you cannot save a word value. + +There doesn't seem to be a hugely pressing reason why not. + +Analysis +-------- + +### Forbid recursion + +What happens if a routine calls itself, directly or indirectly? Many +constraints might be violated in this case. We should probably disallow +recursion by default. (Which means assembling the callgraph in all cases.) + +### Analyze memory usage + +If you define two variables that occupy the same address, an analysis error ought +to be raised. (But there should also be a way to annotate this as intentional. +Intentionally making two tables overlap could be valuable. However, the analysis +will probably completely miss this fact.) + +Optimization +------------ + +### Space optimization of local non-statics + +If there are two routines A and B, and A never calls B (even indirectly), and +B never calls A (even indirectly), then their non-static locals can +be allocated at the same space. + +This is not just an impressive trick -- in the presence of local pointers, which +use up a word in zero-page, which we consider a precious resource, it allow those +zero-page locations to be re-used. + +### Tail-call optimization + +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. + +As long as the routine has consistent type context every place it exits, that should be fine. + +### Branch optimization in `if` + +Currently the `if` generator is not smart enough to avoid generating silly +jump instructions. (See the Fallthru tests.) Improve it. + +### Dead code removal + +Once we have a call graph we can omit routines that we're sure aren't called. + +This would let us use include-files and standard-libraries nicely: any +routines they define, but that you don't use, don't get included. + +Analyzing the set of possible routines that a vector can take on would help +this immensely. + +Implementation +-------------- + +### 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. + +Blue-skying +----------- + +### 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. @@ -53,66 +132,5 @@ at different times. These can co-exist with general, non-specific-table-linked `pointer` variables. -### Local non-statics - -Somewhat related to the above, it should be possible to declare a local storage -location which is not static. - -In this case, it would be considered uninitialized each time the routine was -entered. - -So, you do not have a guarantee that it has a valid value. But you are guaranteed -that no other routine can read or modify it. - -It also enables a trick: if there are two routines A and B, and A never calls B -(even indirectly), and B never calls A (even indirectly), then their locals can -be allocated at the same space. - -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. -You have to `ld a`, `st a`. I think maybe we should have one. - -### Analyze memory usage - -If you define two variables that occupy the same address, an analysis error ought -to be raised. (But there should also be a way to annotate this as intentional. -Intentionally making two tables overlap could be valuable. However, the analysis -will probably completely miss this fact.) - -### Character literals - -For goodness sake, let the programmer say `'A'` instead of `65`. - -### Character set mapping - -Not all computers think `'A'` should be `65`. Allow the character set to be -mapped. Probably copy what Ophis does. - -### Tail-call optimization - -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. - -As long as the routine has consistent type context every place it exits, that should be fine. - -### "Include" directives - -Search a searchlist of include paths. And use them to make libraries of routines. - -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. +If we have local pointers and space optimization for local non-statics, though, +these don't add as much. diff --git a/bin/sixtypical b/bin/sixtypical index 2777b8f..c847389 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -16,7 +16,8 @@ import sys from tempfile import NamedTemporaryFile import traceback -from sixtypical.parser import Parser, SymbolTable, merge_programs +from sixtypical.symtab import SymbolTable +from sixtypical.parser import Parser, merge_programs from sixtypical.analyzer import Analyzer from sixtypical.outputter import outputter_class_for from sixtypical.compiler import Compiler @@ -184,7 +185,7 @@ if __name__ == '__main__': argparser.add_argument( "--version", action="version", - version="%(prog)s 0.19" + version="%(prog)s 0.20" ) options, unknown = argparser.parse_known_args(sys.argv[1:]) diff --git a/doc/SixtyPical.md b/doc/SixtyPical.md index 1107ef4..5608fcd 100644 --- a/doc/SixtyPical.md +++ b/doc/SixtyPical.md @@ -1,7 +1,7 @@ SixtyPical ========== -This document describes the SixtyPical programming language version 0.19, +This document describes the SixtyPical programming language version 0.20, both its static semantics (the capabilities and limits of the static analyses it defines) and its runtime semantics (with reference to the semantics of 6502 machine code.) @@ -196,7 +196,8 @@ table pointed to is implemented with "indirect indexed" addressing, as in There are extended instruction modes for using these types of memory location. See `copy` below, but here is some illustrative example code: - point ptr into buf { // this is the only way to initialize a pointer + point ptr into buf { // this associates this pointer with this table + reset ptr 0 // this is the only way to initialize a pointer add ptr, 4 // note, this is unchecked against table's size! ld y, 0 // you must set this to something yourself copy [ptr] + y, byt // read memory through pointer, into byte @@ -658,4 +659,6 @@ Grammar | "repeat" Block ("until" ["not"] LocExpr | "forever") | "for" LocExpr ("up"|"down") "to" Const Block | "with" "interrupts" LitBit Block + | "point" LocExpr "into" LocExpr Block + | "reset" LocExpr Const . diff --git a/eg/c64/demo-game/demo-game.60p b/eg/c64/demo-game/demo-game.60p index 3135895..31d9f97 100644 --- a/eg/c64/demo-game/demo-game.60p +++ b/eg/c64/demo-game/demo-game.60p @@ -265,37 +265,37 @@ define player_logic logic_routine if c { point ptr into screen { + reset ptr 0 st off, c add ptr, new_pos ld y, 0 + // check collision. ld a, [ptr] + y - } - // if "collision" is with your own self, treat it as if it's blank space! - cmp a, 81 - if z { - ld a, 32 - } - cmp a, 32 - if z { - point ptr into screen { + // if "collision" is with your own self, treat it as if it's blank space! + cmp a, 81 + if z { + ld a, 32 + } + cmp a, 32 + if z { + reset ptr 0 st off, c add ptr, pos copy 32, [ptr] + y - } - copy new_pos, pos + copy new_pos, pos - point ptr into screen { + reset ptr 0 st off, c add ptr, pos copy 81, [ptr] + y } - } else { - ld a, 1 - st a, player_died } + } else { + ld a, 1 + st a, player_died } } @@ -307,28 +307,29 @@ define enemy_logic logic_routine if c { point ptr into screen { + reset ptr 0 st off, c add ptr, new_pos ld y, 0 + // check collision. ld a, [ptr] + y - } - // if "collision" is with your own self, treat it as if it's blank space! - cmp a, 82 - if z { - ld a, 32 - } - cmp a, 32 - if z { - point ptr into screen { + + // if "collision" is with your own self, treat it as if it's blank space! + cmp a, 82 + if z { + ld a, 32 + } + cmp a, 32 + if z { + reset ptr 0 st off, c add ptr, pos copy 32, [ptr] + y - } - copy new_pos, pos + copy new_pos, pos - point ptr into screen { + reset ptr 0 st off, c add ptr, pos copy 82, [ptr] + y diff --git a/eg/rudiments/buffer.60p b/eg/rudiments/buffer.60p index 72ff18a..18eb6d8 100644 --- a/eg/rudiments/buffer.60p +++ b/eg/rudiments/buffer.60p @@ -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 diff --git a/eg/rudiments/nested-for.60p b/eg/rudiments/nested-for.60p new file mode 100644 index 0000000..833f09d --- /dev/null +++ b/eg/rudiments/nested-for.60p @@ -0,0 +1,33 @@ +// Include `support/${PLATFORM}.60p` before this source +// Should print H (being ASCII 72 = 8 * 9) + +// Increase y by 7, circuitously +// +define foo routine + inputs y + outputs y, n, z + trashes a, c +{ + save x { + ld x, 0 + for x up to 6 { + inc y + } + } +} + +// Each iteration increases y by 8; there are 9 iterations +// +define main routine + outputs x, y, n, z + trashes a, c +{ + ld x, 0 + ld y, 0 + for x up to 8 { + inc y + call foo + } + ld a, y + call chrout +} diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 197b68c..1e3991b 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -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,316 +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: - # statics are always meaningful - if self.symtab.has_static(self.routine.name, ref.name): - 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: - # statics are always writeable - if self.symtab.has_static(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_static(self.routine.name, ref.name): - return self.symtab.fetch_static_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): @@ -401,15 +92,15 @@ class Analyzer(object): # - - - - helper methods - - - - def get_type_for_name(self, name): - if self.current_routine and self.symtab.has_static(self.current_routine.name, name): - return self.symtab.fetch_static_type(self.current_routine.name, name) + if self.current_routine and self.symtab.has_local(self.current_routine.name, name): + return self.symtab.fetch_local_type(self.current_routine.name, name) return self.symtab.fetch_global_type(name) def get_type(self, ref): 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): @@ -462,6 +153,14 @@ class Analyzer(object): self.current_routine = routine type_ = self.get_type_for_name(routine.name) context = AnalysisContext(self.symtab, routine, type_.inputs, type_.outputs, type_.trashes) + + # register any local statics as already-initialized + for local_name, local_symentry in self.symtab.locals.get(routine.name, {}).items(): + ref = self.symtab.fetch_local_ref(routine.name, local_name) + if local_symentry.ast_node.initial is not None: + context.set_meaningful(ref) + context.set_range(ref, local_symentry.ast_node.initial, local_symentry.ast_node.initial) + self.exit_contexts = [] self.analyze_block(routine.block, context) @@ -505,7 +204,7 @@ class Analyzer(object): # if something was touched, then it should have been declared to be writable. for ref in context.each_touched(): - if ref not in type_.outputs and ref not in type_.trashes and not self.symtab.has_static(routine.name, ref.name): + if ref not in type_.outputs and ref not in type_.trashes and not self.symtab.has_local(routine.name, ref.name): raise ForbiddenWriteError(routine, ref.name) self.exit_contexts = None @@ -531,15 +230,15 @@ class Analyzer(object): elif isinstance(instr, For): self.analyze_for(instr, context) elif isinstance(instr, WithInterruptsOff): - self.analyze_block(instr.block, context) - if context.encountered_gotos(): - raise IllegalJumpError(instr, instr) + self.analyze_with_interrupts_off(instr, context) elif isinstance(instr, Save): 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): @@ -589,7 +288,6 @@ class Analyzer(object): target = context.get_assoc(dest.ref) if not target: raise ForbiddenWriteError(instr, dest.ref) - context.set_touched(target) context.set_written(target) elif self.get_type(src) != self.get_type(dest): @@ -759,12 +457,11 @@ 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.set_touched(target) context.set_written(target) elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef): @@ -775,10 +472,10 @@ class Analyzer(object): raise UnmeaningfulReadError(instr, src.ref) context.assert_meaningful(origin) - 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: @@ -788,7 +485,6 @@ class Analyzer(object): target = context.get_assoc(dest.ref) if not target: raise ForbiddenWriteError(instr, dest.ref) - context.set_touched(target) context.set_written(target) elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef): @@ -799,7 +495,6 @@ class Analyzer(object): context.set_written(dest.ref) elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef): context.assert_meaningful(src.ref, src.index) - context.set_touched(dest) context.set_written(dest) else: context.assert_meaningful(src) @@ -865,7 +560,6 @@ class Analyzer(object): exit_context = context.clone() for ref in type_.outputs: - exit_context.set_touched(ref) # ? exit_context.set_written(ref) for ref in type_.trashes: @@ -973,6 +667,13 @@ class Analyzer(object): context.set_range(instr.dest, instr.final, instr.final) context.set_writeable(instr.dest) + def analyze_with_interrupts_off(self, instr, context): + block = instr.block + for instr in block.instrs: + if isinstance(instr, (Call, GoTo, WithInterruptsOff)): + raise IllegalJumpError(instr, instr) + self.analyze_instr(instr, context) + def analyze_save(self, instr, context): batons = [] for location in instr.locations: @@ -1007,11 +708,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(): @@ -1021,3 +722,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) diff --git a/src/sixtypical/ast.py b/src/sixtypical/ast.py index fc5f96f..bfaf621 100644 --- a/src/sixtypical/ast.py +++ b/src/sixtypical/ast.py @@ -59,7 +59,7 @@ class Defn(AST): class Routine(AST): value_attrs = ('name', 'addr', 'initial',) - children_attrs = ('statics',) + children_attrs = ('locals',) child_attrs = ('block',) @@ -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',) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index e033cb1..f9fac26 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -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, @@ -34,16 +34,17 @@ class Compiler(object): self.symtab = symtab self.emitter = emitter self.routines = {} # routine.name -> Routine - self.routine_statics = {} # routine.name -> { static.name -> Label } + 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 - - - - def get_type_for_name(self, name): - if self.current_routine and self.symtab.has_static(self.current_routine.name, name): - return self.symtab.fetch_static_type(self.current_routine.name, name) + if self.current_routine and self.symtab.has_local(self.current_routine.name, name): + return self.symtab.fetch_local_type(self.current_routine.name, name) return self.symtab.fetch_global_type(name) def get_type(self, ref): @@ -76,9 +77,9 @@ class Compiler(object): def get_label(self, name): if self.current_routine: - static_label = self.routine_statics.get(self.current_routine.name, {}).get(name) - if static_label: - return static_label + local_label = self.routine_locals.get(self.current_routine.name, {}).get(name) + if local_label: + return local_label return self.labels[name] def absolute_or_zero_page(self, label): @@ -107,16 +108,15 @@ class Compiler(object): label.set_addr(routine.addr) self.labels[routine.name] = label - if hasattr(routine, 'statics'): - self.current_routine = routine - static_labels = {} - for defn in routine.statics: - length = self.compute_length_of_defn(defn) - label = Label(defn.name, addr=defn.addr, length=length) - static_labels[defn.name] = label - declarations.append((defn, self.symtab.fetch_static_type(routine.name, defn.name), label)) - self.routine_statics[routine.name] = static_labels - self.current_routine = None + self.current_routine = routine + local_labels = {} + for defn in routine.locals: + length = self.compute_length_of_defn(defn) + label = Label(defn.name, addr=defn.addr, length=length) + local_labels[defn.name] = label + declarations.append((defn, self.symtab.fetch_local_type(routine.name, defn.name), label)) + self.routine_locals[routine.name] = local_labels + self.current_routine = None if compilation_roster is None: compilation_roster = [['main']] + [[routine.name] for routine in program.routines if routine.name != 'main'] @@ -192,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 @@ -742,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) diff --git a/src/sixtypical/context.py b/src/sixtypical/context.py new file mode 100644 index 0000000..a92c513 --- /dev/null +++ b/src/sixtypical/context.py @@ -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 diff --git a/src/sixtypical/emitter.py b/src/sixtypical/emitter.py index 5b4d346..e574640 100644 --- a/src/sixtypical/emitter.py +++ b/src/sixtypical/emitter.py @@ -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): diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 8a8cb29..0cf7107 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -1,23 +1,15 @@ # 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, RoutineType, VectorType, TableType, PointerType, - LocationRef, ConstantRef, IndirectRef, IndexedRef, + ConstantRef, IndirectRef, IndexedRef, ) from sixtypical.scanner import Scanner - - -class SymEntry(object): - def __init__(self, ast_node, type_): - self.ast_node = ast_node - self.type_ = type_ - - def __repr__(self): - return "%s(%r, %r)" % (self.__class__.__name__, self.ast_node, self.type_) +from sixtypical.symtab import SymEntry class ForwardReference(object): @@ -28,42 +20,6 @@ class ForwardReference(object): return "%s(%r)" % (self.__class__.__name__, self.name) -class SymbolTable(object): - def __init__(self): - self.symbols = {} # symbol name -> SymEntry - self.statics = {} # routine name -> (symbol name -> SymEntry) - self.typedefs = {} # type name -> Type AST - self.consts = {} # const name -> ConstantRef - - for name in ('a', 'x', 'y'): - self.symbols[name] = SymEntry(None, TYPE_BYTE) - for name in ('c', 'z', 'n', 'v'): - self.symbols[name] = SymEntry(None, TYPE_BIT) - - def __str__(self): - return "Symbols: {}\nStatics: {}\nTypedefs: {}\nConsts: {}".format(self.symbols, self.statics, self.typedefs, self.consts) - - def has_static(self, routine_name, name): - return name in self.statics.get(routine_name, {}) - - def fetch_global_type(self, name): - return self.symbols[name].type_ - - def fetch_static_type(self, routine_name, name): - return self.statics[routine_name][name].type_ - - def fetch_global_ref(self, name): - if name in self.symbols: - return LocationRef(name) - return None - - def fetch_static_ref(self, routine_name, name): - routine_statics = self.statics.get(routine_name, {}) - if name in routine_statics: - return LocationRef(name) - return None - - class Parser(object): def __init__(self, symtab, text, filename): self.symtab = symtab @@ -76,7 +32,7 @@ class Parser(object): def lookup(self, name, allow_forward=False, routine_name=None): model = self.symtab.fetch_global_ref(name) if model is None and routine_name: - model = self.symtab.fetch_static_ref(routine_name, name) + model = self.symtab.fetch_local_ref(routine_name, name) if model is None and allow_forward: return ForwardReference(name) if model is None: @@ -88,10 +44,12 @@ class Parser(object): self.syntax_error('Symbol "%s" already declared' % name) self.symtab.symbols[name] = SymEntry(ast_node, type_) - def declare_static(self, routine_name, name, ast_node, type_): + def declare_local(self, routine_name, name, ast_node, type_): + if self.symtab.fetch_local_ref(routine_name, name): + self.syntax_error('Symbol "%s" already declared locally' % name) if self.symtab.fetch_global_ref(name): - self.syntax_error('Symbol "%s" already declared' % name) - self.symtab.statics.setdefault(routine_name, {})[name] = SymEntry(ast_node, type_) + self.syntax_error('Symbol "%s" already declared globally' % name) + self.symtab.locals.setdefault(routine_name, {})[name] = SymEntry(ast_node, type_) # ---- symbol resolution @@ -306,17 +264,17 @@ class Parser(object): type_ = self.defn_type() if not isinstance(type_, RoutineType): self.syntax_error("Can only define a routine, not {}".format(repr(type_))) - statics = [] + locals_ = [] if self.scanner.consume('@'): self.scanner.check_type('integer literal') block = None addr = int(self.scanner.token) self.scanner.scan() else: - statics = self.statics() + locals_ = self.locals() block = self.block() addr = None - return type_, Routine(self.scanner.line_number, name=name, block=block, addr=addr, statics=statics) + return type_, Routine(self.scanner.line_number, name=name, block=block, addr=addr, locals=locals_) def labels(self): accum = [] @@ -370,13 +328,19 @@ class Parser(object): loc = IndexedRef(loc, offset, index) return loc - def statics(self): + def locals(self): defns = [] while self.scanner.consume('static'): type_, defn = self.defn() if defn.initial is None: self.syntax_error("Static definition {} must have initial value".format(defn)) - self.declare_static(self.current_routine_name, defn.name, defn, type_) + self.declare_local(self.current_routine_name, defn.name, defn, type_) + defns.append(defn) + while self.scanner.consume('local'): + type_, defn = self.defn() + if defn.initial is not None: + self.syntax_error("Local definition {} may not have initial value".format(defn)) + self.declare_local(self.current_routine_name, defn.name, defn, type_) defns.append(defn) return defns @@ -424,6 +388,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 diff --git a/src/sixtypical/symtab.py b/src/sixtypical/symtab.py new file mode 100644 index 0000000..99dacec --- /dev/null +++ b/src/sixtypical/symtab.py @@ -0,0 +1,50 @@ +# encoding: UTF-8 + +from sixtypical.model import ( + TYPE_BIT, TYPE_BYTE, LocationRef, +) + + +class SymEntry(object): + def __init__(self, ast_node, type_): + self.ast_node = ast_node + self.type_ = type_ + + def __repr__(self): + return "%s(%r, %r)" % (self.__class__.__name__, self.ast_node, self.type_) + + +class SymbolTable(object): + def __init__(self): + self.symbols = {} # symbol name -> SymEntry + self.locals = {} # routine name -> (symbol name -> SymEntry) + self.typedefs = {} # type name -> Type AST + self.consts = {} # const name -> ConstantRef + + for name in ('a', 'x', 'y'): + self.symbols[name] = SymEntry(None, TYPE_BYTE) + for name in ('c', 'z', 'n', 'v'): + self.symbols[name] = SymEntry(None, TYPE_BIT) + + def __str__(self): + return "Symbols: {}\nLocals: {}\nTypedefs: {}\nConsts: {}".format(self.symbols, self.locals, self.typedefs, self.consts) + + def has_local(self, routine_name, name): + return name in self.locals.get(routine_name, {}) + + def fetch_global_type(self, name): + return self.symbols[name].type_ + + def fetch_local_type(self, routine_name, name): + return self.locals[routine_name][name].type_ + + def fetch_global_ref(self, name): + if name in self.symbols: + return LocationRef(name) + return None + + def fetch_local_ref(self, routine_name, name): + routine_locals = self.locals.get(routine_name, {}) + if name in routine_locals: + return LocationRef(name) + return None diff --git a/test.sh b/test.sh index bed307e..a19ec97 100755 --- a/test.sh +++ b/test.sh @@ -1,7 +1,13 @@ #!/bin/sh +# This currently represents a lot of tests! If you only want to run a subset, +# it's probably best to run `falderal` manually on the file(s) you want to test. + falderal --substring-error \ - tests/SixtyPical\ Syntax.md \ - tests/SixtyPical\ Analysis.md \ - tests/SixtyPical\ Fallthru.md \ - tests/SixtyPical\ Compilation.md + "tests/appliances/sixtypical.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" diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index 040f48f..ffa89a8 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -4,945 +4,14 @@ SixtyPical Analysis This is a test suite, written in [Falderal][] format, for the SixtyPical static analysis rules. +This file mostly contains tests for operations. +For rudiments and storage, see [SixtyPical Storage](SixtyPical%20Storage.md). +For control flow, see [SixtyPical Control Flow](SixtyPical%20Control%20Flow.md). + [Falderal]: http://catseye.tc/node/Falderal - -> Functionality "Analyze SixtyPical program" is implemented by - -> shell command "bin/sixtypical --analyze-only --traceback %(test-body-file) && echo ok" - -> Tests for functionality "Analyze SixtyPical program" -### Rudiments ### - -Routines must declare their inputs, outputs, and memory locations they trash. - - | define up routine - | inputs a - | outputs a - | trashes c, z, v, n - | { - | st off, c - | add a, 1 - | } - = ok - -Routines may not declare a memory location to be both an output and trashed. - - | define main routine - | outputs a - | trashes a - | { - | ld a, 0 - | } - ? InconsistentConstraintsError: a - -If a routine declares it outputs a location, that location should be initialized. - - | define main routine - | outputs a, x, z, n - | { - | ld x, 0 - | } - ? UnmeaningfulOutputError: a - - | define main routine - | inputs a - | outputs a - | { - | } - = ok - -If a routine declares it outputs a location, that location may or may not have -been initialized. Trashing is mainly a signal to the caller. - - | define main routine - | trashes x, z, n - | { - | ld x, 0 - | } - = ok - - | define main routine - | trashes x, z, n - | { - | } - = ok - -If a routine modifies a location, it needs to either output it or trash it. - - | define main routine - | { - | ld x, 0 - | } - ? ForbiddenWriteError: x - - | define main routine - | outputs x, z, n - | { - | ld x, 0 - | } - = ok - - | define main routine - | trashes x, z, n - | { - | ld x, 0 - | } - = ok - -This is true regardless of whether it's an input or not. - - | define main routine - | inputs x - | { - | ld x, 0 - | } - ? ForbiddenWriteError: x - - | define main routine - | inputs x - | outputs x, z, n - | { - | ld x, 0 - | } - = ok - - | define main routine - | inputs x - | trashes x, z, n - | { - | ld x, 0 - | } - = ok - -If a routine trashes a location, this must be declared. - - | define foo routine - | trashes x - | { - | trash x - | } - = ok - - | define foo routine - | { - | trash x - | } - ? ForbiddenWriteError: x - - | define foo routine - | outputs x - | { - | trash x - | } - ? UnmeaningfulOutputError: x - -If a routine causes a location to be trashed, this must be declared in the caller. - - | define trash_x routine - | trashes x, z, n - | { - | ld x, 0 - | } - | - | define foo routine - | trashes x, z, n - | { - | call trash_x - | } - = ok - - | define trash_x routine - | trashes x, z, n - | { - | ld x, 0 - | } - | - | define foo routine - | trashes z, n - | { - | call trash_x - | } - ? ForbiddenWriteError: x - - | define trash_x routine - | trashes x, z, n - | { - | ld x, 0 - | } - | - | define foo routine - | outputs x - | trashes z, n - | { - | call trash_x - | } - ? UnmeaningfulOutputError: x (in foo, line 12) - -If a routine reads or writes a user-define memory location, it needs to declare that too. - - | byte b1 @ 60000 - | byte b2 : 3 - | word w1 @ 60001 - | word w2 : 2000 - | - | define main routine - | inputs b1, w1 - | outputs b2, w2 - | trashes a, z, n - | { - | ld a, b1 - | st a, b2 - | copy w1, w2 - | } - = ok - -### call ### - -You can't call a non-routine. - - | byte up - | - | define main routine outputs x, y trashes z, n { - | ld x, 0 - | ld y, 1 - | call up - | } - ? TypeMismatchError: up - - | define main routine 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 - | - | define main routine { - | goto foo - | } - ? TypeMismatchError: foo - -### ld ### - -Can't `ld` from a memory location that isn't initialized. - - | define main routine - | inputs a, x - | trashes a, z, n - | { - | ld a, x - | } - = ok - - | define main routine - | inputs a - | trashes a - | { - | ld a, x - | } - ? UnmeaningfulReadError: x - -Can't `ld` to a memory location that doesn't appear in (outputs ∪ trashes). - - | define main routine - | trashes a, z, n - | { - | ld a, 0 - | } - = ok - - | define main routine - | outputs a - | trashes z, n - | { - | ld a, 0 - | } - = ok - - | define main routine - | outputs z, n - | trashes a - | { - | ld a, 0 - | } - = ok - - | define main routine - | trashes z, n - | { - | ld a, 0 - | } - ? ForbiddenWriteError: a - - | define main routine - | trashes a, n - | { - | ld a, 0 - | } - ? ForbiddenWriteError: z - -Can't `ld` a `word` type. - - | word foo - | - | define main routine - | inputs foo - | trashes a, n, z - | { - | ld a, foo - | } - ? TypeMismatchError: foo and a - -### st ### - -Can't `st` from a memory location that isn't initialized. - - | byte lives - | define main routine - | inputs x - | trashes lives - | { - | st x, lives - | } - = ok - - | byte lives - | define main routine - | trashes x, lives - | { - | st x, lives - | } - ? UnmeaningfulReadError: x - -Can't `st` to a memory location that doesn't appear in (outputs ∪ trashes). - - | byte lives - | define main routine - | trashes lives - | { - | st 0, lives - | } - = ok - - | byte lives - | define main routine - | outputs lives - | { - | st 0, lives - | } - = ok - - | byte lives - | define main routine - | inputs lives - | { - | st 0, lives - | } - ? ForbiddenWriteError: lives - -Can't `st` a `word` type. - - | word foo - | - | define main routine - | outputs foo - | trashes a, n, z - | { - | ld a, 0 - | st a, foo - | } - ? TypeMismatchError - -### tables ### - -Storing to a table, you must use an index. - - | byte one - | byte table[256] many - | - | define main routine - | outputs one - | trashes a, x, n, z - | { - | ld x, 0 - | ld a, 0 - | st a, one - | } - = ok - - | byte one - | byte table[256] many - | - | define main routine - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | ld a, 0 - | st a, many - | } - ? TypeMismatchError - - | byte one - | byte table[256] many - | - | define main routine - | outputs one - | trashes a, x, n, z - | { - | ld x, 0 - | ld a, 0 - | st a, one + x - | } - ? TypeMismatchError - - | byte one - | byte table[256] many - | - | define main routine - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | ld a, 0 - | st a, many + x - | } - = ok - -The index must be initialized. - - | byte one - | byte table[256] many - | - | define main routine - | outputs many - | trashes a, x, n, z - | { - | ld a, 0 - | st a, many + x - | } - ? UnmeaningfulReadError: x - -Reading from a table, you must use an index. - - | byte one - | - | define main routine - | outputs one - | trashes a, x, n, z - | { - | ld x, 0 - | st x, one - | ld a, one - | } - = ok - - | byte one - | - | define main routine - | outputs one - | trashes a, x, n, z - | { - | ld x, 0 - | st x, one - | ld a, one + x - | } - ? TypeMismatchError - - | byte table[256] many - | - | define main routine - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | ld a, 0 - | st a, many + x - | ld a, many - | } - ? TypeMismatchError - - | byte table[256] many - | - | define main routine - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | ld a, 0 - | st a, many + x - | ld a, many + x - | } - = ok - - | byte table[256] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | ld a, many + x - | } - = ok - -The index must be initialized. - - | byte table[256] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld a, many + x - | } - ? UnmeaningfulReadError: x - -Storing to a table, you may also include a constant offset. - - | byte one - | byte table[256] many - | - | define main routine - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | ld a, 0 - | st a, many + 100 + x - | } - = ok - -Reading from a table, you may also include a constant offset. - - | byte table[256] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | ld a, many + 100 + x - | } - = ok - -Using a constant offset, you can read and write entries in -the table beyond the 256th. - - | byte one - | byte table[1024] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | ld a, many + 999 + x - | st a, many + 1000 + x - | } - = ok - -There are other operations you can do on tables. (1/3) - - | byte table[256] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, c, n, z, v - | { - | ld x, 0 - | ld a, 0 - | st off, c - | add a, many + x - | sub a, many + x - | cmp a, many + x - | } - = ok - -There are other operations you can do on tables. (2/3) - - | byte table[256] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, c, n, z - | { - | ld x, 0 - | ld a, 0 - | and a, many + x - | or a, many + x - | xor a, many + x - | } - = ok - -There are other operations you can do on tables. (3/3) - - | byte table[256] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, c, n, z - | { - | ld x, 0 - | ld a, 0 - | st off, c - | shl many + x - | shr many + x - | inc many + x - | dec many + x - | } - = ok - -Copying to and from a word table. - - | word one - | word table[256] many - | - | define main routine - | inputs one, many - | outputs one, many - | trashes a, x, n, z - | { - | ld x, 0 - | copy one, many + x - | copy many + x, one - | } - = ok - - | word one - | word table[256] many - | - | define main routine - | inputs one, many - | outputs one, many - | trashes a, x, n, z - | { - | ld x, 0 - | copy one, many - | } - ? TypeMismatchError - - | word one - | word table[256] many - | - | define main routine - | inputs one, many - | outputs one, many - | trashes a, x, n, z - | { - | ld x, 0 - | copy one + x, many - | } - ? TypeMismatchError - -You can also copy a literal word to a word table. -(Even if the table has fewer than 256 entries.) - - | word table[32] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | copy 9999, many + x - | } - = ok - -Copying to and from a word table with a constant offset. - - | word one - | word table[256] many - | - | define main routine - | inputs one, many - | outputs one, many - | trashes a, x, n, z - | { - | ld x, 0 - | copy one, many + 100 + x - | copy many + 100 + x, one - | copy 9999, many + 1 + x - | } - = ok - -#### tables: range checking #### - -It is a static analysis error if it cannot be proven that a read or write -to a table falls within the defined size of that table. - -If a table has 256 entries, then there is never a problem (so long as -no constant offset is supplied), because a byte cannot index any entry -outside of 0..255. - -But if the table has fewer than 256 entries, or if a constant offset is -supplied, there is the possibility that the index will refer to an -entry in the table which does not exist. - -A SixtyPical implementation must be able to prove that the index is inside -the range of the table in various ways. The simplest is to show that a -constant value falls inside or outside the range of the table. - - | byte table[32] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 31 - | ld a, many + x - | st a, many + x - | } - = ok - - | byte table[32] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 32 - | ld a, many + x - | } - ? RangeExceededError - - | byte table[32] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 32 - | ld a, 0 - | st a, many + x - | } - ? RangeExceededError - -Any constant offset is taken into account in this check. - - | byte table[32] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 31 - | ld a, many + 1 + x - | } - ? RangeExceededError - - | byte table[32] many - | - | define main routine - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 31 - | ld a, 0 - | st a, many + 1 + x - | } - ? RangeExceededError - -This applies to `copy` as well. - - | word one: 77 - | word table[32] many - | - | define main routine - | inputs many, one - | outputs many, one - | trashes a, x, n, z - | { - | ld x, 31 - | copy one, many + x - | copy many + x, one - | } - = ok - - | word one: 77 - | word table[32] many - | - | define main routine - | inputs many, one - | outputs many, one - | trashes a, x, n, z - | { - | ld x, 32 - | copy many + x, one - | } - ? RangeExceededError - - | word one: 77 - | word table[32] many - | - | define main routine - | inputs many, one - | outputs many, one - | trashes a, x, n, z - | { - | ld x, 32 - | copy one, many + x - | } - ? RangeExceededError - -Any constant offset is taken into account in this check. - - | word one: 77 - | word table[32] many - | - | define main routine - | inputs many, one - | outputs many, one - | trashes a, x, n, z - | { - | ld x, 31 - | copy many + 1 + x, one - | } - ? RangeExceededError - - | word one: 77 - | word table[32] many - | - | define main routine - | inputs many, one - | outputs many, one - | trashes a, x, n, z - | { - | ld x, 31 - | copy one, many + 1 + x - | } - ? RangeExceededError - -`AND`'ing a register with a value ensures the range of the -register will not exceed the range of the value. This can -be used to "clip" the range of an index so that it fits in -a table. - - | word one: 77 - | word table[32] many - | - | define main routine - | inputs a, many, one - | outputs many, one - | trashes a, x, n, z - | { - | and a, 31 - | ld x, a - | copy one, many + x - | copy many + x, one - | } - = ok - -Tests for "clipping", but not enough. - - | word one: 77 - | word table[32] many - | - | define main routine - | inputs a, many, one - | outputs many, one - | trashes a, x, n, z - | { - | and a, 63 - | ld x, a - | copy one, many + x - | copy many + x, one - | } - ? RangeExceededError - - | word one: 77 - | word table[32] many - | - | define main routine - | inputs a, many, one - | outputs many, one - | trashes a, x, n, z - | { - | and a, 31 - | ld x, a - | copy one, many + 1 + x - | copy many + 1 + x, one - | } - ? RangeExceededError - -If you alter the value after "clipping" it, the range can -no longer be guaranteed. - - | word one: 77 - | word table[32] many - | - | define main routine - | inputs a, many, one - | outputs many, one - | trashes a, x, n, z - | { - | and a, 31 - | ld x, a - | inc x - | copy one, many + x - | copy many + x, one - | } - ? RangeExceededError - -When the range of a location is known, incrementing or -decrementing that location's value will shift the known -range. It will not invalidate it unless the known range -is at the limits of the possible ranges for the type. - - | vector routine - | trashes a, z, n - | print - | - | vector (routine - | trashes a, z, n) - | table[32] vectors - | - | define main routine - | inputs vectors, print - | outputs vectors - | trashes print, a, x, z, n, c - | { - | ld x, 0 - | inc x - | copy print, vectors + x - | } - = ok - - | vector routine - | trashes a, z, n - | print - | - | vector (routine - | trashes a, z, n) - | table[32] vectors - | - | define main routine - | inputs vectors, print - | outputs vectors - | trashes print, a, x, z, n, c - | { - | ld x, 32 - | dec x - | copy print, vectors + x - | } - = ok - ### add ### Can't `add` from or to a memory location that isn't initialized. @@ -1624,1084 +693,6 @@ Some rudimentary tests for `nop`. | } = ok -### call ### - -When calling a routine, all of the locations it lists as inputs must be -initialized. - - | byte lives - | - | define foo routine - | inputs x - | trashes lives - | { - | st x, lives - | } - | - | define main routine - | { - | call foo - | } - ? UnmeaningfulReadError: x - -Note that if you call a routine that trashes a location, you also trash it. - - | byte lives - | - | define foo routine - | inputs x - | trashes lives - | { - | st x, lives - | } - | - | define main routine - | outputs x, z, n - | { - | ld x, 0 - | call foo - | } - ? ForbiddenWriteError: lives - - | byte lives - | - | define foo routine - | inputs x - | trashes lives - | { - | st x, lives - | } - | - | define main routine - | outputs x, z, n - | trashes lives - | { - | ld x, 0 - | call foo - | } - = ok - -You can't output a value that the thing you called trashed. - - | byte lives - | - | define foo routine - | inputs x - | trashes lives - | { - | st x, lives - | } - | - | define main routine - | outputs x, z, n, lives - | { - | ld x, 0 - | call foo - | } - ? UnmeaningfulOutputError: lives - -...unless you write to it yourself afterwards. - - | byte lives - | - | define foo routine - | inputs x - | trashes lives - | { - | st x, lives - | } - | - | define main routine - | outputs x, z, n, lives - | { - | ld x, 0 - | call foo - | st x, lives - | } - = ok - -If a routine declares outputs, they are initialized in the caller after -calling it. - - | define foo routine - | outputs x, z, n - | { - | ld x, 0 - | } - | - | define main routine - | outputs a - | trashes x, z, n - | { - | call foo - | ld a, x - | } - = ok - - | define foo routine - | { - | } - | - | define main routine - | outputs a - | trashes x - | { - | call foo - | ld a, x - | } - ? UnmeaningfulReadError: x - -If a routine trashes locations, they are uninitialized in the caller after -calling it. - - | define foo routine - | trashes x, z, n - | { - | ld x, 0 - | } - = ok - - | define foo routine - | trashes x, z, n - | { - | ld x, 0 - | } - | - | define main routine - | outputs a - | trashes x, z, n - | { - | call foo - | ld a, x - | } - ? UnmeaningfulReadError: x - -Calling an extern is just the same as calling a defined routine with the -same constraints. - - | define chrout routine - | inputs a - | trashes a - | @ 65490 - | - | define main routine - | trashes a, z, n - | { - | ld a, 65 - | call chrout - | } - = ok - - | define chrout routine - | inputs a - | trashes a - | @ 65490 - | - | define main routine - | trashes a, z, n - | { - | call chrout - | } - ? UnmeaningfulReadError: a - - | define chrout routine - | inputs a - | trashes a - | @ 65490 - | - | define main routine - | trashes a, x, z, n - | { - | ld a, 65 - | call chrout - | ld x, a - | } - ? UnmeaningfulReadError: a - -### trash ### - -Trash does nothing except indicate that we do not care about the value anymore. - - | define foo routine - | inputs a - | outputs x - | trashes a, z, n - | { - | st a, x - | ld a, 0 - | trash a - | } - = ok - - | define foo routine - | inputs a - | outputs a, x - | trashes z, n - | { - | st a, x - | ld a, 0 - | trash a - | } - ? UnmeaningfulOutputError: a - - | define foo routine - | inputs a - | outputs x - | trashes a, z, n - | { - | st a, x - | trash a - | st a, x - | } - ? UnmeaningfulReadError: a - -### if ### - -Both blocks of an `if` are analyzed. - - | define foo routine - | inputs a - | outputs x - | trashes a, z, n, c - | { - | cmp a, 42 - | if z { - | ld x, 7 - | } else { - | ld x, 23 - | } - | } - = ok - -If a location is initialized in one block, it must be initialized in the other as well -in order to be considered to be initialized after the block. If it is not consistent, -it will be considered uninitialized. - - | define foo routine - | inputs a - | outputs x - | trashes a, z, n, c - | { - | cmp a, 42 - | if z { - | ld x, 7 - | } else { - | ld a, 23 - | } - | } - ? UnmeaningfulOutputError: x - - | define foo routine - | inputs a - | outputs x - | trashes a, z, n, c - | { - | cmp a, 42 - | if z { - | ld a, 6 - | } else { - | ld x, 7 - | } - | } - ? UnmeaningfulOutputError: x - - | define foo routine - | inputs a - | outputs x - | trashes a, z, n, c - | { - | cmp a, 42 - | if not z { - | ld a, 6 - | } else { - | ld x, 7 - | } - | } - ? UnmeaningfulOutputError: x - - | define foo routine - | inputs a - | trashes a, x, z, n, c - | { - | cmp a, 42 - | if not z { - | ld a, 6 - | } else { - | ld x, 7 - | } - | ld a, x - | } - ? UnmeaningfulReadError: x - -If we don't care if it's uninitialized after the `if`, that's okay then. - - | define foo routine - | inputs a - | trashes a, x, z, n, c - | { - | cmp a, 42 - | if not z { - | ld a, 6 - | } else { - | ld x, 7 - | } - | } - = ok - -Or, if it does get initialized on both branches, that's okay then. - - | define foo routine - | inputs a - | outputs x - | trashes a, z, n, c - | { - | cmp a, 42 - | if not z { - | ld x, 0 - | ld a, 6 - | } else { - | ld x, 7 - | } - | } - = ok - -However, this only pertains to initialization. If a value is already -initialized, either because it was set previous to the `if`, or is an -input to the routine, and it is initialized in one branch, it need not -be initialized in the other. - - | define foo routine - | outputs x - | trashes a, z, n, c - | { - | ld x, 0 - | ld a, 0 - | cmp a, 42 - | if z { - | ld x, 7 - | } else { - | ld a, 23 - | } - | } - = ok - - | define foo routine - | inputs x - | outputs x - | trashes a, z, n, c - | { - | ld a, 0 - | cmp a, 42 - | if z { - | ld x, 7 - | } else { - | ld a, 23 - | } - | } - = ok - -An `if` with a single block is analyzed as if it had an empty `else` block. - - | define foo routine - | inputs a - | outputs x - | trashes a, z, n, c - | { - | cmp a, 42 - | if z { - | ld x, 7 - | } - | } - ? UnmeaningfulOutputError: x - - | define foo routine - | inputs a - | outputs x - | trashes a, z, n, c - | { - | ld x, 0 - | cmp a, 42 - | if z { - | ld x, 7 - | } - | } - = ok - - | define foo routine - | inputs a - | outputs x - | trashes a, z, n, c - | { - | ld x, 0 - | cmp a, 42 - | if not z { - | ld x, 7 - | } - | } - = ok - -The cardinal rule for trashes in an `if` is the "union rule": if one branch -trashes {`a`} and the other branch trashes {`b`} then the whole `if` statement -trashes {`a`, `b`}. - - | define foo routine - | inputs a, x, z - | trashes a, x - | { - | if z { - | trash a - | } else { - | trash x - | } - | } - = ok - - | define foo routine - | inputs a, x, z - | trashes a - | { - | if z { - | trash a - | } else { - | trash x - | } - | } - ? ForbiddenWriteError: x (in foo, line 10) - - | define foo routine - | inputs a, x, z - | trashes x - | { - | if z { - | trash a - | } else { - | trash x - | } - | } - ? ForbiddenWriteError: a (in foo, line 10) - -### repeat ### - -Repeat loop. - - | define main routine - | outputs x, y, n, z, c - | { - | ld x, 0 - | ld y, 15 - | repeat { - | inc x - | inc y - | cmp x, 10 - | } until z - | } - = ok - -You can initialize something inside the loop that was uninitialized outside. - - | define main routine - | outputs x, y, n, z, c - | { - | ld x, 0 - | repeat { - | ld y, 15 - | inc x - | cmp x, 10 - | } until z - | } - = ok - -But you can't UNinitialize something at the end of the loop that you need -initialized at the start. - - | define foo routine - | trashes y - | { - | } - | - | define main routine - | outputs x, y, n, z, c - | { - | ld x, 0 - | ld y, 15 - | repeat { - | inc x - | inc y - | call foo - | cmp x, 10 - | } until z - | } - ? UnmeaningfulReadError: y - -And if you trash the test expression (i.e. `z` in the below) inside the loop, -this is an error too. - - | word one : 0 - | word two : 0 - | - | define main routine - | inputs one, two - | outputs two - | trashes a, z, n - | { - | repeat { - | copy one, two - | } until z - | } - ? UnmeaningfulReadError: z - -The body of `repeat forever` can be empty. - - | define main routine - | { - | repeat { - | } forever - | } - = ok - -While `repeat` is most often used with `z`, it can also be used with `n`. - - | define main routine - | outputs y, n, z - | { - | ld y, 15 - | repeat { - | dec y - | } until n - | } - = ok - -### for ### - -Basic "open-faced for" loop. We'll start with the "upto" variant. - -#### upward-counting variant - -Even though we do not give the starting value in the "for" construct, -we know the exact range the loop variable takes on. - - | byte table[16] tab - | - | define foo routine inputs tab trashes a, x, c, z, v, n { - | ld x, 0 - | for x up to 15 { - | ld a, tab + x - | } - | } - = ok - - | byte table[15] tab - | - | define foo routine inputs tab trashes a, x, c, z, v, n { - | ld x, 0 - | for x up to 15 { - | ld a, tab + x - | } - | } - ? RangeExceededError - -You need to initialize the loop variable before the loop. - - | byte table[16] tab - | - | define foo routine inputs tab trashes a, x, c, z, v, n { - | for x up to 15 { - | ld a, 0 - | } - | } - ? UnmeaningfulReadError - -Because routines currently do not include range constraints, -the loop variable may not be useful as an input (the location -is assumed to have the maximum range.) - - | byte table[16] tab - | - | define foo routine - | inputs tab, x - | trashes a, x, c, z, v, n { - | for x up to 15 { - | ld a, 0 - | } - | } - ? RangeExceededError - -You cannot modify the loop variable in a "for" loop. - - | byte table[16] tab - | - | define foo routine inputs tab trashes a, x, c, z, v, n { - | ld x, 0 - | for x up to 15 { - | ld x, 0 - | } - | } - ? ForbiddenWriteError - -This includes nesting a loop on the same variable. - - | byte table[16] tab - | - | define foo routine inputs tab trashes a, x, c, z, v, n { - | ld x, 0 - | for x up to 8 { - | for x up to 15 { - | ld a, 0 - | } - | } - | } - ? ForbiddenWriteError - -But nesting with two different variables is okay. - - | byte table[16] tab - | - | define foo routine inputs tab trashes a, x, y, c, z, v, n { - | ld x, 0 - | for x up to 8 { - | ld a, x - | ld y, a - | for y up to 15 { - | ld a, 0 - | } - | } - | } - = ok - -Inside the inner loop, the outer variable is still not writeable. - - | byte table[16] tab - | - | define foo routine inputs tab trashes a, x, y, c, z, v, n { - | ld x, 0 - | for x up to 8 { - | ld a, x - | ld y, a - | for y up to 15 { - | ld x, 0 - | } - | } - | } - ? ForbiddenWriteError - -If the range isn't known to be smaller than the final value, you can't go up to it. - - | byte table[32] tab - | - | define foo routine inputs tab trashes a, x, c, z, v, n { - | ld x, 16 - | for x up to 15 { - | ld a, tab + x - | } - | } - ? RangeExceededError - -You can initialize something inside the loop that was uninitialized outside. - - | define main routine - | outputs x, y, n, z - | trashes c - | { - | ld x, 0 - | for x up to 15 { - | ld y, 15 - | } - | } - = ok - -But you can't UNinitialize something at the end of the loop that you need -initialized at the start of that loop. - - | define foo routine - | trashes y - | { - | } - | - | define main routine - | outputs x, y, n, z - | trashes c - | { - | ld x, 0 - | ld y, 15 - | for x up to 15 { - | inc y - | call foo - | } - | } - ? UnmeaningfulReadError: y - -The "for" loop does not preserve the `z` or `n` registers. - - | define foo routine trashes x { - | ld x, 0 - | for x up to 15 { - | } - | } - ? ForbiddenWriteError - -But it does preserve the other registers, such as `c`. - - | define foo routine trashes x, z, n { - | ld x, 0 - | for x up to 15 { - | } - | } - = ok - -In fact it does not strictly trash `z` and `n`, as they are -always set to known values after the loop. TODO: document -what these known values are! - - | define foo routine outputs z, n trashes x { - | ld x, 0 - | for x up to 15 { - | } - | } - = ok - -#### downward-counting variant - -In a "for" loop (downward-counting variant), we know the exact range the loop variable takes on. - - | byte table[16] tab - | - | define foo routine inputs tab trashes a, x, c, z, v, n { - | ld x, 15 - | for x down to 0 { - | ld a, tab + x - | } - | } - = ok - - | byte table[15] tab - | - | define foo routine inputs tab trashes a, x, c, z, v, n { - | ld x, 15 - | for x down to 0 { - | ld a, tab + x - | } - | } - ? RangeExceededError - -You need to initialize the loop variable before a "for" loop (downward variant). - - | byte table[16] tab - | - | define foo routine inputs tab trashes a, x, c, z, v, n { - | for x down to 15 { - | ld a, 0 - | } - | } - ? UnmeaningfulReadError - -You cannot modify the loop variable in a "for" loop (downward variant). - - | byte table[16] tab - | - | define foo routine inputs tab trashes a, x, c, z, v, n { - | ld x, 15 - | for x down to 0 { - | ld x, 0 - | } - | } - ? ForbiddenWriteError - -If the range isn't known to be larger than the final value, you can't go down to it. - - | byte table[32] tab - | - | define foo routine inputs tab trashes a, x, c, z, v, n { - | ld x, 0 - | for x down to 0 { - | ld a, tab + x - | } - | } - ? RangeExceededError - -The "for" loop does not preserve the `z` or `n` registers. - - | define foo routine trashes x { - | ld x, 15 - | for x down to 0 { - | } - | } - ? ForbiddenWriteError - -But it does preserve the other registers, such as `c`. - - | define foo routine trashes x, z, n { - | ld x, 15 - | for x down to 0 { - | } - | } - = ok - -In fact it does not strictly trash `z` and `n`, as they are -always set to known values after the loop. TODO: document -what these known values are! - - | define foo routine outputs z, n trashes x { - | ld x, 15 - | for x down to 0 { - | } - | } - = ok - -### save ### - -Basic neutral test, where the `save` makes no difference. - - | define main routine - | inputs a, x - | outputs a, x - | trashes z, n - | { - | ld a, 1 - | save x { - | ld a, 2 - | } - | ld a, 3 - | } - = ok - -Saving any location (other than `a`) will trash `a`. - - | define main routine - | inputs a, x - | outputs a, x - | trashes z, n - | { - | ld a, 1 - | save x { - | ld a, 2 - | } - | } - ? UnmeaningfulOutputError - -Saving `a` does not trash anything. - - | define main routine - | inputs a, x - | outputs a, x - | trashes z, n - | { - | ld x, 1 - | save a { - | ld x, 2 - | } - | ld x, 3 - | } - = ok - -A defined value that has been saved can be trashed inside the block. -It will continue to be defined outside the block. - - | define main routine - | outputs x, y - | trashes a, z, n - | { - | ld x, 0 - | save x { - | ld y, 0 - | trash x - | } - | } - = ok - -A trashed value that has been saved can be used inside the block. -It will continue to be trashed outside the block. - -(Note, both x and a are unmeaningful in this test.) - - | define main routine - | inputs a - | outputs a, x - | trashes z, n - | { - | ld x, 0 - | trash x - | save x { - | ld a, 0 - | ld x, 1 - | } - | } - ? UnmeaningfulOutputError - -The known range of a value will be preserved outside the block as well. - - | word one: 77 - | word table[32] many - | - | define main routine - | inputs a, many, one - | outputs many, one - | trashes a, x, n, z - | { - | and a, 31 - | ld x, a - | save x { - | ld x, 255 - | } - | copy one, many + x - | copy many + x, one - | } - = ok - - | word one: 77 - | word table[32] many - | - | define main routine - | inputs a, many, one - | outputs many, one - | trashes a, x, n, z - | { - | and a, 63 - | ld x, a - | save x { - | ld x, 1 - | } - | copy one, many + x - | copy many + x, one - | } - ? RangeExceededError - -The known properties of a value are preserved inside the block, too. - - | word one: 77 - | word table[32] many - | - | define main routine - | inputs a, many, one - | outputs many, one - | trashes a, x, n, z - | { - | and a, 31 - | ld x, a - | save x { - | copy one, many + x - | copy many + x, one - | } - | copy one, many + x - | copy many + x, one - | } - = ok - -A value which is not output from the routine, is preserved by the -routine; and can appear in a `save` exactly because a `save` preserves it. - - | define main routine - | outputs y - | trashes a, z, n - | { - | save x { - | ld y, 0 - | ld x, 1 - | } - | } - = ok - -Because saving anything except `a` trashes `a`, a common idiom is to save `a` -first in a nested series of `save`s. - - | define main routine - | inputs a - | outputs a - | trashes z, n - | { - | save a { - | save x { - | ld a, 0 - | ld x, 1 - | } - | } - | } - = ok - -There is a shortcut syntax for a nested series of `save`s. - - | define main routine - | inputs a - | outputs a - | trashes z, n - | { - | save a, x { - | ld a, 0 - | ld x, 1 - | } - | } - = ok - -`a` is only preserved if it is the outermost thing `save`d. - - | define main routine - | inputs a - | outputs a - | trashes z, n - | { - | save x, a { - | ld a, 0 - | ld x, 1 - | } - | } - ? UnmeaningfulOutputError: a - -Not just registers, but also user-defined locations can be saved. - - | byte foo - | - | define main routine - | trashes a, z, n - | { - | save foo { - | st 5, foo - | } - | } - = ok - -But only if they are bytes. - - | word foo - | - | define main routine - | trashes a, z, n - | { - | save foo { - | copy 555, foo - | } - | } - ? TypeMismatchError - - | byte table[16] tab - | - | define main routine - | trashes a, y, z, n - | { - | save tab { - | ld y, 0 - | st 5, tab + y - | } - | } - ? TypeMismatchError - -A `goto` cannot appear within a `save` block. - - | define other routine - | trashes a, z, n - | { - | ld a, 0 - | } - | - | define main routine - | trashes a, z, n - | { - | ld a, 1 - | save x { - | ld x, 2 - | goto other - | } - | } - ? IllegalJumpError - ### with interrupts ### | vector routine @@ -2760,1444 +751,65 @@ A `goto` cannot appear within a `with interrupts` block. | } ? IllegalJumpError -### copy ### - -Can't `copy` from a memory location that isn't initialized. - - | byte lives - | define main routine - | inputs x - | outputs lives - | trashes a, z, n - | { - | copy x, lives - | } - = ok - - | byte lives - | define main routine - | outputs lives - | trashes x, a, z, n - | { - | copy x, lives - | } - ? UnmeaningfulReadError: x - -Can't `copy` to a memory location that doesn't appear in (outputs ∪ trashes). - - | byte lives - | define main routine - | trashes lives, a, z, n - | { - | copy 0, lives - | } - = ok - - | byte lives - | define main routine - | outputs lives - | trashes a, z, n - | { - | copy 0, lives - | } - = ok - - | byte lives - | define main routine - | inputs lives - | trashes a, z, n - | { - | copy 0, lives - | } - ? ForbiddenWriteError: lives - -a, z, and n are trashed, and must be declared as such. - -(Note, both n and z are forbidden writes in this test.) - - | byte lives - | define main routine - | outputs lives - | { - | copy 0, lives - | } - ? ForbiddenWriteError - -a, z, and n are trashed, and must not be declared as outputs. - -(Note, both n and a are unmeaningful outputs in this test.) - - | byte lives - | define main routine - | outputs lives, a, z, n - | { - | copy 0, lives - | } - ? UnmeaningfulOutputError - -Unless of course you subsequently initialize them. - - | byte lives - | define main routine - | outputs lives, a, z, n - | { - | copy 0, lives - | ld a, 0 - | } - = ok - -Can `copy` from a `byte` to a `byte`. - - | byte source : 0 - | byte dest - | - | define main routine - | inputs source - | outputs dest - | trashes a, z, n - | { - | copy source, dest - | } - = ok - -The understanding is that, because `copy` trashes `a`, `a` cannot be used -as the destination of a `copy`. - - | byte source : 0 - | byte dest - | - | define main routine - | inputs source - | outputs dest - | trashes a, z, n - | { - | copy source, a - | } - ? ForbiddenWriteError - -Can `copy` from a `word` to a `word`. - - | word source : 0 - | word dest - | - | define main routine - | inputs source - | outputs dest - | trashes a, z, n - | { - | copy source, dest - | } - = ok - -Can't `copy` from a `byte` to a `word`. - - | byte source : 0 - | word dest - | - | define main routine - | inputs source - | outputs dest - | trashes a, z, n - | { - | copy source, dest - | } - ? TypeMismatchError - -Can't `copy` from a `word` to a `byte`. - - | word source : 0 - | byte dest - | - | define main routine - | inputs source - | outputs dest - | trashes a, z, n - | { - | copy source, dest - | } - ? TypeMismatchError - -### point ... into blocks ### - -Pointer must be a pointer type. - - | byte table[256] tab - | word ptr - | - | define main routine - | inputs tab - | outputs y, tab - | trashes a, z, n, ptr - | { - | ld y, 0 - | point ptr into tab { - | copy 123, [ptr] + y - | } - | } - ? TypeMismatchError - -Cannot write through pointer outside a `point ... into` block. - - | byte table[256] tab - | pointer ptr - | - | define main routine - | inputs tab, ptr - | outputs y, tab - | trashes a, z, n, ptr - | { - | ld y, 0 - | copy 123, [ptr] + y - | } - ? ForbiddenWriteError - - | byte table[256] tab - | pointer ptr - | - | define main routine - | inputs tab - | outputs y, tab - | trashes a, z, n, ptr - | { - | ld y, 0 - | point ptr into tab { - | copy 123, [ptr] + y - | } - | copy 123, [ptr] + y - | } - ? ForbiddenWriteError - -Write literal through a pointer into a table. - - | byte table[256] tab - | pointer ptr - | - | define main routine - | inputs tab - | outputs y, tab - | trashes a, z, n, ptr - | { - | ld y, 0 - | point ptr into tab { - | copy 123, [ptr] + y - | } - | } - = ok - -Writing into a table via a pointer does use `y`. - - | byte table[256] tab - | pointer ptr - | - | define main routine - | inputs tab - | outputs tab - | trashes a, z, n, ptr - | { - | point ptr into tab { - | copy 123, [ptr] + y - | } - | } - ? UnmeaningfulReadError - -Write stored value through a pointer into a table. - - | byte table[256] tab - | pointer ptr - | byte foo - | - | define main routine - | inputs foo, tab - | outputs y, tab - | trashes a, z, n, ptr - | { - | ld y, 0 - | point ptr into tab { - | copy foo, [ptr] + y - | } - | } - = ok - -Read a table entry via a pointer. - - | byte table[256] tab - | pointer ptr - | byte foo - | - | define main routine - | inputs tab - | outputs foo - | trashes a, y, z, n, ptr - | { - | ld y, 0 - | point ptr into tab { - | copy [ptr] + y, foo - | } - | } - = ok - -Read and write through two pointers into a table. - - | byte table[256] tab - | pointer ptra - | pointer ptrb - | - | define main routine - | inputs tab - | outputs tab - | trashes a, y, z, n, ptra, ptrb - | { - | ld y, 0 - | point ptra into tab { - | point ptrb into tab { - | copy [ptra] + y, [ptrb] + y - | } - | } - | } - = ok - -Read through a pointer into a table, to the `a` register. Note that this is done with `ld`, -not `copy`. - - | byte table[256] tab - | pointer ptr - | byte foo - | - | define main routine - | inputs tab - | outputs a - | trashes y, z, n, ptr - | { - | ld y, 0 - | point ptr into tab { - | ld a, [ptr] + y - | } - | } - = ok - -Write the `a` register through a pointer into a table. Note that this is done with `st`, -not `copy`. - - | byte table[256] tab - | pointer ptr - | byte foo - | - | define main routine - | inputs tab - | outputs tab - | trashes a, y, z, n, ptr - | { - | ld y, 0 - | point ptr into tab { - | ld a, 255 - | st a, [ptr] + y - | } - | } - = ok - -Cannot get a pointer into a non-byte (for instance, word) table. - - | word table[256] tab - | pointer ptr - | byte foo - | - | define main routine - | inputs tab - | outputs foo - | trashes a, y, z, n, ptr - | { - | ld y, 0 - | point ptr into tab { - | copy [ptr] + y, foo - | } - | } - ? TypeMismatchError - -Cannot get a pointer into a non-byte (for instance, vector) table. - - | vector (routine trashes a, z, n) table[256] tab - | pointer ptr - | vector (routine trashes a, z, n) foo - | - | define main routine - | inputs tab - | outputs foo - | trashes a, y, z, n, ptr - | { - | ld y, 0 - | point ptr into tab { - | copy [ptr] + y, foo - | } - | } - ? TypeMismatchError - -`point into` by itself only requires `ptr` to be writeable. By itself, -it does not require `tab` to be readable or writeable. - - | byte table[256] tab - | pointer ptr - | - | define main routine - | trashes a, z, n, ptr - | { - | point ptr into tab { - | ld a, 0 - | } - | } - = ok - - | byte table[256] tab - | pointer ptr - | - | define main routine - | trashes a, z, n - | { - | point ptr into tab { - | ld a, 0 - | } - | } - ? ForbiddenWriteError - -After a `point into` block, the pointer is no longer meaningful and cannot -be considered an output of the routine. - - | byte table[256] tab - | pointer ptr - | - | define main routine - | inputs tab - | outputs y, tab, ptr - | trashes a, z, n - | { - | ld y, 0 - | point ptr into tab { - | copy 123, [ptr] + y - | } - | } - ? UnmeaningfulOutputError - -If code in a routine reads from a table through a pointer, the table must be in -the `inputs` of that routine. - - | byte table[256] tab - | pointer ptr - | byte foo - | - | define main routine - | outputs foo - | trashes a, y, z, n, ptr - | { - | ld y, 0 - | point ptr into tab { - | copy [ptr] + y, foo - | } - | } - ? UnmeaningfulReadError - -Likewise, if code in a routine writes into a table via a pointer, the table must -be in the `outputs` of that routine. - - | byte table[256] tab - | pointer ptr - | - | define main routine - | inputs tab - | trashes a, y, z, n, ptr - | { - | ld y, 0 - | point ptr into tab { - | copy 123, [ptr] + y - | } - | } - ? ForbiddenWriteError - -If code in a routine reads from a table through a pointer, the pointer *should* -remain inside the range of the table. This is currently not checked. - - | byte table[32] tab - | pointer ptr - | byte foo - | - | define main routine - | inputs tab - | outputs foo - | trashes a, y, c, z, n, v, ptr - | { - | ld y, 0 - | point ptr into tab { - | st off, c - | add ptr, word 100 - | copy [ptr] + y, foo - | } - | } - = ok - -Likewise, if code in a routine writes into a table through a pointer, the pointer -*should* remain inside the range of the table. This is currently not checked. - - | byte table[32] tab - | pointer ptr - | - | define main routine - | inputs tab - | outputs tab - | trashes a, y, c, z, n, v, ptr - | { - | ld y, 0 - | point ptr into tab { - | st off, c - | add ptr, word 100 - | copy 123, [ptr] + y - | } - | } - = ok - -### routines ### - -Routines are constants. You need not, and in fact cannot, specify a constant -as an input to, an output of, or as a trashed value of a routine. +A `call` cannot appear within a `with interrupts` block. | vector routine | inputs x | outputs x | trashes z, n - | vec + | bar | | define foo routine | inputs x | outputs x | trashes z, n | { - | inc x + | inc x + | } + | + | define other routine + | trashes bar, a, n, z + | { + | ld a, 0 | } | | define main routine - | inputs foo - | outputs vec - | trashes a, z, n + | trashes bar, a, n, z | { - | copy foo, vec - | } - ? ConstantConstraintError: foo - - | vector routine - | inputs x - | outputs x - | trashes z, n - | vec - | - | define foo routine - | inputs x - | outputs x - | trashes z, n - | { - | inc x - | } - | - | define main routine - | outputs vec, foo - | trashes a, z, n - | { - | copy foo, vec - | } - ? ConstantConstraintError: foo - - | vector routine - | inputs x - | outputs x - | trashes z, n - | vec - | - | define foo routine - | inputs x - | outputs x - | trashes z, n - | { - | inc x - | } - | - | define main routine - | outputs vec - | trashes a, z, n, foo - | { - | copy foo, vec - | } - ? ConstantConstraintError: foo - -#### routine-vector type compatibility - -You can copy the address of a routine into a vector, if that vector type -is at least as "wide" as the type of the routine. More specifically, - -- the vector must take _at least_ the inputs that the routine takes -- the vector must produce _at least_ the outputs that the routine produces -- the vector must trash _at least_ what the routine trashes - -If the vector and the routine have the very same signature, that's not an error. - - | vector routine - | inputs x, y - | outputs x, y - | trashes z, n - | vec - | - | define foo routine - | inputs x, y - | outputs x, y - | trashes z, n - | { - | inc x - | inc y - | } - | - | define main routine - | outputs vec - | trashes a, z, n - | { - | copy foo, vec - | } - = ok - -If the vector takes an input that the routine doesn't take, that's not an error. -(The interface requires that a parameter be specified before calling, but the -implementation doesn't actually read it.) - - | vector routine - | inputs x, y, a - | outputs x, y - | trashes z, n - | vec - | - | define foo routine - | inputs x, y - | outputs x, y - | trashes z, n - | { - | inc x - | inc y - | } - | - | define main routine - | outputs vec - | trashes a, z, n - | { - | copy foo, vec - | } - = ok - -If the vector fails to take an input that the routine takes, that's an error. - - | vector routine - | inputs x - | outputs x, y - | trashes z, n - | vec - | - | define foo routine - | inputs x, y - | outputs x, y - | trashes z, n - | { - | inc x - | inc y - | } - | - | define main routine - | outputs vec - | trashes a, z, n - | { - | copy foo, vec - | } - ? IncompatibleConstraintsError - -If the vector produces an output that the routine doesn't produce, that's not an error. -(The interface claims the result of calling the routine is defined, but the implementation -actually preserves it instead of changing it; the caller can still treat it as a defined -output.) - - | vector routine - | inputs x, y - | outputs x, y, a - | trashes z, n - | vec - | - | define foo routine - | inputs x, y - | outputs x, y - | trashes z, n - | { - | inc x - | inc y - | } - | - | define main routine - | outputs vec - | trashes a, z, n - | { - | copy foo, vec - | } - = ok - -If the vector fails to produce an output that the routine produces, that's an error. - - | vector routine - | inputs x, y - | outputs x - | trashes z, n - | vec - | - | define foo routine - | inputs x, y - | outputs x, y - | trashes z, n - | { - | inc x - | inc y - | } - | - | define main routine - | outputs vec - | trashes a, z, n - | { - | copy foo, vec - | } - ? IncompatibleConstraintsError - -If the vector fails to trash something the routine trashes, that's an error. - - | vector routine - | inputs x, y - | outputs x, y - | trashes z - | vec - | - | define foo routine - | inputs x, y - | outputs x, y - | trashes z, n - | { - | inc x - | inc y - | } - | - | define main routine - | outputs vec - | trashes a, z, n - | { - | copy foo, vec - | } - ? IncompatibleConstraintsError - -If the vector trashes something the routine doesn't trash, that's not an error. -(The implementation preserves something the interface doesn't guarantee is -preserved. The caller gets no guarantee that it's preserved. It actually is, -but it doesn't know that.) - - | vector routine - | inputs x, y - | outputs x, y - | trashes a, z, n - | vec - | - | define foo routine - | inputs x, y - | outputs x, y - | trashes z, n - | { - | inc x - | inc y - | } - | - | define main routine - | outputs vec - | trashes a, z, n - | { - | copy foo, vec - | } - = ok - -#### other properties of routines - -Routines are read-only. - - | vector routine - | inputs x - | outputs x - | trashes z, n - | vec - | - | define foo routine - | inputs x - | outputs x - | trashes z, n - | { - | inc x - | } - | - | define main routine - | outputs vec - | trashes a, z, n - | { - | copy vec, foo - | } - ? TypeMismatchError - -Indirect call. - - | vector routine - | outputs x trashes z, n - | foo - | - | define bar routine outputs x trashes z, n { - | ld x, 200 - | } - | - | define main routine outputs x, foo trashes a, z, n { - | copy bar, foo - | call foo - | } - = ok - -Calling the vector does indeed trash the things the vector says it does. - - | vector routine trashes x, z, n foo - | - | define bar routine trashes x, z, n { - | ld x, 200 - | } - | - | define main routine outputs x, foo trashes z, n { - | ld x, 0 - | copy bar, foo - | call foo - | } - ? UnmeaningfulOutputError: x - -For now at least, you cannot have a `goto` inside a `repeat` loop. - - | define bar routine trashes x, z, n { - | ld x, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | repeat { - | inc x - | goto bar - | } until z + | with interrupts off { + | copy foo, bar + | call other + | } | } ? IllegalJumpError -`goto`, as a matter of syntax, can only appear at the end -of a block; but it need not be the final instruction in a -routine. - - | define bar routine trashes x, z, n { - | ld x, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | goto bar - | } - = ok - - | define bar routine trashes x, z, n { - | ld x, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | if z { - | ld x, 1 - | goto bar - | } - | } - = ok - - | define bar routine trashes x, z, n { - | ld x, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | if z { - | ld x, 1 - | goto bar - | } - | goto bar - | } - = ok - - | define bar routine trashes x, z, n { - | ld x, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | if z { - | ld x, 1 - | goto bar - | } - | ld x, 0 - | } - = ok - - | define bar routine trashes x, z, n { - | ld x, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | if z { - | ld x, 1 - | goto bar - | } else { - | ld x, 0 - | goto bar - | } - | } - = ok - - | define bar routine trashes x, z, n { - | ld x, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | if z { - | ld x, 1 - | goto bar - | } else { - | ld x, 0 - | } - | } - = ok - - | define bar routine trashes x, z, n { - | ld x, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | if z { - | ld x, 1 - | goto bar - | } else { - | ld x, 0 - | } - | ld x, 0 - | } - = ok - - | define bar routine trashes x, z, n { - | ld x, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | if z { - | ld x, 1 - | goto bar - | } else { - | ld x, 0 - | } - | goto bar - | } - = ok - -Even though `goto` can only appear at the end of a block, -you can still wind up with dead code; the analysis detects -this. - - | define bar routine trashes x, z, n { - | ld x, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | if z { - | ld x, 1 - | goto bar - | } else { - | ld x, 0 - | goto bar - | } - | ld x, 100 - | } - ? TerminatedContextError - -It is important that the type context at every -`goto` is compatible with the type context at the end of -the routine. - - | define bar routine - | inputs x - | trashes x, z, n - | { - | ld x, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | if z { - | ld x, 1 - | goto bar - | } else { - | ld x, 0 - | } - | ld x, 1 - | } - = ok - -Here, we try to trash `x` before `goto`ing a routine that inputs `x`. - - | define bar routine - | inputs x - | trashes x, z, n - | { - | ld x, 200 - | } - | - | define main routine - | outputs a - | trashes x, z, n - | { - | ld x, 0 - | if z { - | trash x - | goto bar - | } else { - | trash x - | } - | ld a, 1 - | } - ? UnmeaningfulReadError: x - -Here, we declare that main outputs `a`, but we `goto` a routine that does not output `a`. - - | define bar routine - | inputs x - | trashes x, z, n - | { - | ld x, 200 - | } - | - | 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 - | } - ? 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 - -Here, we declare that main outputs `a`, and we `goto` two routines, and they both output `a`. - - | define bar0 routine - | inputs x - | outputs a - | trashes x, z, n - | { - | ld a, x - | } - | - | define bar1 routine - | inputs x - | outputs a - | trashes x, z, n - | { - | ld a, 200 - | } - | - | define main routine - | outputs a - | trashes x, z, n - | { - | ld x, 0 - | if z { - | ld x, 1 - | goto bar0 - | } else { - | ld x, 2 - | goto bar1 - | } - | } - = ok - -Here is like just above, but one routine doesn't output `a`. - - | define bar0 routine - | inputs x - | outputs a - | trashes x, z, n - | { - | ld a, x - | } - | - | define bar1 routine - | inputs x - | trashes x, z, n - | { - | ld x, 200 - | } - | - | define main routine - | outputs a - | trashes x, z, n - | { - | ld x, 0 - | if z { - | ld x, 1 - | goto bar0 - | } else { - | ld x, 2 - | goto bar1 - | } - | } - ? InconsistentExitError - -Here is like the above, but the two routines have different inputs, and that's OK. - - | define bar0 routine - | inputs x - | outputs a - | trashes x, z, n - | { - | ld a, x - | } - | - | define bar1 routine - | outputs a - | trashes x, z, n - | { - | ld a, 200 - | } - | - | define main routine - | outputs a - | trashes x, z, n - | { - | ld x, 0 - | if z { - | ld x, 1 - | goto bar0 - | } else { - | ld x, 2 - | goto bar1 - | } - | } - = ok - -Another inconsistent exit test, this one based on "real" code -(the `ribos2` demo). - - | typedef routine - | inputs border_color, vic_intr - | outputs border_color, vic_intr - | trashes a, z, n, c - | irq_handler - | - | vector irq_handler cinv @ $314 - | vector irq_handler saved_irq_vec - | byte vic_intr @ $d019 - | byte border_color @ $d020 - | - | define pla_tay_pla_tax_pla_rti routine - | inputs a - | trashes a - | @ $EA81 - | - | define our_service_routine irq_handler - | { - | ld a, vic_intr - | st a, vic_intr - | and a, 1 - | cmp a, 1 - | if not z { - | goto saved_irq_vec - | } else { - | ld a, border_color - | xor a, $ff - | st a, border_color - | goto pla_tay_pla_tax_pla_rti - | } - | } - | - | define main routine - | { - | } - ? InconsistentExitError - - | typedef routine - | inputs border_color, vic_intr - | outputs border_color, vic_intr - | trashes a, z, n, c - | irq_handler - | - | vector irq_handler cinv @ $314 - | vector irq_handler saved_irq_vec - | byte vic_intr @ $d019 - | byte border_color @ $d020 - | - | define pla_tay_pla_tax_pla_rti routine - | inputs border_color, vic_intr - | outputs border_color, vic_intr - | trashes a, z, n, c - | @ $EA81 - | - | define our_service_routine irq_handler - | { - | ld a, vic_intr - | st a, vic_intr - | and a, 1 - | cmp a, 1 - | if not z { - | goto saved_irq_vec - | } else { - | ld a, border_color - | xor a, $ff - | st a, border_color - | goto pla_tay_pla_tax_pla_rti - | } - | } - | - | define main routine - | { - | } - = ok - -Can't `goto` a routine that outputs or trashes more than the current routine. - - | define bar routine trashes x, y, z, n { - | ld x, 200 - | ld y, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | goto bar - | } - ? IncompatibleConstraintsError - - | define bar routine outputs y trashes z, n { - | ld y, 200 - | } - | - | define main routine trashes x, z, n { - | ld x, 0 - | goto bar - | } - ? IncompatibleConstraintsError - -Can `goto` a routine that outputs or trashes less than the current routine. - - | define bar routine trashes x, z, n { - | ld x, 1 - | } - | - | define main routine trashes a, x, z, n { - | ld a, 0 - | ld x, 0 - | goto bar - | } - = ok - -Indirect goto. - - | vector routine outputs x trashes a, z, n foo - | - | define bar routine outputs x trashes a, z, n { - | ld x, 200 - | } - | - | define main routine outputs x trashes foo, a, z, n { - | copy bar, foo - | goto foo - | } - = ok - -Jumping through the vector does indeed trash, or output, the things the -vector says it does. - - | vector routine - | trashes a, x, z, n - | foo - | - | define bar routine - | trashes a, x, z, n { - | ld x, 200 - | } - | - | define sub routine - | trashes foo, a, x, z, n { - | ld x, 0 - | copy bar, foo - | goto foo - | } - | - | define main routine - | outputs a - | trashes foo, x, z, n { - | call sub - | ld a, x - | } - ? UnmeaningfulReadError: x +A `with interrupts` block cannot appear within a `with interrupts` block. | vector routine + | inputs x | outputs x - | trashes a, z, n foo + | trashes z, n + | bar | - | define bar routine + | define foo routine + | inputs x | outputs x - | trashes a, z, n { - | ld x, 200 - | } - | - | define sub routine - | outputs x - | trashes foo, a, z, n { - | ld x, 0 - | copy bar, foo - | goto foo - | } - | - | define main routine - | outputs a - | trashes foo, x, z, n { - | call sub - | ld a, x - | } - = ok - -### vector tables ### - -A vector can be copied into a vector table. - - | vector routine - | outputs x - | trashes a, z, n - | one - | vector (routine - | outputs x - | trashes a, z, n) - | table[256] many - | - | define bar routine outputs x trashes a, z, n { - | ld x, 200 - | } - | - | define main routine - | inputs one, many - | outputs one, many - | trashes a, x, n, z + | trashes z, n | { - | ld x, 0 - | copy bar, one - | copy one, many + x - | } - = ok - -A vector can be copied out of a vector table. - - | vector routine - | outputs x - | trashes a, z, n - | one - | vector (routine - | outputs x - | trashes a, z, n) - | table[256] many - | - | define bar routine outputs x trashes a, z, n { - | ld x, 200 + | inc x | } | | define main routine - | inputs one, many - | outputs one, many - | trashes a, x, n, z + | trashes bar, a, n, z | { - | ld x, 0 - | copy many + x, one - | call one + | with interrupts off { + | copy foo, bar + | with interrupts off { + | copy foo, bar + | } + | } | } - = ok - -A routine can be copied into a vector table. - - | vector (routine - | outputs x - | trashes a, z, n) - | table[256] many - | - | define bar routine outputs x trashes a, z, n { - | ld x, 200 - | } - | - | define main routine - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | copy bar, many + x - | } - = ok - -A vector in a vector table cannot be directly called. - - | vector (routine - | outputs x - | trashes a, z, n) - | table[256] many - | - | routine bar outputs x trashes a, z, n { - | ld x, 200 - | } - | - | define main routine - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | copy bar, many + x - | call many + x - | } - ? SyntaxError + ? IllegalJumpError ### typedef ### @@ -4250,29 +862,3 @@ The new style routine definitions support typedefs. | copy foo, vec | } = ok - -### static ### - -When memory locations are defined static to a routine, they cannot be -directly input, nor directly output; and since they are always initialized, -they cannot be trashed. Thus, they really don't participate in the analysis. - - | define foo routine - | inputs x - | outputs x - | trashes z, n - | static byte t : 0 - | { - | st x, t - | inc t - | ld x, t - | } - | - | define main routine - | trashes a, x, z, n - | static byte t : 0 - | { - | ld x, t - | call foo - | } - = ok diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index ec29a16..687d57e 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -6,9 +6,6 @@ SixtyPical to 6502 machine code. [Falderal]: http://catseye.tc/node/Falderal - -> Functionality "Compile SixtyPical program" is implemented by - -> shell command "bin/sixtypical --output-format=c64-basic-prg --traceback %(test-body-file) --output /tmp/foo && tests/appliances/bin/dcc6502-adapter Tests for functionality "Compile SixtyPical program" Null program. @@ -1400,7 +1397,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 +1412,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 +1448,7 @@ Write literal through a pointer. | { | ld y, 0 | point ptr into tab { + | reset ptr 0 | copy 123, [ptr] + y | } | } @@ -1458,6 +1474,7 @@ Write stored value through a pointer. | { | ld y, 0 | point ptr into tab { + | reset ptr 0 | copy foo, [ptr] + y | } | } @@ -1483,6 +1500,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 +1515,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 +1561,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 +1594,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 +1625,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 +1674,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 +1706,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 diff --git a/tests/SixtyPical Control Flow.md b/tests/SixtyPical Control Flow.md new file mode 100644 index 0000000..5572529 --- /dev/null +++ b/tests/SixtyPical Control Flow.md @@ -0,0 +1,1845 @@ +SixtyPical Analysis - Control Flow +================================== + +This is a test suite, written in [Falderal][] format, for the SixtyPical +static analysis rules, with regard to flow of control. + +[Falderal]: http://catseye.tc/node/Falderal + + -> Tests for functionality "Analyze SixtyPical program" + +### call ### + +You can't call a non-routine. + + | byte up + | + | define main routine outputs x, y trashes z, n { + | ld x, 0 + | ld y, 1 + | call up + | } + ? TypeMismatchError: up + + | define main routine 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 + | + | define main routine { + | goto foo + | } + ? TypeMismatchError: foo + +When calling a routine, all of the locations it lists as inputs must be +initialized. + + | byte lives + | + | define foo routine + | inputs x + | trashes lives + | { + | st x, lives + | } + | + | define main routine + | { + | call foo + | } + ? UnmeaningfulReadError: x + +Note that if you call a routine that trashes a location, you also trash it. + + | byte lives + | + | define foo routine + | inputs x + | trashes lives + | { + | st x, lives + | } + | + | define main routine + | outputs x, z, n + | { + | ld x, 0 + | call foo + | } + ? ForbiddenWriteError: lives + + | byte lives + | + | define foo routine + | inputs x + | trashes lives + | { + | st x, lives + | } + | + | define main routine + | outputs x, z, n + | trashes lives + | { + | ld x, 0 + | call foo + | } + = ok + +You can't output a value that the thing you called trashed. + + | byte lives + | + | define foo routine + | inputs x + | trashes lives + | { + | st x, lives + | } + | + | define main routine + | outputs x, z, n, lives + | { + | ld x, 0 + | call foo + | } + ? UnmeaningfulOutputError: lives + +...unless you write to it yourself afterwards. + + | byte lives + | + | define foo routine + | inputs x + | trashes lives + | { + | st x, lives + | } + | + | define main routine + | outputs x, z, n, lives + | { + | ld x, 0 + | call foo + | st x, lives + | } + = ok + +If a routine declares outputs, they are initialized in the caller after +calling it. + + | define foo routine + | outputs x, z, n + | { + | ld x, 0 + | } + | + | define main routine + | outputs a + | trashes x, z, n + | { + | call foo + | ld a, x + | } + = ok + + | define foo routine + | { + | } + | + | define main routine + | outputs a + | trashes x + | { + | call foo + | ld a, x + | } + ? UnmeaningfulReadError: x + +If a routine trashes locations, they are uninitialized in the caller after +calling it. + + | define foo routine + | trashes x, z, n + | { + | ld x, 0 + | } + = ok + + | define foo routine + | trashes x, z, n + | { + | ld x, 0 + | } + | + | define main routine + | outputs a + | trashes x, z, n + | { + | call foo + | ld a, x + | } + ? UnmeaningfulReadError: x + +Calling an extern is just the same as calling a defined routine with the +same constraints. + + | define chrout routine + | inputs a + | trashes a + | @ 65490 + | + | define main routine + | trashes a, z, n + | { + | ld a, 65 + | call chrout + | } + = ok + + | define chrout routine + | inputs a + | trashes a + | @ 65490 + | + | define main routine + | trashes a, z, n + | { + | call chrout + | } + ? UnmeaningfulReadError: a + + | define chrout routine + | inputs a + | trashes a + | @ 65490 + | + | define main routine + | trashes a, x, z, n + | { + | ld a, 65 + | call chrout + | ld x, a + | } + ? UnmeaningfulReadError: a + +### if ### + +Both blocks of an `if` are analyzed. + + | define foo routine + | inputs a + | outputs x + | trashes a, z, n, c + | { + | cmp a, 42 + | if z { + | ld x, 7 + | } else { + | ld x, 23 + | } + | } + = ok + +If a location is initialized in one block, it must be initialized in the other as well +in order to be considered to be initialized after the block. If it is not consistent, +it will be considered uninitialized. + + | define foo routine + | inputs a + | outputs x + | trashes a, z, n, c + | { + | cmp a, 42 + | if z { + | ld x, 7 + | } else { + | ld a, 23 + | } + | } + ? UnmeaningfulOutputError: x + + | define foo routine + | inputs a + | outputs x + | trashes a, z, n, c + | { + | cmp a, 42 + | if z { + | ld a, 6 + | } else { + | ld x, 7 + | } + | } + ? UnmeaningfulOutputError: x + + | define foo routine + | inputs a + | outputs x + | trashes a, z, n, c + | { + | cmp a, 42 + | if not z { + | ld a, 6 + | } else { + | ld x, 7 + | } + | } + ? UnmeaningfulOutputError: x + + | define foo routine + | inputs a + | trashes a, x, z, n, c + | { + | cmp a, 42 + | if not z { + | ld a, 6 + | } else { + | ld x, 7 + | } + | ld a, x + | } + ? UnmeaningfulReadError: x + +If we don't care if it's uninitialized after the `if`, that's okay then. + + | define foo routine + | inputs a + | trashes a, x, z, n, c + | { + | cmp a, 42 + | if not z { + | ld a, 6 + | } else { + | ld x, 7 + | } + | } + = ok + +Or, if it does get initialized on both branches, that's okay then. + + | define foo routine + | inputs a + | outputs x + | trashes a, z, n, c + | { + | cmp a, 42 + | if not z { + | ld x, 0 + | ld a, 6 + | } else { + | ld x, 7 + | } + | } + = ok + +However, this only pertains to initialization. If a value is already +initialized, either because it was set previous to the `if`, or is an +input to the routine, and it is initialized in one branch, it need not +be initialized in the other. + + | define foo routine + | outputs x + | trashes a, z, n, c + | { + | ld x, 0 + | ld a, 0 + | cmp a, 42 + | if z { + | ld x, 7 + | } else { + | ld a, 23 + | } + | } + = ok + + | define foo routine + | inputs x + | outputs x + | trashes a, z, n, c + | { + | ld a, 0 + | cmp a, 42 + | if z { + | ld x, 7 + | } else { + | ld a, 23 + | } + | } + = ok + +An `if` with a single block is analyzed as if it had an empty `else` block. + + | define foo routine + | inputs a + | outputs x + | trashes a, z, n, c + | { + | cmp a, 42 + | if z { + | ld x, 7 + | } + | } + ? UnmeaningfulOutputError: x + + | define foo routine + | inputs a + | outputs x + | trashes a, z, n, c + | { + | ld x, 0 + | cmp a, 42 + | if z { + | ld x, 7 + | } + | } + = ok + + | define foo routine + | inputs a + | outputs x + | trashes a, z, n, c + | { + | ld x, 0 + | cmp a, 42 + | if not z { + | ld x, 7 + | } + | } + = ok + +The cardinal rule for trashes in an `if` is the "union rule": if one branch +trashes {`a`} and the other branch trashes {`b`} then the whole `if` statement +trashes {`a`, `b`}. + + | define foo routine + | inputs a, x, z + | trashes a, x + | { + | if z { + | trash a + | } else { + | trash x + | } + | } + = ok + + | define foo routine + | inputs a, x, z + | trashes a + | { + | if z { + | trash a + | } else { + | trash x + | } + | } + ? ForbiddenWriteError: x (in foo, line 10) + + | define foo routine + | inputs a, x, z + | trashes x + | { + | if z { + | trash a + | } else { + | trash x + | } + | } + ? ForbiddenWriteError: a (in foo, line 10) + +### repeat ### + +Repeat loop. + + | define main routine + | outputs x, y, n, z, c + | { + | ld x, 0 + | ld y, 15 + | repeat { + | inc x + | inc y + | cmp x, 10 + | } until z + | } + = ok + +You can initialize something inside the loop that was uninitialized outside. + + | define main routine + | outputs x, y, n, z, c + | { + | ld x, 0 + | repeat { + | ld y, 15 + | inc x + | cmp x, 10 + | } until z + | } + = ok + +But you can't UNinitialize something at the end of the loop that you need +initialized at the start. + + | define foo routine + | trashes y + | { + | } + | + | define main routine + | outputs x, y, n, z, c + | { + | ld x, 0 + | ld y, 15 + | repeat { + | inc x + | inc y + | call foo + | cmp x, 10 + | } until z + | } + ? UnmeaningfulReadError: y + +And if you trash the test expression (i.e. `z` in the below) inside the loop, +this is an error too. + + | word one : 0 + | word two : 0 + | + | define main routine + | inputs one, two + | outputs two + | trashes a, z, n + | { + | repeat { + | copy one, two + | } until z + | } + ? UnmeaningfulReadError: z + +The body of `repeat forever` can be empty. + + | define main routine + | { + | repeat { + | } forever + | } + = ok + +While `repeat` is most often used with `z`, it can also be used with `n`. + + | define main routine + | outputs y, n, z + | { + | ld y, 15 + | repeat { + | dec y + | } until n + | } + = ok + +### for ### + +Basic "open-faced for" loop. We'll start with the "upto" variant. + +#### upward-counting variant + +Even though we do not give the starting value in the "for" construct, +we know the exact range the loop variable takes on. + + | byte table[16] tab + | + | define foo routine inputs tab trashes a, x, c, z, v, n { + | ld x, 0 + | for x up to 15 { + | ld a, tab + x + | } + | } + = ok + + | byte table[15] tab + | + | define foo routine inputs tab trashes a, x, c, z, v, n { + | ld x, 0 + | for x up to 15 { + | ld a, tab + x + | } + | } + ? RangeExceededError + +You need to initialize the loop variable before the loop. + + | byte table[16] tab + | + | define foo routine inputs tab trashes a, x, c, z, v, n { + | for x up to 15 { + | ld a, 0 + | } + | } + ? UnmeaningfulReadError + +Because routines currently do not include range constraints, +the loop variable may not be useful as an input (the location +is assumed to have the maximum range.) + + | byte table[16] tab + | + | define foo routine + | inputs tab, x + | trashes a, x, c, z, v, n { + | for x up to 15 { + | ld a, 0 + | } + | } + ? RangeExceededError + +You cannot modify the loop variable in a "for" loop. + + | byte table[16] tab + | + | define foo routine inputs tab trashes a, x, c, z, v, n { + | ld x, 0 + | for x up to 15 { + | ld x, 0 + | } + | } + ? ForbiddenWriteError + +This includes nesting a loop on the same variable. + + | byte table[16] tab + | + | define foo routine inputs tab trashes a, x, c, z, v, n { + | ld x, 0 + | for x up to 8 { + | for x up to 15 { + | ld a, 0 + | } + | } + | } + ? ForbiddenWriteError + +But nesting with two different variables is okay. + + | byte table[16] tab + | + | define foo routine inputs tab trashes a, x, y, c, z, v, n { + | ld x, 0 + | for x up to 8 { + | ld a, x + | ld y, a + | for y up to 15 { + | ld a, 0 + | } + | } + | } + = ok + +Inside the inner loop, the outer variable is still not writeable. + + | byte table[16] tab + | + | define foo routine inputs tab trashes a, x, y, c, z, v, n { + | ld x, 0 + | for x up to 8 { + | ld a, x + | ld y, a + | for y up to 15 { + | ld x, 0 + | } + | } + | } + ? ForbiddenWriteError + +If the range isn't known to be smaller than the final value, you can't go up to it. + + | byte table[32] tab + | + | define foo routine inputs tab trashes a, x, c, z, v, n { + | ld x, 16 + | for x up to 15 { + | ld a, tab + x + | } + | } + ? RangeExceededError + +You can initialize something inside the loop that was uninitialized outside. + + | define main routine + | outputs x, y, n, z + | trashes c + | { + | ld x, 0 + | for x up to 15 { + | ld y, 15 + | } + | } + = ok + +But you can't UNinitialize something at the end of the loop that you need +initialized at the start of that loop. + + | define foo routine + | trashes y + | { + | } + | + | define main routine + | outputs x, y, n, z + | trashes c + | { + | ld x, 0 + | ld y, 15 + | for x up to 15 { + | inc y + | call foo + | } + | } + ? UnmeaningfulReadError: y + +And in particular, you can't uninitialize the loop variable, in the loop. + + | define foo routine + | trashes x + | { + | } + | + | define main routine + | outputs x, y, n, z + | trashes c + | { + | ld x, 0 + | ld y, 15 + | for x up to 15 { + | inc y + | call foo + | } + | } + ? ForbiddenWriteError: x + +So, if you call a routine from inside the loop, it better not also +loop on the same variable. + + | define foo routine + | inputs y + | outputs x, y, n, z + | trashes c + | { + | ld x, 0 + | for x up to 15 { + | inc y + | } + | } + | + | define main routine + | outputs x, y, n, z + | trashes c + | { + | ld x, 0 + | ld y, 15 + | for x up to 15 { + | inc y + | call foo + | } + | } + ? ForbiddenWriteError: x + +But if you take care to save and restore the loop variable in the +called routine, it will be okay. + + | define foo routine + | inputs y + | outputs y, n, z + | trashes a, c + | { + | save x { + | ld x, 0 + | for x up to 15 { + | inc y + | } + | } + | } + | + | define main routine + | outputs x, y, n, z + | trashes a, c + | { + | ld x, 0 + | ld y, 15 + | for x up to 15 { + | inc y + | call foo + | } + | } + = ok + +The "for" loop does not preserve the `z` or `n` registers. + + | define foo routine trashes x { + | ld x, 0 + | for x up to 15 { + | } + | } + ? ForbiddenWriteError + +But it does preserve the other registers, such as `c`. + + | define foo routine trashes x, z, n { + | ld x, 0 + | for x up to 15 { + | } + | } + = ok + +In fact it does not strictly trash `z` and `n`, as they are +always set to known values after the loop. TODO: document +what these known values are! + + | define foo routine outputs z, n trashes x { + | ld x, 0 + | for x up to 15 { + | } + | } + = ok + +#### downward-counting variant + +In a "for" loop (downward-counting variant), we know the exact range the loop variable takes on. + + | byte table[16] tab + | + | define foo routine inputs tab trashes a, x, c, z, v, n { + | ld x, 15 + | for x down to 0 { + | ld a, tab + x + | } + | } + = ok + + | byte table[15] tab + | + | define foo routine inputs tab trashes a, x, c, z, v, n { + | ld x, 15 + | for x down to 0 { + | ld a, tab + x + | } + | } + ? RangeExceededError + +You need to initialize the loop variable before a "for" loop (downward variant). + + | byte table[16] tab + | + | define foo routine inputs tab trashes a, x, c, z, v, n { + | for x down to 15 { + | ld a, 0 + | } + | } + ? UnmeaningfulReadError + +You cannot modify the loop variable in a "for" loop (downward variant). + + | byte table[16] tab + | + | define foo routine inputs tab trashes a, x, c, z, v, n { + | ld x, 15 + | for x down to 0 { + | ld x, 0 + | } + | } + ? ForbiddenWriteError + +If the range isn't known to be larger than the final value, you can't go down to it. + + | byte table[32] tab + | + | define foo routine inputs tab trashes a, x, c, z, v, n { + | ld x, 0 + | for x down to 0 { + | ld a, tab + x + | } + | } + ? RangeExceededError + +The "for" loop does not preserve the `z` or `n` registers. + + | define foo routine trashes x { + | ld x, 15 + | for x down to 0 { + | } + | } + ? ForbiddenWriteError + +But it does preserve the other registers, such as `c`. + + | define foo routine trashes x, z, n { + | ld x, 15 + | for x down to 0 { + | } + | } + = ok + +In fact it does not strictly trash `z` and `n`, as they are +always set to known values after the loop. TODO: document +what these known values are! + + | define foo routine outputs z, n trashes x { + | ld x, 15 + | for x down to 0 { + | } + | } + = ok + +### routines ### + +Routines are constants. You need not, and in fact cannot, specify a constant +as an input to, an output of, or as a trashed value of a routine. + + | vector routine + | inputs x + | outputs x + | trashes z, n + | vec + | + | define foo routine + | inputs x + | outputs x + | trashes z, n + | { + | inc x + | } + | + | define main routine + | inputs foo + | outputs vec + | trashes a, z, n + | { + | copy foo, vec + | } + ? ConstantConstraintError: foo + + | vector routine + | inputs x + | outputs x + | trashes z, n + | vec + | + | define foo routine + | inputs x + | outputs x + | trashes z, n + | { + | inc x + | } + | + | define main routine + | outputs vec, foo + | trashes a, z, n + | { + | copy foo, vec + | } + ? ConstantConstraintError: foo + + | vector routine + | inputs x + | outputs x + | trashes z, n + | vec + | + | define foo routine + | inputs x + | outputs x + | trashes z, n + | { + | inc x + | } + | + | define main routine + | outputs vec + | trashes a, z, n, foo + | { + | copy foo, vec + | } + ? ConstantConstraintError: foo + +#### routine-vector type compatibility + +You can copy the address of a routine into a vector, if that vector type +is at least as "wide" as the type of the routine. More specifically, + +- the vector must take _at least_ the inputs that the routine takes +- the vector must produce _at least_ the outputs that the routine produces +- the vector must trash _at least_ what the routine trashes + +If the vector and the routine have the very same signature, that's not an error. + + | vector routine + | inputs x, y + | outputs x, y + | trashes z, n + | vec + | + | define foo routine + | inputs x, y + | outputs x, y + | trashes z, n + | { + | inc x + | inc y + | } + | + | define main routine + | outputs vec + | trashes a, z, n + | { + | copy foo, vec + | } + = ok + +If the vector takes an input that the routine doesn't take, that's not an error. +(The interface requires that a parameter be specified before calling, but the +implementation doesn't actually read it.) + + | vector routine + | inputs x, y, a + | outputs x, y + | trashes z, n + | vec + | + | define foo routine + | inputs x, y + | outputs x, y + | trashes z, n + | { + | inc x + | inc y + | } + | + | define main routine + | outputs vec + | trashes a, z, n + | { + | copy foo, vec + | } + = ok + +If the vector fails to take an input that the routine takes, that's an error. + + | vector routine + | inputs x + | outputs x, y + | trashes z, n + | vec + | + | define foo routine + | inputs x, y + | outputs x, y + | trashes z, n + | { + | inc x + | inc y + | } + | + | define main routine + | outputs vec + | trashes a, z, n + | { + | copy foo, vec + | } + ? IncompatibleConstraintsError + +If the vector produces an output that the routine doesn't produce, that's not an error. +(The interface claims the result of calling the routine is defined, but the implementation +actually preserves it instead of changing it; the caller can still treat it as a defined +output.) + + | vector routine + | inputs x, y + | outputs x, y, a + | trashes z, n + | vec + | + | define foo routine + | inputs x, y + | outputs x, y + | trashes z, n + | { + | inc x + | inc y + | } + | + | define main routine + | outputs vec + | trashes a, z, n + | { + | copy foo, vec + | } + = ok + +If the vector fails to produce an output that the routine produces, that's an error. + + | vector routine + | inputs x, y + | outputs x + | trashes z, n + | vec + | + | define foo routine + | inputs x, y + | outputs x, y + | trashes z, n + | { + | inc x + | inc y + | } + | + | define main routine + | outputs vec + | trashes a, z, n + | { + | copy foo, vec + | } + ? IncompatibleConstraintsError + +If the vector fails to trash something the routine trashes, that's an error. + + | vector routine + | inputs x, y + | outputs x, y + | trashes z + | vec + | + | define foo routine + | inputs x, y + | outputs x, y + | trashes z, n + | { + | inc x + | inc y + | } + | + | define main routine + | outputs vec + | trashes a, z, n + | { + | copy foo, vec + | } + ? IncompatibleConstraintsError + +If the vector trashes something the routine doesn't trash, that's not an error. +(The implementation preserves something the interface doesn't guarantee is +preserved. The caller gets no guarantee that it's preserved. It actually is, +but it doesn't know that.) + + | vector routine + | inputs x, y + | outputs x, y + | trashes a, z, n + | vec + | + | define foo routine + | inputs x, y + | outputs x, y + | trashes z, n + | { + | inc x + | inc y + | } + | + | define main routine + | outputs vec + | trashes a, z, n + | { + | copy foo, vec + | } + = ok + +#### other properties of routines + +Routines are read-only. + + | vector routine + | inputs x + | outputs x + | trashes z, n + | vec + | + | define foo routine + | inputs x + | outputs x + | trashes z, n + | { + | inc x + | } + | + | define main routine + | outputs vec + | trashes a, z, n + | { + | copy vec, foo + | } + ? TypeMismatchError + +Indirect call. + + | vector routine + | outputs x trashes z, n + | foo + | + | define bar routine outputs x trashes z, n { + | ld x, 200 + | } + | + | define main routine outputs x, foo trashes a, z, n { + | copy bar, foo + | call foo + | } + = ok + +Calling the vector does indeed trash the things the vector says it does. + + | vector routine trashes x, z, n foo + | + | define bar routine trashes x, z, n { + | ld x, 200 + | } + | + | define main routine outputs x, foo trashes z, n { + | ld x, 0 + | copy bar, foo + | call foo + | } + ? UnmeaningfulOutputError: x + +For now at least, you cannot have a `goto` inside a `repeat` loop. + + | define bar routine trashes x, z, n { + | ld x, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | repeat { + | inc x + | goto bar + | } until z + | } + ? IllegalJumpError + +`goto`, as a matter of syntax, can only appear at the end +of a block; but it need not be the final instruction in a +routine. + + | define bar routine trashes x, z, n { + | ld x, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | goto bar + | } + = ok + + | define bar routine trashes x, z, n { + | ld x, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | if z { + | ld x, 1 + | goto bar + | } + | } + = ok + + | define bar routine trashes x, z, n { + | ld x, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | if z { + | ld x, 1 + | goto bar + | } + | goto bar + | } + = ok + + | define bar routine trashes x, z, n { + | ld x, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | if z { + | ld x, 1 + | goto bar + | } + | ld x, 0 + | } + = ok + + | define bar routine trashes x, z, n { + | ld x, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | if z { + | ld x, 1 + | goto bar + | } else { + | ld x, 0 + | goto bar + | } + | } + = ok + + | define bar routine trashes x, z, n { + | ld x, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | if z { + | ld x, 1 + | goto bar + | } else { + | ld x, 0 + | } + | } + = ok + + | define bar routine trashes x, z, n { + | ld x, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | if z { + | ld x, 1 + | goto bar + | } else { + | ld x, 0 + | } + | ld x, 0 + | } + = ok + + | define bar routine trashes x, z, n { + | ld x, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | if z { + | ld x, 1 + | goto bar + | } else { + | ld x, 0 + | } + | goto bar + | } + = ok + +Even though `goto` can only appear at the end of a block, +you can still wind up with dead code; the analysis detects +this. + + | define bar routine trashes x, z, n { + | ld x, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | if z { + | ld x, 1 + | goto bar + | } else { + | ld x, 0 + | goto bar + | } + | ld x, 100 + | } + ? TerminatedContextError + +It is important that the type context at every +`goto` is compatible with the type context at the end of +the routine. + + | define bar routine + | inputs x + | trashes x, z, n + | { + | ld x, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | if z { + | ld x, 1 + | goto bar + | } else { + | ld x, 0 + | } + | ld x, 1 + | } + = ok + +Here, we try to trash `x` before `goto`ing a routine that inputs `x`. + + | define bar routine + | inputs x + | trashes x, z, n + | { + | ld x, 200 + | } + | + | define main routine + | outputs a + | trashes x, z, n + | { + | ld x, 0 + | if z { + | trash x + | goto bar + | } else { + | trash x + | } + | ld a, 1 + | } + ? UnmeaningfulReadError: x + +Here, we declare that main outputs `a`, but we `goto` a routine that does not output `a`. + + | define bar routine + | inputs x + | trashes x, z, n + | { + | ld x, 200 + | } + | + | 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 + | } + ? 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 + +Here, we declare that main outputs `a`, and we `goto` two routines, and they both output `a`. + + | define bar0 routine + | inputs x + | outputs a + | trashes x, z, n + | { + | ld a, x + | } + | + | define bar1 routine + | inputs x + | outputs a + | trashes x, z, n + | { + | ld a, 200 + | } + | + | define main routine + | outputs a + | trashes x, z, n + | { + | ld x, 0 + | if z { + | ld x, 1 + | goto bar0 + | } else { + | ld x, 2 + | goto bar1 + | } + | } + = ok + +Here is like just above, but one routine doesn't output `a`. + + | define bar0 routine + | inputs x + | outputs a + | trashes x, z, n + | { + | ld a, x + | } + | + | define bar1 routine + | inputs x + | trashes x, z, n + | { + | ld x, 200 + | } + | + | define main routine + | outputs a + | trashes x, z, n + | { + | ld x, 0 + | if z { + | ld x, 1 + | goto bar0 + | } else { + | ld x, 2 + | goto bar1 + | } + | } + ? InconsistentExitError + +Here is like the above, but the two routines have different inputs, and that's OK. + + | define bar0 routine + | inputs x + | outputs a + | trashes x, z, n + | { + | ld a, x + | } + | + | define bar1 routine + | outputs a + | trashes x, z, n + | { + | ld a, 200 + | } + | + | define main routine + | outputs a + | trashes x, z, n + | { + | ld x, 0 + | if z { + | ld x, 1 + | goto bar0 + | } else { + | ld x, 2 + | goto bar1 + | } + | } + = ok + +Another inconsistent exit test, this one based on "real" code +(the `ribos2` demo). + + | typedef routine + | inputs border_color, vic_intr + | outputs border_color, vic_intr + | trashes a, z, n, c + | irq_handler + | + | vector irq_handler cinv @ $314 + | vector irq_handler saved_irq_vec + | byte vic_intr @ $d019 + | byte border_color @ $d020 + | + | define pla_tay_pla_tax_pla_rti routine + | inputs a + | trashes a + | @ $EA81 + | + | define our_service_routine irq_handler + | { + | ld a, vic_intr + | st a, vic_intr + | and a, 1 + | cmp a, 1 + | if not z { + | goto saved_irq_vec + | } else { + | ld a, border_color + | xor a, $ff + | st a, border_color + | goto pla_tay_pla_tax_pla_rti + | } + | } + | + | define main routine + | { + | } + ? InconsistentExitError + + | typedef routine + | inputs border_color, vic_intr + | outputs border_color, vic_intr + | trashes a, z, n, c + | irq_handler + | + | vector irq_handler cinv @ $314 + | vector irq_handler saved_irq_vec + | byte vic_intr @ $d019 + | byte border_color @ $d020 + | + | define pla_tay_pla_tax_pla_rti routine + | inputs border_color, vic_intr + | outputs border_color, vic_intr + | trashes a, z, n, c + | @ $EA81 + | + | define our_service_routine irq_handler + | { + | ld a, vic_intr + | st a, vic_intr + | and a, 1 + | cmp a, 1 + | if not z { + | goto saved_irq_vec + | } else { + | ld a, border_color + | xor a, $ff + | st a, border_color + | goto pla_tay_pla_tax_pla_rti + | } + | } + | + | define main routine + | { + | } + = ok + +Can't `goto` a routine that outputs or trashes more than the current routine. + + | define bar routine trashes x, y, z, n { + | ld x, 200 + | ld y, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | goto bar + | } + ? IncompatibleConstraintsError + + | define bar routine outputs y trashes z, n { + | ld y, 200 + | } + | + | define main routine trashes x, z, n { + | ld x, 0 + | goto bar + | } + ? IncompatibleConstraintsError + +Can `goto` a routine that outputs or trashes less than the current routine. + + | define bar routine trashes x, z, n { + | ld x, 1 + | } + | + | define main routine trashes a, x, z, n { + | ld a, 0 + | ld x, 0 + | goto bar + | } + = ok + +Indirect goto. + + | vector routine outputs x trashes a, z, n foo + | + | define bar routine outputs x trashes a, z, n { + | ld x, 200 + | } + | + | define main routine outputs x trashes foo, a, z, n { + | copy bar, foo + | goto foo + | } + = ok + +Jumping through the vector does indeed trash, or output, the things the +vector says it does. + + | vector routine + | trashes a, x, z, n + | foo + | + | define bar routine + | trashes a, x, z, n { + | ld x, 200 + | } + | + | define sub routine + | trashes foo, a, x, z, n { + | ld x, 0 + | copy bar, foo + | goto foo + | } + | + | define main routine + | outputs a + | trashes foo, x, z, n { + | call sub + | ld a, x + | } + ? UnmeaningfulReadError: x + + | vector routine + | outputs x + | trashes a, z, n foo + | + | define bar routine + | outputs x + | trashes a, z, n { + | ld x, 200 + | } + | + | define sub routine + | outputs x + | trashes foo, a, z, n { + | ld x, 0 + | copy bar, foo + | goto foo + | } + | + | define main routine + | outputs a + | trashes foo, x, z, n { + | call sub + | ld a, x + | } + = ok + +### vector tables ### + +A vector can be copied into a vector table. + + | vector routine + | outputs x + | trashes a, z, n + | one + | vector (routine + | outputs x + | trashes a, z, n) + | table[256] many + | + | define bar routine outputs x trashes a, z, n { + | ld x, 200 + | } + | + | define main routine + | inputs one, many + | outputs one, many + | trashes a, x, n, z + | { + | ld x, 0 + | copy bar, one + | copy one, many + x + | } + = ok + +A vector can be copied out of a vector table. + + | vector routine + | outputs x + | trashes a, z, n + | one + | vector (routine + | outputs x + | trashes a, z, n) + | table[256] many + | + | define bar routine outputs x trashes a, z, n { + | ld x, 200 + | } + | + | define main routine + | inputs one, many + | outputs one, many + | trashes a, x, n, z + | { + | ld x, 0 + | copy many + x, one + | call one + | } + = ok + +A routine can be copied into a vector table. + + | vector (routine + | outputs x + | trashes a, z, n) + | table[256] many + | + | define bar routine outputs x trashes a, z, n { + | ld x, 200 + | } + | + | define main routine + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | copy bar, many + x + | } + = ok + +A vector in a vector table cannot be directly called. + + | vector (routine + | outputs x + | trashes a, z, n) + | table[256] many + | + | routine bar outputs x trashes a, z, n { + | ld x, 200 + | } + | + | define main routine + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | copy bar, many + x + | call many + x + | } + ? SyntaxError diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index 75e3779..7f95573 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -61,12 +61,6 @@ to pass these tests to be considered an implementation of SixtyPical. [Falderal]: http://catseye.tc/node/Falderal - -> Functionality "Dump fallthru info for SixtyPical program" is implemented by - -> shell command "bin/sixtypical --optimize-fallthru --dump-fallthru-info --analyze-only --traceback %(test-body-file)" - - -> Functionality "Compile SixtyPical program with fallthru optimization" is implemented by - -> shell command "bin/sixtypical --output-format=c64-basic-prg --optimize-fallthru --traceback %(test-body-file) --output /tmp/foo && tests/appliances/bin/dcc6502-adapter Tests for functionality "Dump fallthru info for SixtyPical program" A single routine, obviously, falls through to nothing and has nothing fall diff --git a/tests/SixtyPical Storage.md b/tests/SixtyPical Storage.md new file mode 100644 index 0000000..5222fc8 --- /dev/null +++ b/tests/SixtyPical Storage.md @@ -0,0 +1,1984 @@ +SixtyPical Analysis - Storage +============================= + +This is a test suite, written in [Falderal][] format, for the SixtyPical +static analysis rules, with regard to storage (load, store, tables, etc.) + +[Falderal]: http://catseye.tc/node/Falderal + + -> Tests for functionality "Analyze SixtyPical program" + +### Rudiments ### + +Routines must declare their inputs, outputs, and memory locations they trash. + + | define up routine + | inputs a + | outputs a + | trashes c, z, v, n + | { + | st off, c + | add a, 1 + | } + = ok + +Routines may not declare a memory location to be both an output and trashed. + + | define main routine + | outputs a + | trashes a + | { + | ld a, 0 + | } + ? InconsistentConstraintsError: a + +If a routine declares it outputs a location, that location should be initialized. + + | define main routine + | outputs a, x, z, n + | { + | ld x, 0 + | } + ? UnmeaningfulOutputError: a + + | define main routine + | inputs a + | outputs a + | { + | } + = ok + +If a routine declares it outputs a location, that location may or may not have +been initialized. Trashing is mainly a signal to the caller. + + | define main routine + | trashes x, z, n + | { + | ld x, 0 + | } + = ok + + | define main routine + | trashes x, z, n + | { + | } + = ok + +If a routine modifies a location, it needs to either output it or trash it. + + | define main routine + | { + | ld x, 0 + | } + ? ForbiddenWriteError: x + + | define main routine + | outputs x, z, n + | { + | ld x, 0 + | } + = ok + + | define main routine + | trashes x, z, n + | { + | ld x, 0 + | } + = ok + +This is true regardless of whether it's an input or not. + + | define main routine + | inputs x + | { + | ld x, 0 + | } + ? ForbiddenWriteError: x + + | define main routine + | inputs x + | outputs x, z, n + | { + | ld x, 0 + | } + = ok + + | define main routine + | inputs x + | trashes x, z, n + | { + | ld x, 0 + | } + = ok + +If a routine trashes a location, this must be declared. + + | define foo routine + | trashes x + | { + | trash x + | } + = ok + + | define foo routine + | { + | trash x + | } + ? ForbiddenWriteError: x + + | define foo routine + | outputs x + | { + | trash x + | } + ? UnmeaningfulOutputError: x + +If a routine causes a location to be trashed, this must be declared in the caller. + + | define trash_x routine + | trashes x, z, n + | { + | ld x, 0 + | } + | + | define foo routine + | trashes x, z, n + | { + | call trash_x + | } + = ok + + | define trash_x routine + | trashes x, z, n + | { + | ld x, 0 + | } + | + | define foo routine + | trashes z, n + | { + | call trash_x + | } + ? ForbiddenWriteError: x + + | define trash_x routine + | trashes x, z, n + | { + | ld x, 0 + | } + | + | define foo routine + | outputs x + | trashes z, n + | { + | call trash_x + | } + ? UnmeaningfulOutputError: x (in foo, line 12) + +If a routine reads or writes a user-define memory location, it needs to declare that too. + + | byte b1 @ 60000 + | byte b2 : 3 + | word w1 @ 60001 + | word w2 : 2000 + | + | define main routine + | inputs b1, w1 + | outputs b2, w2 + | trashes a, z, n + | { + | ld a, b1 + | st a, b2 + | copy w1, w2 + | } + = ok + +### ld ### + +Can't `ld` from a memory location that isn't initialized. + + | define main routine + | inputs a, x + | trashes a, z, n + | { + | ld a, x + | } + = ok + + | define main routine + | inputs a + | trashes a + | { + | ld a, x + | } + ? UnmeaningfulReadError: x + +Can't `ld` to a memory location that doesn't appear in (outputs ∪ trashes). + + | define main routine + | trashes a, z, n + | { + | ld a, 0 + | } + = ok + + | define main routine + | outputs a + | trashes z, n + | { + | ld a, 0 + | } + = ok + + | define main routine + | outputs z, n + | trashes a + | { + | ld a, 0 + | } + = ok + + | define main routine + | trashes z, n + | { + | ld a, 0 + | } + ? ForbiddenWriteError: a + + | define main routine + | trashes a, n + | { + | ld a, 0 + | } + ? ForbiddenWriteError: z + +Can't `ld` a `word` type. + + | word foo + | + | define main routine + | inputs foo + | trashes a, n, z + | { + | ld a, foo + | } + ? TypeMismatchError: foo and a + +### st ### + +Can't `st` from a memory location that isn't initialized. + + | byte lives + | define main routine + | inputs x + | trashes lives + | { + | st x, lives + | } + = ok + + | byte lives + | define main routine + | trashes x, lives + | { + | st x, lives + | } + ? UnmeaningfulReadError: x + +Can't `st` to a memory location that doesn't appear in (outputs ∪ trashes). + + | byte lives + | define main routine + | trashes lives + | { + | st 0, lives + | } + = ok + + | byte lives + | define main routine + | outputs lives + | { + | st 0, lives + | } + = ok + + | byte lives + | define main routine + | inputs lives + | { + | st 0, lives + | } + ? ForbiddenWriteError: lives + +Can't `st` a `word` type. + + | word foo + | + | define main routine + | outputs foo + | trashes a, n, z + | { + | ld a, 0 + | st a, foo + | } + ? TypeMismatchError + +### tables ### + +Storing to a table, you must use an index. + + | byte one + | byte table[256] many + | + | define main routine + | outputs one + | trashes a, x, n, z + | { + | ld x, 0 + | ld a, 0 + | st a, one + | } + = ok + + | byte one + | byte table[256] many + | + | define main routine + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | ld a, 0 + | st a, many + | } + ? TypeMismatchError + + | byte one + | byte table[256] many + | + | define main routine + | outputs one + | trashes a, x, n, z + | { + | ld x, 0 + | ld a, 0 + | st a, one + x + | } + ? TypeMismatchError + + | byte one + | byte table[256] many + | + | define main routine + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | ld a, 0 + | st a, many + x + | } + = ok + +The index must be initialized. + + | byte one + | byte table[256] many + | + | define main routine + | outputs many + | trashes a, x, n, z + | { + | ld a, 0 + | st a, many + x + | } + ? UnmeaningfulReadError: x + +Reading from a table, you must use an index. + + | byte one + | + | define main routine + | outputs one + | trashes a, x, n, z + | { + | ld x, 0 + | st x, one + | ld a, one + | } + = ok + + | byte one + | + | define main routine + | outputs one + | trashes a, x, n, z + | { + | ld x, 0 + | st x, one + | ld a, one + x + | } + ? TypeMismatchError + + | byte table[256] many + | + | define main routine + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | ld a, 0 + | st a, many + x + | ld a, many + | } + ? TypeMismatchError + + | byte table[256] many + | + | define main routine + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | ld a, 0 + | st a, many + x + | ld a, many + x + | } + = ok + + | byte table[256] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | ld a, many + x + | } + = ok + +The index must be initialized. + + | byte table[256] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld a, many + x + | } + ? UnmeaningfulReadError: x + +Storing to a table, you may also include a constant offset. + + | byte one + | byte table[256] many + | + | define main routine + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | ld a, 0 + | st a, many + 100 + x + | } + = ok + +Reading from a table, you may also include a constant offset. + + | byte table[256] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | ld a, many + 100 + x + | } + = ok + +Using a constant offset, you can read and write entries in +the table beyond the 256th. + + | byte one + | byte table[1024] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | ld a, many + 999 + x + | st a, many + 1000 + x + | } + = ok + +There are other operations you can do on tables. (1/3) + + | byte table[256] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, c, n, z, v + | { + | ld x, 0 + | ld a, 0 + | st off, c + | add a, many + x + | sub a, many + x + | cmp a, many + x + | } + = ok + +There are other operations you can do on tables. (2/3) + + | byte table[256] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, c, n, z + | { + | ld x, 0 + | ld a, 0 + | and a, many + x + | or a, many + x + | xor a, many + x + | } + = ok + +There are other operations you can do on tables. (3/3) + + | byte table[256] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, c, n, z + | { + | ld x, 0 + | ld a, 0 + | st off, c + | shl many + x + | shr many + x + | inc many + x + | dec many + x + | } + = ok + +Copying to and from a word table. + + | word one + | word table[256] many + | + | define main routine + | inputs one, many + | outputs one, many + | trashes a, x, n, z + | { + | ld x, 0 + | copy one, many + x + | copy many + x, one + | } + = ok + + | word one + | word table[256] many + | + | define main routine + | inputs one, many + | outputs one, many + | trashes a, x, n, z + | { + | ld x, 0 + | copy one, many + | } + ? TypeMismatchError + + | word one + | word table[256] many + | + | define main routine + | inputs one, many + | outputs one, many + | trashes a, x, n, z + | { + | ld x, 0 + | copy one + x, many + | } + ? TypeMismatchError + +You can also copy a literal word to a word table. +(Even if the table has fewer than 256 entries.) + + | word table[32] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | copy 9999, many + x + | } + = ok + +Copying to and from a word table with a constant offset. + + | word one + | word table[256] many + | + | define main routine + | inputs one, many + | outputs one, many + | trashes a, x, n, z + | { + | ld x, 0 + | copy one, many + 100 + x + | copy many + 100 + x, one + | copy 9999, many + 1 + x + | } + = ok + +#### tables: range checking #### + +It is a static analysis error if it cannot be proven that a read or write +to a table falls within the defined size of that table. + +If a table has 256 entries, then there is never a problem (so long as +no constant offset is supplied), because a byte cannot index any entry +outside of 0..255. + +But if the table has fewer than 256 entries, or if a constant offset is +supplied, there is the possibility that the index will refer to an +entry in the table which does not exist. + +A SixtyPical implementation must be able to prove that the index is inside +the range of the table in various ways. The simplest is to show that a +constant value falls inside or outside the range of the table. + + | byte table[32] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 31 + | ld a, many + x + | st a, many + x + | } + = ok + + | byte table[32] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 32 + | ld a, many + x + | } + ? RangeExceededError + + | byte table[32] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 32 + | ld a, 0 + | st a, many + x + | } + ? RangeExceededError + +Any constant offset is taken into account in this check. + + | byte table[32] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 31 + | ld a, many + 1 + x + | } + ? RangeExceededError + + | byte table[32] many + | + | define main routine + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 31 + | ld a, 0 + | st a, many + 1 + x + | } + ? RangeExceededError + +This applies to `copy` as well. + + | word one: 77 + | word table[32] many + | + | define main routine + | inputs many, one + | outputs many, one + | trashes a, x, n, z + | { + | ld x, 31 + | copy one, many + x + | copy many + x, one + | } + = ok + + | word one: 77 + | word table[32] many + | + | define main routine + | inputs many, one + | outputs many, one + | trashes a, x, n, z + | { + | ld x, 32 + | copy many + x, one + | } + ? RangeExceededError + + | word one: 77 + | word table[32] many + | + | define main routine + | inputs many, one + | outputs many, one + | trashes a, x, n, z + | { + | ld x, 32 + | copy one, many + x + | } + ? RangeExceededError + +Any constant offset is taken into account in this check. + + | word one: 77 + | word table[32] many + | + | define main routine + | inputs many, one + | outputs many, one + | trashes a, x, n, z + | { + | ld x, 31 + | copy many + 1 + x, one + | } + ? RangeExceededError + + | word one: 77 + | word table[32] many + | + | define main routine + | inputs many, one + | outputs many, one + | trashes a, x, n, z + | { + | ld x, 31 + | copy one, many + 1 + x + | } + ? RangeExceededError + +`AND`'ing a register with a value ensures the range of the +register will not exceed the range of the value. This can +be used to "clip" the range of an index so that it fits in +a table. + + | word one: 77 + | word table[32] many + | + | define main routine + | inputs a, many, one + | outputs many, one + | trashes a, x, n, z + | { + | and a, 31 + | ld x, a + | copy one, many + x + | copy many + x, one + | } + = ok + +Tests for "clipping", but not enough. + + | word one: 77 + | word table[32] many + | + | define main routine + | inputs a, many, one + | outputs many, one + | trashes a, x, n, z + | { + | and a, 63 + | ld x, a + | copy one, many + x + | copy many + x, one + | } + ? RangeExceededError + + | word one: 77 + | word table[32] many + | + | define main routine + | inputs a, many, one + | outputs many, one + | trashes a, x, n, z + | { + | and a, 31 + | ld x, a + | copy one, many + 1 + x + | copy many + 1 + x, one + | } + ? RangeExceededError + +If you alter the value after "clipping" it, the range can +no longer be guaranteed. + + | word one: 77 + | word table[32] many + | + | define main routine + | inputs a, many, one + | outputs many, one + | trashes a, x, n, z + | { + | and a, 31 + | ld x, a + | inc x + | copy one, many + x + | copy many + x, one + | } + ? RangeExceededError + +When the range of a location is known, incrementing or +decrementing that location's value will shift the known +range. It will not invalidate it unless the known range +is at the limits of the possible ranges for the type. + + | vector routine + | trashes a, z, n + | print + | + | vector (routine + | trashes a, z, n) + | table[32] vectors + | + | define main routine + | inputs vectors, print + | outputs vectors + | trashes print, a, x, z, n, c + | { + | ld x, 0 + | inc x + | copy print, vectors + x + | } + = ok + + | vector routine + | trashes a, z, n + | print + | + | vector (routine + | trashes a, z, n) + | table[32] vectors + | + | define main routine + | inputs vectors, print + | outputs vectors + | trashes print, a, x, z, n, c + | { + | ld x, 32 + | dec x + | copy print, vectors + x + | } + = ok + +### trash ### + +Trash does nothing except indicate that we do not care about the value anymore. + + | define foo routine + | inputs a + | outputs x + | trashes a, z, n + | { + | st a, x + | ld a, 0 + | trash a + | } + = ok + + | define foo routine + | inputs a + | outputs a, x + | trashes z, n + | { + | st a, x + | ld a, 0 + | trash a + | } + ? UnmeaningfulOutputError: a + + | define foo routine + | inputs a + | outputs x + | trashes a, z, n + | { + | st a, x + | trash a + | st a, x + | } + ? UnmeaningfulReadError: a + +### copy ### + +Can't `copy` from a memory location that isn't initialized. + + | byte lives + | define main routine + | inputs x + | outputs lives + | trashes a, z, n + | { + | copy x, lives + | } + = ok + + | byte lives + | define main routine + | outputs lives + | trashes x, a, z, n + | { + | copy x, lives + | } + ? UnmeaningfulReadError: x + +Can't `copy` to a memory location that doesn't appear in (outputs ∪ trashes). + + | byte lives + | define main routine + | trashes lives, a, z, n + | { + | copy 0, lives + | } + = ok + + | byte lives + | define main routine + | outputs lives + | trashes a, z, n + | { + | copy 0, lives + | } + = ok + + | byte lives + | define main routine + | inputs lives + | trashes a, z, n + | { + | copy 0, lives + | } + ? ForbiddenWriteError: lives + +a, z, and n are trashed, and must be declared as such. + +(Note, both n and z are forbidden writes in this test.) + + | byte lives + | define main routine + | outputs lives + | { + | copy 0, lives + | } + ? ForbiddenWriteError + +a, z, and n are trashed, and must not be declared as outputs. + +(Note, both n and a are unmeaningful outputs in this test.) + + | byte lives + | define main routine + | outputs lives, a, z, n + | { + | copy 0, lives + | } + ? UnmeaningfulOutputError + +Unless of course you subsequently initialize them. + + | byte lives + | define main routine + | outputs lives, a, z, n + | { + | copy 0, lives + | ld a, 0 + | } + = ok + +Can `copy` from a `byte` to a `byte`. + + | byte source : 0 + | byte dest + | + | define main routine + | inputs source + | outputs dest + | trashes a, z, n + | { + | copy source, dest + | } + = ok + +The understanding is that, because `copy` trashes `a`, `a` cannot be used +as the destination of a `copy`. + + | byte source : 0 + | byte dest + | + | define main routine + | inputs source + | outputs dest + | trashes a, z, n + | { + | copy source, a + | } + ? ForbiddenWriteError + +Can `copy` from a `word` to a `word`. + + | word source : 0 + | word dest + | + | define main routine + | inputs source + | outputs dest + | trashes a, z, n + | { + | copy source, dest + | } + = ok + +Can't `copy` from a `byte` to a `word`. + + | byte source : 0 + | word dest + | + | define main routine + | inputs source + | outputs dest + | trashes a, z, n + | { + | copy source, dest + | } + ? TypeMismatchError + +Can't `copy` from a `word` to a `byte`. + + | word source : 0 + | byte dest + | + | define main routine + | inputs source + | outputs dest + | trashes a, z, n + | { + | copy source, dest + | } + ? TypeMismatchError + +### point ... into blocks ### + +Pointer must be a pointer type. + + | byte table[256] tab + | word ptr + | + | define main routine + | inputs tab + | outputs y, tab + | trashes a, z, n, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | copy 123, [ptr] + y + | } + | } + ? TypeMismatchError + +Cannot write through pointer outside a `point ... into` block. + + | byte table[256] tab + | pointer ptr + | + | define main routine + | inputs tab, ptr + | outputs y, tab + | trashes a, z, n, ptr + | { + | ld y, 0 + | copy 123, [ptr] + y + | } + ? ForbiddenWriteError + +After a `point ... into` block, the pointer is no longer meaningful. + + | byte table[256] tab + | pointer ptr + | + | define main routine + | inputs tab + | outputs y, tab + | trashes a, z, n, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | copy 123, [ptr] + y + | } + | copy 123, [ptr] + y + | } + ? UnmeaningfulReadError: ptr + +Write literal through a pointer into a table. + + | byte table[256] tab + | pointer ptr + | + | define main routine + | inputs tab + | outputs y, tab + | trashes a, z, n, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | copy 123, [ptr] + y + | } + | } + = ok + +Writing into a table via a pointer does use `y`. + + | byte table[256] tab + | pointer ptr + | + | define main routine + | inputs tab + | outputs tab + | trashes a, z, n, ptr + | { + | point ptr into tab { + | reset ptr 0 + | copy 123, [ptr] + y + | } + | } + ? UnmeaningfulReadError + +Write stored value through a pointer into a table. + + | byte table[256] tab + | pointer ptr + | byte foo + | + | define main routine + | inputs foo, tab + | outputs y, tab + | trashes a, z, n, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | copy foo, [ptr] + y + | } + | } + = ok + +Read a table entry via a pointer. + + | byte table[256] tab + | pointer ptr + | byte foo + | + | define main routine + | inputs tab + | outputs foo + | trashes a, y, z, n, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | copy [ptr] + y, foo + | } + | } + = ok + +Read and write through two pointers into a table. + + | byte table[256] tab + | pointer ptra + | pointer ptrb + | + | define main routine + | inputs tab + | outputs tab + | trashes a, y, z, n, ptra, ptrb + | { + | ld y, 0 + | point ptra into tab { + | reset ptra 0 + | point ptrb into tab { + | reset ptrb 0 + | copy [ptra] + y, [ptrb] + y + | } + | } + | } + = ok + +Read through a pointer into a table, to the `a` register. Note that this is done with `ld`, +not `copy`. + + | byte table[256] tab + | pointer ptr + | byte foo + | + | define main routine + | inputs tab + | outputs a + | trashes y, z, n, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | ld a, [ptr] + y + | } + | } + = ok + +Write the `a` register through a pointer into a table. Note that this is done with `st`, +not `copy`. + + | byte table[256] tab + | pointer ptr + | byte foo + | + | define main routine + | inputs tab + | outputs tab + | trashes a, y, z, n, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | ld a, 255 + | st a, [ptr] + y + | } + | } + = ok + +Cannot get a pointer into a non-byte (for instance, word) table. + + | word table[256] tab + | pointer ptr + | byte foo + | + | define main routine + | inputs tab + | outputs foo + | trashes a, y, z, n, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | copy [ptr] + y, foo + | } + | } + ? TypeMismatchError + +Cannot get a pointer into a non-byte (for instance, vector) table. + + | vector (routine trashes a, z, n) table[256] tab + | pointer ptr + | vector (routine trashes a, z, n) foo + | + | define main routine + | inputs tab + | outputs foo + | trashes a, y, z, n, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | copy [ptr] + y, foo + | } + | } + ? TypeMismatchError + +`point into` by itself only requires `ptr` to be writeable. By itself, +it does not require `tab` to be readable or writeable. + + | byte table[256] tab + | pointer ptr + | + | define main routine + | trashes a, z, n, ptr + | { + | point ptr into tab { + | ld a, 0 + | } + | } + = ok + + | byte table[256] tab + | pointer ptr + | + | define main routine + | trashes a, z, n + | { + | point ptr into tab { + | ld a, 0 + | } + | } + = ok + +It does need to be accessible by the routine if you're going to reset the pointer +to it though. + + | byte table[256] tab + | pointer ptr + | + | define main routine + | trashes a, z, n + | { + | point ptr into tab { + | reset ptr 0 + | ld a, 0 + | } + | } + ? UnmeaningfulReadError: tab + +And the pointer needs to be writeable if it's going to be reset as well. + + | byte table[256] tab + | pointer ptr + | + | define main routine + | inputs tab + | trashes a, z, n + | { + | point ptr into tab { + | reset ptr 0 + | ld a, 0 + | } + | } + ? ForbiddenWriteError + +After a `point into` block, the pointer is no longer meaningful and cannot +be considered an output of the routine. + + | byte table[256] tab + | pointer ptr + | + | define main routine + | inputs tab + | outputs y, tab, ptr + | trashes a, z, n + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | copy 123, [ptr] + y + | } + | } + ? UnmeaningfulOutputError + +If code in a routine reads from a table through a pointer, the table must be in +the `inputs` of that routine. + + | byte table[256] tab + | pointer ptr + | byte foo + | + | define main routine + | outputs foo + | trashes a, y, z, n, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | copy [ptr] + y, foo + | } + | } + ? UnmeaningfulReadError + +Likewise, if code in a routine writes into a table via a pointer, the table must +be in the `outputs` of that routine. + + | byte table[256] tab + | pointer ptr + | + | define main routine + | inputs tab + | trashes a, y, z, n, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | copy 123, [ptr] + y + | } + | } + ? ForbiddenWriteError + +If code in a routine reads from a table through a pointer, the pointer *should* +remain inside the range of the table. This is currently not checked. + + | byte table[32] tab + | pointer ptr + | byte foo + | + | define main routine + | inputs tab + | outputs foo + | trashes a, y, c, z, n, v, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | st off, c + | add ptr, word 100 + | copy [ptr] + y, foo + | } + | } + = ok + +Likewise, if code in a routine writes into a table through a pointer, the pointer +*should* remain inside the range of the table. This is currently not checked. + + | byte table[32] tab + | pointer ptr + | + | define main routine + | inputs tab + | outputs tab + | trashes a, y, c, z, n, v, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | st off, c + | add ptr, word 100 + | copy 123, [ptr] + y + | } + | } + = ok + +### reset ### + +Can't have a `reset` outside a `point ... into` block. + + | byte table[32] tab + | pointer ptr + | + | define main routine + | inputs tab + | outputs tab + | trashes a, y, c, z, n, v, ptr + | { + | ld y, 0 + | reset ptr 0 + | point ptr into tab { + | st off, c + | add ptr, word 10 + | copy 123, [ptr] + y + | } + | } + ? ForbiddenWriteError: ptr + +Can't write into a table if the pointer hasn't been `reset`; the +pointer is not meaningful until it's `reset`. + + | byte table[32] tab + | pointer ptr + | + | define main routine + | inputs tab + | outputs tab + | trashes a, y, c, z, n, v, ptr + | { + | ld y, 0 + | point ptr into tab { + | st off, c + | copy 123, [ptr] + y + | } + | } + ? UnmeaningfulReadError: ptr + +Can't read from a table if the pointer hasn't been reset. + + | byte table[32] tab + | pointer ptr + | byte ou + | + | define main routine + | inputs tab + | outputs tab, ou + | trashes a, y, c, z, n, v, ptr + | { + | ld y, 0 + | point ptr into tab { + | st off, c + | copy [ptr] + y, ou + | } + | } + ? UnmeaningfulReadError: ptr + +Multiple `reset`s may occur inside the same block. + + | byte table[32] tab + | pointer ptr + | + | define main routine + | inputs tab + | outputs tab + | trashes a, y, c, z, n, v, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 10 + | copy 123, [ptr] + y + | reset ptr 20 + | copy 35, [ptr] + y + | } + | } + = ok + +The offset in `reset` may not exceed the table's size. + + | byte table[32] tab + | pointer ptr + | + | define main routine + | inputs tab + | outputs tab + | trashes a, y, c, z, n, v, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 32 + | copy 123, [ptr] + y + | } + | } + ? RangeExceededError + +### dynamic recurrence of `point ... into` blocks + +You cannot call a routine which trashes the pointer from inside a +`point ... into` block. Remember that `point ... into` by +itself doesn't change anything, so doesn't trash anything. + + | byte table[256] tab + | byte table[256] other + | pointer ptr + | + | define sub routine + | inputs other + | outputs other + | { + | point ptr into other { + | } + | } + | + | define main routine + | inputs tab, other + | outputs tab, other + | trashes a, y, c, z, n, v, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | copy 123, [ptr] + y + | call sub + | copy 123, [ptr] + y + | } + | } + = ok + + | byte table[256] tab + | byte table[256] other + | pointer ptr + | + | define sub routine + | inputs other + | outputs other + | trashes ptr + | { + | point ptr into other { + | reset ptr 0 + | } + | } + | + | define main routine + | inputs tab, other + | outputs tab, other + | trashes a, y, c, z, n, v, ptr + | { + | ld y, 0 + | point ptr into tab { + | reset ptr 0 + | copy 123, [ptr] + y + | call sub + | copy 123, [ptr] + y + | } + | } + ? UnmeaningfulReadError + +### locals ### + +When memory locations are defined static to a routine, they cannot be +directly input, nor directly output; and since they are always initialized, +they cannot be trashed. Thus, they really don't participate in the analysis. + + | define foo routine + | inputs x + | outputs x + | trashes z, n + | static byte t : 0 + | { + | st x, t + | inc t + | ld x, t + | } + | + | define main routine + | trashes a, x, z, n + | static byte t : 0 + | { + | ld x, t + | call foo + | } + = ok + +When memory locations are defined local to a routine, but not static, +they cannot be directly input, nor directly output; but they are considered +uninitialized from the time the routine is called to until a value is stored +in them. + + | define main routine + | inputs x + | outputs x + | trashes z, n + | local byte t + | { + | st x, t + | inc t + | ld x, t + | } + = ok + + | define main routine + | outputs x + | trashes z, n + | local byte t + | { + | inc t + | ld x, t + | } + ? UnmeaningfulReadError: t + +Local non-statics can be meaningully given an explicit address. + + | define main routine + | inputs x + | outputs x + | trashes z, n + | local byte t @ 1024 + | { + | st x, t + | inc t + | ld x, t + | } + = ok + + | define main routine + | outputs x + | trashes z, n + | local byte t @ 1024 + | { + | inc t + | ld x, t + | } + ? UnmeaningfulReadError: t + +### save ### + +Basic neutral test, where the `save` makes no difference. + + | define main routine + | inputs a, x + | outputs a, x + | trashes z, n + | { + | ld a, 1 + | save x { + | ld a, 2 + | } + | ld a, 3 + | } + = ok + +Saving any location (other than `a`) will trash `a`. + + | define main routine + | inputs a, x + | outputs a, x + | trashes z, n + | { + | ld a, 1 + | save x { + | ld a, 2 + | } + | } + ? UnmeaningfulOutputError + +Saving `a` does not trash anything. + + | define main routine + | inputs a, x + | outputs a, x + | trashes z, n + | { + | ld x, 1 + | save a { + | ld x, 2 + | } + | ld x, 3 + | } + = ok + +A defined value that has been saved can be trashed inside the block. +It will continue to be defined outside the block. + + | define main routine + | outputs x, y + | trashes a, z, n + | { + | ld x, 0 + | save x { + | ld y, 0 + | trash x + | } + | } + = ok + +A trashed value that has been saved can be used inside the block. +It will continue to be trashed outside the block. + +(Note, both x and a are unmeaningful in this test.) + + | define main routine + | inputs a + | outputs a, x + | trashes z, n + | { + | ld x, 0 + | trash x + | save x { + | ld a, 0 + | ld x, 1 + | } + | } + ? UnmeaningfulOutputError + +The known range of a value will be preserved outside the block as well. + + | word one: 77 + | word table[32] many + | + | define main routine + | inputs a, many, one + | outputs many, one + | trashes a, x, n, z + | { + | and a, 31 + | ld x, a + | save x { + | ld x, 255 + | } + | copy one, many + x + | copy many + x, one + | } + = ok + + | word one: 77 + | word table[32] many + | + | define main routine + | inputs a, many, one + | outputs many, one + | trashes a, x, n, z + | { + | and a, 63 + | ld x, a + | save x { + | ld x, 1 + | } + | copy one, many + x + | copy many + x, one + | } + ? RangeExceededError + +The known properties of a value are preserved inside the block, too. + + | word one: 77 + | word table[32] many + | + | define main routine + | inputs a, many, one + | outputs many, one + | trashes a, x, n, z + | { + | and a, 31 + | ld x, a + | save x { + | copy one, many + x + | copy many + x, one + | } + | copy one, many + x + | copy many + x, one + | } + = ok + +A value which is not output from the routine, is preserved by the +routine; and can appear in a `save` exactly because a `save` preserves it. + + | define main routine + | outputs y + | trashes a, z, n + | { + | save x { + | ld y, 0 + | ld x, 1 + | } + | } + = ok + +Because saving anything except `a` trashes `a`, a common idiom is to save `a` +first in a nested series of `save`s. + + | define main routine + | inputs a + | outputs a + | trashes z, n + | { + | save a { + | save x { + | ld a, 0 + | ld x, 1 + | } + | } + | } + = ok + +There is a shortcut syntax for a nested series of `save`s. + + | define main routine + | inputs a + | outputs a + | trashes z, n + | { + | save a, x { + | ld a, 0 + | ld x, 1 + | } + | } + = ok + +`a` is only preserved if it is the outermost thing `save`d. + + | define main routine + | inputs a + | outputs a + | trashes z, n + | { + | save x, a { + | ld a, 0 + | ld x, 1 + | } + | } + ? UnmeaningfulOutputError: a + +Not just registers, but also user-defined locations can be saved. + + | byte foo + | + | define main routine + | trashes a, z, n + | { + | save foo { + | st 5, foo + | } + | } + = ok + +But only if they are bytes. + + | word foo + | + | define main routine + | trashes a, z, n + | { + | save foo { + | copy 555, foo + | } + | } + ? TypeMismatchError + + | byte table[16] tab + | + | define main routine + | trashes a, y, z, n + | { + | save tab { + | ld y, 0 + | st 5, tab + y + | } + | } + ? TypeMismatchError + +A `goto` cannot appear within a `save` block. + + | define other routine + | trashes a, z, n + | { + | ld a, 0 + | } + | + | define main routine + | trashes a, z, n + | { + | ld a, 1 + | save x { + | ld x, 2 + | goto other + | } + | } + ? IllegalJumpError diff --git a/tests/SixtyPical Syntax.md b/tests/SixtyPical Syntax.md index ea02c99..f830394 100644 --- a/tests/SixtyPical Syntax.md +++ b/tests/SixtyPical Syntax.md @@ -9,9 +9,6 @@ but not necessarily sensible programs. [Falderal]: http://catseye.tc/node/Falderal - -> Functionality "Check syntax of SixtyPical program" is implemented by - -> shell command "bin/sixtypical --parse-only --traceback %(test-body-file) && echo ok" - -> Tests for functionality "Check syntax of SixtyPical program" Rudimentary program. @@ -176,6 +173,7 @@ Other blocks. | ld a, 0 | } | point ptr into tab { + | reset ptr 0 | ld a, [ptr] + y | } | } @@ -681,6 +679,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 @@ -746,13 +745,14 @@ Only routines can be defined in the new style. | } ? SyntaxError -Memory locations can be defined static to a routine. +Memory locations can be defined local to a routine. | define foo routine | inputs x | outputs x | trashes z, n | static byte t : 0 + | local word w | { | st x, t | inc t @@ -762,13 +762,14 @@ Memory locations can be defined static to a routine. | define main routine | trashes a, x, z, n | static byte t : 0 + | local word w | { | ld x, t | call foo | } = ok -Static memory locations must always be given an initial value. +Local static memory locations must always be given an initial value. | define main routine | inputs x @@ -782,7 +783,49 @@ Static memory locations must always be given an initial value. | } ? SyntaxError -Name of a static cannot shadow an existing global or static. +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 + | inputs x + | outputs x + | trashes z, n + | local byte t : 10 + | { + | st x, t + | inc t + | ld x, t + | } + ? 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 | @@ -790,7 +833,7 @@ Name of a static cannot shadow an existing global or static. | inputs x | outputs x | trashes z, n - | static byte t + | static byte t : 10 | { | st x, t | inc t @@ -802,11 +845,89 @@ Name of a static cannot shadow an existing global or static. | inputs x | outputs x | trashes z, n - | static byte t - | static byte t + | static byte t : 10 + | static byte t : 20 | { | st x, t | inc t | ld x, t | } ? SyntaxError + + | byte t + | + | define main routine + | inputs x + | outputs x + | trashes z, n + | local byte t + | { + | st x, t + | inc t + | ld x, t + | } + ? SyntaxError + + | define main routine + | inputs x + | outputs x + | trashes z, n + | local word w + | local word w + | { + | st x, t + | inc t + | ld x, t + | } + ? SyntaxError + +Since the names of locals are lexically local to a routine, they cannot +appear in the inputs, outputs, trashes list of the routine. + + | define main routine + | inputs t + | static byte t : 0 + | { + | inc t + | } + ? SyntaxError + + | define main routine + | outputs t + | static byte t : 0 + | { + | inc t + | } + ? SyntaxError + + | define main routine + | trashes t + | static byte t : 0 + | { + | inc t + | } + ? SyntaxError + + | define main routine + | inputs t + | local byte t + | { + | inc t + | } + ? SyntaxError + + | define main routine + | outputs t + | local byte t + | { + | inc t + | } + ? SyntaxError + + | define main routine + | trashes t + | local byte t + | { + | inc t + | } + ? SyntaxError diff --git a/tests/appliances/sixtypical.md b/tests/appliances/sixtypical.md new file mode 100644 index 0000000..1ee2bc4 --- /dev/null +++ b/tests/appliances/sixtypical.md @@ -0,0 +1,20 @@ +This file contains only the [Falderal][] directives that define the different +functionalities tested by the test suite, assuming that it's the reference +implementation, `sixtypical`, that is going to implement these functionalities. + +[Falderal]: http://catseye.tc/node/Falderal + + -> Functionality "Check syntax of SixtyPical program" is implemented by + -> shell command "bin/sixtypical --parse-only --traceback %(test-body-file) && echo ok" + + -> Functionality "Analyze SixtyPical program" is implemented by + -> shell command "bin/sixtypical --analyze-only --traceback %(test-body-file) && echo ok" + + -> Functionality "Compile SixtyPical program" is implemented by + -> shell command "bin/sixtypical --output-format=c64-basic-prg --traceback %(test-body-file) --output /tmp/foo && tests/appliances/bin/dcc6502-adapter Functionality "Dump fallthru info for SixtyPical program" is implemented by + -> shell command "bin/sixtypical --optimize-fallthru --dump-fallthru-info --analyze-only --traceback %(test-body-file)" + + -> Functionality "Compile SixtyPical program with fallthru optimization" is implemented by + -> shell command "bin/sixtypical --output-format=c64-basic-prg --optimize-fallthru --traceback %(test-body-file) --output /tmp/foo && tests/appliances/bin/dcc6502-adapter