From ab3333192401cb4792e5f6e07034447ce5690fcd Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 14 Feb 2018 13:31:03 +0000 Subject: [PATCH 01/35] Associate a range with each meaningful storage location in context. --- src/sixtypical/analyzer.py | 36 +++++++++++++++++++++--------------- src/sixtypical/model.py | 18 ++++++++++++++---- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index ecc88e4..1c9e51d 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -66,11 +66,17 @@ class Context(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. - + 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. """ @@ -78,13 +84,13 @@ class Context(object): self.routines = routines # Location -> AST node self.routine = routine self._touched = set() - self._meaningful = set() + self._range = dict() self._writeable = set() for ref in inputs: if ref.is_constant(): raise ConstantConstraintError('%s in %s' % (ref.name, routine.name)) - self._meaningful.add(ref) + self._range[ref] = ref.max_range() output_names = set() for ref in outputs: if ref.is_constant(): @@ -99,19 +105,19 @@ class Context(object): self._writeable.add(ref) def __str__(self): - return "Context(\n _touched={},\n _meaningful={},\n _writeable={}\n)".format( - LocationRef.format_set(self._touched), LocationRef.format_set(self._meaningful), LocationRef.format_set(self._writeable) + return "Context(\n _touched={},\n _range={},\n _writeable={}\n)".format( + LocationRef.format_set(self._touched), LocationRef.format_set(self._range), LocationRef.format_set(self._writeable) ) def clone(self): c = Context(self.routines, self.routine, [], [], []) c._touched = set(self._touched) - c._meaningful = set(self._meaningful) + c._range = dict(self._range) c._writeable = set(self._writeable) return c def each_meaningful(self): - for ref in self._meaningful: + for ref in self._range.keys(): yield ref def each_touched(self): @@ -127,7 +133,7 @@ class Context(object): if ref.is_constant() or ref in self.routines: pass elif isinstance(ref, LocationRef): - if ref not in self._meaningful: + if ref not in self._range: message = '%s in %s' % (ref.name, self.routine.name) if kwargs.get('message'): message += ' (%s)' % kwargs['message'] @@ -156,12 +162,12 @@ class Context(object): def set_meaningful(self, *refs): for ref in refs: - self._meaningful.add(ref) + self._range[ref] = ref.max_range() def set_unmeaningful(self, *refs): for ref in refs: - if ref in self._meaningful: - self._meaningful.remove(ref) + if ref in self._range: + del self._range[ref] def set_written(self, *refs): """A "helper" method which does the following common sequence for @@ -223,7 +229,7 @@ class Analyzer(object): print context self.analyze_block(routine.block, context) - trashed = set(context.each_touched()) - context._meaningful + trashed = set(context.each_touched()) - set(context.each_meaningful()) if self.debug: print "at end of routine `{}`:".format(routine.name) @@ -397,7 +403,7 @@ class Analyzer(object): # merge the contexts. this used to be a method called `set_from` context._touched = set(context1._touched) | set(context2._touched) - context._meaningful = outgoing_meaningful + context.set_meaningful(*list(outgoing_meaningful)) context._writeable = set(context1._writeable) | set(context2._writeable) for ref in outgoing_trashes: diff --git a/src/sixtypical/model.py b/src/sixtypical/model.py index dde7da9..28e5dfe 100644 --- a/src/sixtypical/model.py +++ b/src/sixtypical/model.py @@ -2,8 +2,9 @@ class Type(object): - def __init__(self, name): + def __init__(self, name, max_range=None): self.name = name + self.max_range = max_range def __repr__(self): return 'Type(%r)' % self.name @@ -32,9 +33,9 @@ class Type(object): self.trashes = set([resolve(w) for w in self.trashes]) -TYPE_BIT = Type('bit') -TYPE_BYTE = Type('byte') -TYPE_WORD = Type('word') +TYPE_BIT = Type('bit', max_range=(0, 1)) +TYPE_BYTE = Type('byte', max_range=(0, 255)) +TYPE_WORD = Type('word', max_range=(0, 65535)) @@ -132,6 +133,9 @@ class Ref(object): will not change during the lifetime of the program.""" raise NotImplementedError + def max_range(self): + raise NotImplementedError + class LocationRef(Ref): def __init__(self, type, name): @@ -160,6 +164,12 @@ class LocationRef(Ref): def is_constant(self): return isinstance(self.type, RoutineType) + def max_range(self): + try: + return self.type.max_range + except: + return (0, 0) + @classmethod def format_set(cls, location_refs): return '{%s}' % ', '.join([str(loc) for loc in sorted(location_refs)]) From aa5b06980cb40dbc04d03f9e1d0363055e562c62 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 14 Feb 2018 13:41:48 +0000 Subject: [PATCH 02/35] Add some initial failing tests. --- tests/SixtyPical Analysis.md | 74 +++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index 05c556f..5453829 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -517,8 +517,9 @@ Copying to and from a word table. ? TypeMismatchError You can also copy a literal word to a word table. +(Even if the table has fewer than 256 entries.) - | word table[256] many + | word table[32] many | | routine main | inputs many @@ -530,6 +531,77 @@ You can also copy a literal word to a word table. | } = ok +#### tables: range checking #### + +A table may not have more than 256 entries. + + | word table[512] many + | + | routine main + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | copy 9999, many + x + | } + ? zzzzz + +The number of entries in a table must be a power of two. + + | word table[48] many + | + | routine main + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | copy 9999, many + x + | } + ? zzzz + +If a table has fewer than 256 entries, it cannot be read or written +beyond the maximum number of entries it has. + + | byte table[32] many + | + | routine main + | 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 + | + | routine main + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 32 + | ld a, many + x + | } + ? RangeError + + | byte table[32] many + | + | routine main + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 32 + | ld a, 0 + | st a, many + x + | } + ? RangeError + ### add ### Can't `add` from or to a memory location that isn't initialized. From 93e2dada16a9d1ffdecf9a4c36b319af943adf0c Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 14 Feb 2018 13:53:22 +0000 Subject: [PATCH 03/35] Check for table size (in parser, thus tests are for syntax.) --- src/sixtypical/parser.py | 2 ++ tests/SixtyPical Analysis.md | 28 -------------------------- tests/SixtyPical Syntax.md | 39 ++++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index d7f5cd4..cf3b5a6 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -146,6 +146,8 @@ class Parser(object): if self.scanner.consume('table'): size = self.defn_size() + if size not in (1, 2, 4, 8, 16, 32, 64, 129, 256): + raise SyntaxError("Table size must be a power of two, 0 < size <= 256") type_ = TableType(type_, size) return type_ diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index 5453829..c0560f5 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -533,34 +533,6 @@ You can also copy a literal word to a word table. #### tables: range checking #### -A table may not have more than 256 entries. - - | word table[512] many - | - | routine main - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | copy 9999, many + x - | } - ? zzzzz - -The number of entries in a table must be a power of two. - - | word table[48] many - | - | routine main - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | copy 9999, many + x - | } - ? zzzz - If a table has fewer than 256 entries, it cannot be read or written beyond the maximum number of entries it has. diff --git a/tests/SixtyPical Syntax.md b/tests/SixtyPical Syntax.md index 1a3b5b4..e9862ec 100644 --- a/tests/SixtyPical Syntax.md +++ b/tests/SixtyPical Syntax.md @@ -153,6 +153,45 @@ Tables of different types. | } = ok +The number of entries in a table must be a power of two +which is greater than 0 and less than or equal to 256. + + | word table[512] many + | + | routine main + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | copy 9999, many + x + | } + ? SyntaxError + + | word table[48] many + | + | routine main + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | copy 9999, many + x + | } + ? SyntaxError + + | word table[0] many + | + | routine main + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | copy 9999, many + x + | } + ? SyntaxError + Typedefs of different types. | typedef byte octet From 2897a04718cc4dee71c2480d95535dd9aa4fe40a Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 14 Feb 2018 15:13:35 +0000 Subject: [PATCH 04/35] Get and set top of range of a location. Make AND do this to A reg. --- src/sixtypical/analyzer.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 1c9e51d..4de8fc1 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -164,6 +164,18 @@ class Context(object): for ref in refs: self._range[ref] = ref.max_range() + def set_top_of_range(self, ref, top): + self.assert_meaningful(ref) + (bottom, _) = self._range[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 set_unmeaningful(self, *refs): for ref in refs: if ref in self._range: @@ -351,7 +363,15 @@ class Analyzer(object): self.assert_type(TYPE_BYTE, src, dest) context.assert_meaningful(src, dest) context.set_written(FLAG_Z, FLAG_N, FLAG_C) - elif opcode in ('and', 'or', 'xor'): + elif opcode == 'and': + self.assert_type(TYPE_BYTE, src, dest) + context.assert_meaningful(src, dest) + context.set_written(dest, FLAG_Z, FLAG_N) + # If you AND the A register with a value V, the resulting value of A + # cannot exceed the value of V; i.e. the maximum value of A becomes + # the maximum value of V. + context.set_top_of_range(dest, context.get_top_of_range(src)) + elif opcode in ('or', 'xor'): self.assert_type(TYPE_BYTE, src, dest) context.assert_meaningful(src, dest) context.set_written(dest, FLAG_Z, FLAG_N) From 0687da80d5d16a0504694e6dee696a02bd674365 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 14 Feb 2018 15:54:57 +0000 Subject: [PATCH 05/35] Some headway on context.assert_in_range(). --- src/sixtypical/analyzer.py | 28 ++++++++++++++++++++++++++++ tests/SixtyPical Analysis.md | 4 ++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 4de8fc1..3781195 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -37,7 +37,12 @@ class IllegalJumpError(StaticAnalysisError): pass +class RangeExceededError(StaticAnalysisError): + pass + + class ConstraintsError(StaticAnalysisError): + """The constraints of a routine (inputs, outputs, trashes) have been violated.""" pass @@ -156,6 +161,27 @@ class Context(object): message += ' (%s)' % kwargs['message'] raise exception_class(message) + def assert_in_range(self, inside, outside): + # FIXME there's a bit of I'm-not-sure-the-best-way-to-do-this-ness, here... + + # 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 = outside.max_range() + if isinstance(outside.type, TableType): + outside_range = (0, outside.type.size-1) + + if inside_range[0] < outside_range[0] or inside_range[1] > outside_range[1]: + raise RangeExceededError( + "Possible range of {} {} exceeds acceptable range of {} {}".format( + inside, inside_range, outside, outside_range + ) + ) + def set_touched(self, *refs): for ref in refs: self._touched.add(ref) @@ -291,6 +317,7 @@ class Analyzer(object): (src.ref.name, dest.name, self.current_routine.name) ) context.assert_meaningful(src, src.index) + context.assert_in_range(src.index, src.ref) elif isinstance(src, IndirectRef): # copying this analysis from the matching branch in `copy`, below if isinstance(src.ref.type, PointerType) and dest.type == TYPE_BYTE: @@ -312,6 +339,7 @@ class Analyzer(object): else: raise TypeMismatchError((src, dest)) context.assert_meaningful(dest.index) + context.assert_in_range(dest.index, dest.ref) context.set_written(dest.ref) elif isinstance(dest, IndirectRef): # copying this analysis from the matching branch in `copy`, below diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index c0560f5..d66822c 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -559,7 +559,7 @@ beyond the maximum number of entries it has. | ld x, 32 | ld a, many + x | } - ? RangeError + ? RangeExceededError | byte table[32] many | @@ -572,7 +572,7 @@ beyond the maximum number of entries it has. | ld a, 0 | st a, many + x | } - ? RangeError + ? RangeExceededError ### add ### From 0e0a3d552a2519ea5eb13915d3ffa02614d75957 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 14 Feb 2018 16:35:19 +0000 Subject: [PATCH 06/35] Implement Context.copy_range(), ranges for constants. Progress! --- src/sixtypical/analyzer.py | 12 +++++++++++- src/sixtypical/model.py | 7 +++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 3781195..0cd8977 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -188,7 +188,8 @@ class Context(object): def set_meaningful(self, *refs): for ref in refs: - self._range[ref] = ref.max_range() + if ref not in self._range: + self._range[ref] = ref.max_range() def set_top_of_range(self, ref, top): self.assert_meaningful(ref) @@ -202,6 +203,14 @@ class Context(object): (_, top) = self._range[ref] return top + def copy_range(self, src, dest): + self.assert_meaningful(src) + if src in self._range: + src_range = self._range[src] + else: + src_range = src.max_range() + self._range[dest] = src_range + def set_unmeaningful(self, *refs): for ref in refs: if ref in self._range: @@ -331,6 +340,7 @@ class Analyzer(object): ) else: context.assert_meaningful(src) + context.copy_range(src, dest) context.set_written(dest, FLAG_Z, FLAG_N) elif opcode == 'st': if isinstance(dest, IndexedRef): diff --git a/src/sixtypical/model.py b/src/sixtypical/model.py index 28e5dfe..75f7a7f 100644 --- a/src/sixtypical/model.py +++ b/src/sixtypical/model.py @@ -131,10 +131,10 @@ class Ref(object): """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.""" - raise NotImplementedError + raise NotImplementedError("class {} must implement is_constant()".format(self.__class__.__name__)) def max_range(self): - raise NotImplementedError + raise NotImplementedError("class {} must implement max_range()".format(self.__class__.__name__)) class LocationRef(Ref): @@ -287,6 +287,9 @@ class ConstantRef(Ref): def is_constant(self): return True + def max_range(self): + return (self.value, self.value) + def high_byte(self): return (self.value >> 8) & 255 From e2d10d7d33dfb1e42808978a06089572efb99677 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Fri, 16 Feb 2018 09:51:24 +0000 Subject: [PATCH 07/35] Documentation/notes changes for the 0.13 branch. --- HISTORY.md | 8 ++++++++ README.md | 14 ++------------ doc/SixtyPical.md | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 62a3214..a9f9b14 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,14 @@ History of SixtyPical ===================== +0.13 +---- + +* 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. +* The reference analyzer's ability to prove this is currently fairly weak, + but it does exist. + 0.12 ---- diff --git a/README.md b/README.md index 1509c44..583158c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ SixtyPical ========== -_Version 0.12. Work-in-progress, everything is subject to change._ +_Version 0.13. Work-in-progress, everything is subject to change._ SixtyPical is a very low-level programming language, similar to 6502 assembly, with static analysis through abstract interpretation. @@ -44,17 +44,6 @@ TODO This preserves them, so that, semantically, they can be used later even though they are trashed inside the block. -### Range checking in the abstract interpretation - -If you copy the address of a buffer (say it is size N) to a pointer, it is valid. -If you add a value from 0 to N-1 to the pointer, it is still valid. -But if you add a value ≥ N to it, it becomes invalid. -This should be tracked in the abstract interpretation. -(If only because abstract interpretation is the major point of this project!) - -Range-checking buffers might be too difficult. Range checking tables will be easier. -If a value is ANDed with 15, its range must be 0-15, etc. - ### Re-order routines and optimize tail-calls to fallthroughs Not because it saves 3 bytes, but because it's a neat trick. Doing it optimally @@ -62,6 +51,7 @@ is probably NP-complete. But doing it adeuqately is probably not that hard. ### And at some point... +* Confirm that `and` can be used to restrict the range of table reads/writes. * `low` and `high` address operators - to turn `word` type into `byte`. * `const`s that can be used in defining the size of tables, etc. * Tests, and implementation, ensuring a routine can be assigned to a vector of "wider" type diff --git a/doc/SixtyPical.md b/doc/SixtyPical.md index 79f4782..3d9570e 100644 --- a/doc/SixtyPical.md +++ b/doc/SixtyPical.md @@ -27,7 +27,7 @@ There are also three *type constructors*: * T table[N] (N is a power of 2, 1 ≤ N ≤ 256; each entry holds a value of type T, where T is `byte`, `word`, or `vector`) -* buffer[N] (N entries; each entry is a byte; N is a power of 2, ≤ 64K) +* buffer[N] (N entries; each entry is a byte; 1 ≤ N ≤ 65536) * vector T (address of a value of type T; T must be a routine type) ### User-defined ### From 15fa4889e7371e80a2589134345c461dbc5584d8 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Fri, 16 Feb 2018 12:28:11 +0000 Subject: [PATCH 08/35] Add some more tests (failing currently). --- tests/SixtyPical Analysis.md | 53 ++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index d66822c..4cd21c6 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -533,8 +533,15 @@ You can also copy a literal word to a word table. #### tables: range checking #### -If a table has fewer than 256 entries, it cannot be read or written -beyond the maximum number of entries it has. +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, because a byte +cannot index any entry outside of 0..255.) + +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 | @@ -574,6 +581,48 @@ beyond the maximum number of entries it has. | } ? RangeExceededError +This applies to `copy` as well. + + | word one: 77 + | word table[32] many + | + | routine main + | 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 + | + | routine main + | 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 + | + | routine main + | inputs many, one + | outputs many, one + | trashes a, x, n, z + | { + | ld x, 32 + | copy one, many + x + | } + ? RangeExceededError + ### add ### Can't `add` from or to a memory location that isn't initialized. From 42c4bed36e95cdb95110dff71895832f45e9c1a9 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Fri, 16 Feb 2018 15:46:55 +0000 Subject: [PATCH 09/35] Make tests pass. --- src/sixtypical/analyzer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 0cd8977..396e116 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -514,6 +514,7 @@ class Analyzer(object): pass else: raise TypeMismatchError((src, dest)) + context.assert_in_range(dest.index, dest.ref) elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef): if TableType.is_a_table_type(src.ref.type, TYPE_WORD) and dest.type == TYPE_WORD: @@ -523,6 +524,7 @@ class Analyzer(object): pass else: raise TypeMismatchError((src, dest)) + context.assert_in_range(src.index, src.ref) elif isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, LocationRef): if src.type == dest.type: From a115c2bc9f82e72af6f242c30393c1f360c66fc8 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 5 Mar 2018 10:34:49 +0000 Subject: [PATCH 10/35] Add notes for `for`-like loop. --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index 583158c..fe63f7e 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,37 @@ Documentation TODO ---- +### `for`-like loop + +We have range-checking in the abstract analysis now, but we lack practical ways +to use it. + +We can `and` a value to ensure it is within a certain range. However, in the 6502 +ISA the only register you can `and` is `A`, while loops are done with `X` or `Y`. +Insisting this as the way to do it would result in a lot of `TXA`s and `TAX`s. + +What would be better is a dedicated `for` loop, like + + for x in 0 to 15 { + // in here, we know the range of x is exactly 0-15 inclusive + // also in here: we are disallowed from changing x + } + +However, this is slightly restrictive, and hides a lot. + +However however, options which do not hide a lot, require a lot of looking at +(to ensure: did you increment the loop variable? only once? etc.) + +The leading compromise so far is an "open-faced for loop", like + + ld x, 15 + for x downto 0 { + // same as above + } + +This makes it a little more explicit, at least, even though the loop +decrementation is still hidden. + ### Save registers on stack This preserves them, so that, semantically, they can be used later even though they From d24e9fa2e150d24685237e50b9021b2800af3e07 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 5 Mar 2018 10:38:20 +0000 Subject: [PATCH 11/35] I can no longer see a reason to require that it is a power of two. --- doc/SixtyPical.md | 2 +- src/sixtypical/parser.py | 2 +- tests/SixtyPical Syntax.md | 28 ++++++++++++++-------------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/doc/SixtyPical.md b/doc/SixtyPical.md index 3d9570e..7e29545 100644 --- a/doc/SixtyPical.md +++ b/doc/SixtyPical.md @@ -25,7 +25,7 @@ There are five *primitive types* in SixtyPical: There are also three *type constructors*: -* T table[N] (N is a power of 2, 1 ≤ N ≤ 256; each entry holds a value +* T table[N] (N entries, 1 ≤ N ≤ 256; each entry holds a value of type T, where T is `byte`, `word`, or `vector`) * buffer[N] (N entries; each entry is a byte; 1 ≤ N ≤ 65536) * vector T (address of a value of type T; T must be a routine type) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index cf3b5a6..320e53c 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -146,7 +146,7 @@ class Parser(object): if self.scanner.consume('table'): size = self.defn_size() - if size not in (1, 2, 4, 8, 16, 32, 64, 129, 256): + if size <= 0 or size > 256: raise SyntaxError("Table size must be a power of two, 0 < size <= 256") type_ = TableType(type_, size) diff --git a/tests/SixtyPical Syntax.md b/tests/SixtyPical Syntax.md index e9862ec..a434345 100644 --- a/tests/SixtyPical Syntax.md +++ b/tests/SixtyPical Syntax.md @@ -153,8 +153,8 @@ Tables of different types. | } = ok -The number of entries in a table must be a power of two -which is greater than 0 and less than or equal to 256. +The number of entries in a table must be +greater than 0 and less than or equal to 256. | word table[512] many | @@ -168,18 +168,6 @@ which is greater than 0 and less than or equal to 256. | } ? SyntaxError - | word table[48] many - | - | routine main - | inputs many - | outputs many - | trashes a, x, n, z - | { - | ld x, 0 - | copy 9999, many + x - | } - ? SyntaxError - | word table[0] many | | routine main @@ -192,6 +180,18 @@ which is greater than 0 and less than or equal to 256. | } ? SyntaxError + | word table[48] many + | + | routine main + | inputs many + | outputs many + | trashes a, x, n, z + | { + | ld x, 0 + | copy 9999, many + x + | } + = ok + Typedefs of different types. | typedef byte octet From b42aa8b05139a331c270f2bbe2cd9a833a6de31a Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 5 Mar 2018 11:28:08 +0000 Subject: [PATCH 12/35] Fix error message. --- src/sixtypical/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 320e53c..480953c 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -147,7 +147,7 @@ class Parser(object): if self.scanner.consume('table'): size = self.defn_size() if size <= 0 or size > 256: - raise SyntaxError("Table size must be a power of two, 0 < size <= 256") + raise SyntaxError("Table size must be > 0 and <= 256") type_ = TableType(type_, size) return type_ From 4b43ba734e7f8c462768b9fc54ef203fe8708e3d Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 5 Mar 2018 11:55:06 +0000 Subject: [PATCH 13/35] Make AST more structured (borrowed from ALPACA.) --- src/sixtypical/ast.py | 59 ++++++++++++++++++++++++++++++++++++---- src/sixtypical/parser.py | 24 ++++++++-------- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/sixtypical/ast.py b/src/sixtypical/ast.py index bbcc68a..771fc1d 100644 --- a/src/sixtypical/ast.py +++ b/src/sixtypical/ast.py @@ -1,8 +1,29 @@ # encoding: UTF-8 class AST(object): + children_attrs = () + child_attrs = () + value_attrs = () + def __init__(self, **kwargs): - self.attrs = kwargs + self.attrs = {} + for attr in self.children_attrs: + self.attrs[attr] = kwargs.pop(attr, []) + for child in self.attrs[attr]: + assert child is None or isinstance(child, AST), \ + "child %s=%r of %r is not an AST node" % (attr, child, self) + for attr in self.child_attrs: + self.attrs[attr] = kwargs.pop(attr, None) + child = self.attrs[attr] + assert child is None or isinstance(child, AST), \ + "child %s=%r of %r is not an AST node" % (attr, child, self) + for attr in self.value_attrs: + self.attrs[attr] = kwargs.pop(attr, None) + assert (not kwargs), "extra arguments supplied to {} node: {}".format(self.type, kwargs) + + @property + def type(self): + return self.__class__.__name__ def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self.attrs) @@ -12,22 +33,50 @@ class AST(object): return self.attrs[name] raise AttributeError(name) + def all_children(self): + for attr in self.children_attrs: + for child in self.attrs[attr]: + yield child + for subchild in child.all_children(): + yield subchild + for attr in self.child_attrs: + child = self.attrs[attr] + yield child + for subchild in child.all_children(): + yield subchild + class Program(AST): - pass + children_attrs = ('defns', 'routines',) class Defn(AST): - pass + value_attrs = ('name', 'addr', 'initial', 'location',) class Routine(AST): - pass + value_attrs = ('name', 'addr', 'initial', 'location',) + children_attrs = ('statics',) + child_attrs = ('block',) class Block(AST): - pass + children_attrs = ('instrs',) class Instr(AST): pass + + +class SingleOp(Instr): + value_attrs = ('opcode', 'dest', 'src', 'index', 'location',) + + +class BlockOp(Instr): + value_attrs = ('opcode', 'dest', 'src', 'inverted') + child_attrs = ('block',) + + +class IfOp(Instr): + value_attrs = ('opcode', 'dest', 'src', 'inverted') + child_attrs = ('block1', 'block2',) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 480953c..8fd1059 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -1,6 +1,6 @@ # encoding: UTF-8 -from sixtypical.ast import Program, Defn, Routine, Block, Instr +from sixtypical.ast import Program, Defn, Routine, Block, SingleOp, BlockOp, IfOp from sixtypical.model import ( TYPE_BIT, TYPE_BYTE, TYPE_WORD, RoutineType, VectorType, TableType, BufferType, PointerType, @@ -352,8 +352,8 @@ class Parser(object): block2 = None if self.scanner.consume('else'): block2 = self.block() - return Instr(opcode='if', dest=None, src=src, - block1=block1, block2=block2, inverted=inverted) + return IfOp(opcode='if', dest=None, src=src, + block1=block1, block2=block2, inverted=inverted) elif self.scanner.consume('repeat'): inverted = False src = None @@ -364,7 +364,7 @@ class Parser(object): src = self.locexpr() else: self.scanner.expect('forever') - return Instr(opcode='repeat', dest=None, src=src, + return BlockOp(opcode='repeat', dest=None, src=src, block=block, inverted=inverted) elif self.scanner.token in ("ld",): # the same as add, sub, cmp etc below, except supports an indlocexpr for the src @@ -373,7 +373,7 @@ class Parser(object): dest = self.locexpr() self.scanner.expect(',') src = self.indlocexpr() - return Instr(opcode=opcode, dest=dest, src=src, index=None) + return SingleOp(opcode=opcode, dest=dest, src=src, index=None) elif self.scanner.token in ("add", "sub", "cmp", "and", "or", "xor"): opcode = self.scanner.token self.scanner.scan() @@ -383,25 +383,25 @@ class Parser(object): index = None if self.scanner.consume('+'): index = self.locexpr() - return Instr(opcode=opcode, dest=dest, src=src, index=index) + return SingleOp(opcode=opcode, dest=dest, src=src, index=index) elif self.scanner.token in ("st",): opcode = self.scanner.token self.scanner.scan() src = self.locexpr() self.scanner.expect(',') dest = self.indlocexpr() - return Instr(opcode=opcode, dest=dest, src=src, index=None) + return SingleOp(opcode=opcode, dest=dest, src=src, index=None) elif self.scanner.token in ("shl", "shr", "inc", "dec"): opcode = self.scanner.token self.scanner.scan() dest = self.locexpr() - return Instr(opcode=opcode, dest=dest, src=None) + return SingleOp(opcode=opcode, dest=dest, src=None) elif self.scanner.token in ("call", "goto"): opcode = self.scanner.token self.scanner.scan() name = self.scanner.token self.scanner.scan() - instr = Instr(opcode=opcode, location=name, dest=None, src=None) + instr = SingleOp(opcode=opcode, location=name, dest=None, src=None) self.backpatch_instrs.append(instr) return instr elif self.scanner.token in ("copy",): @@ -410,16 +410,16 @@ class Parser(object): src = self.indlocexpr(forward=True) self.scanner.expect(',') dest = self.indlocexpr() - instr = Instr(opcode=opcode, dest=dest, src=src) + instr = SingleOp(opcode=opcode, dest=dest, src=src) self.backpatch_instrs.append(instr) return instr elif self.scanner.consume("with"): self.scanner.expect("interrupts") self.scanner.expect("off") block = self.block() - return Instr(opcode='with-sei', dest=None, src=None, block=block) + return BlockOp(opcode='with-sei', dest=None, src=None, block=block) elif self.scanner.consume("trash"): dest = self.locexpr() - return Instr(opcode='trash', src=None, dest=dest) + return SingleOp(opcode='trash', src=None, dest=dest) else: raise ValueError('bad opcode "%s"' % self.scanner.token) From 0a83d905158a51df7d3eeefa6a81df6e1612880e Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 5 Mar 2018 12:13:15 +0000 Subject: [PATCH 14/35] Tighten structure of AST more. --- src/sixtypical/analyzer.py | 126 ++++++++++++++++++++----------------- src/sixtypical/ast.py | 4 +- src/sixtypical/compiler.py | 113 ++++++++++++++++++--------------- src/sixtypical/parser.py | 8 +-- 4 files changed, 138 insertions(+), 113 deletions(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 396e116..9c9589e 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -1,6 +1,6 @@ # encoding: UTF-8 -from sixtypical.ast import Program, Routine, Block, Instr +from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, BlockOp, IfOp from sixtypical.model import ( TYPE_BYTE, TYPE_WORD, TableType, BufferType, PointerType, VectorType, RoutineType, @@ -312,7 +312,17 @@ class Analyzer(object): self.analyze_instr(i, context) def analyze_instr(self, instr, context): - assert isinstance(instr, Instr) + if isinstance(instr, SingleOp): + return self.analyze_single_op(instr, context) + elif isinstance(instr, BlockOp): + return self.analyze_block_op(instr, context) + elif isinstance(instr, IfOp): + return self.analyze_if_op(instr, context) + else: + raise NotImplementedError + + def analyze_single_op(self, instr, context): + opcode = instr.opcode dest = instr.dest src = instr.src @@ -429,58 +439,6 @@ class Analyzer(object): context.assert_writeable(ref) context.set_touched(ref) context.set_unmeaningful(ref) - elif opcode == 'if': - incoming_meaningful = set(context.each_meaningful()) - - context1 = context.clone() - context2 = context.clone() - self.analyze_block(instr.block1, context1) - if instr.block2 is not None: - self.analyze_block(instr.block2, context2) - - outgoing_meaningful = set(context1.each_meaningful()) & set(context2.each_meaningful()) - outgoing_trashes = incoming_meaningful - outgoing_meaningful - - # TODO may we need to deal with touched separately here too? - # probably not; if it wasn't meaningful in the first place, it - # doesn't really matter if you modified it or not, coming out. - for ref in context1.each_meaningful(): - if ref in outgoing_trashes: - continue - context2.assert_meaningful( - ref, exception_class=InconsistentInitializationError, - message='initialized in block 1 but not in block 2 of `if {}`'.format(src) - ) - for ref in context2.each_meaningful(): - if ref in outgoing_trashes: - continue - context1.assert_meaningful( - ref, exception_class=InconsistentInitializationError, - message='initialized in block 2 but not in block 1 of `if {}`'.format(src) - ) - - # merge the contexts. this used to be a method called `set_from` - context._touched = set(context1._touched) | set(context2._touched) - context.set_meaningful(*list(outgoing_meaningful)) - context._writeable = set(context1._writeable) | set(context2._writeable) - - for ref in outgoing_trashes: - context.set_touched(ref) - context.set_unmeaningful(ref) - - elif opcode == 'repeat': - # it will always be executed at least once, so analyze it having - # been executed the first time. - self.analyze_block(instr.block, context) - if src is not None: # None indicates 'repeat forever' - context.assert_meaningful(src) - - # now analyze it having been executed a second time, with the context - # of it having already been executed. - self.analyze_block(instr.block, context) - if src is not None: - context.assert_meaningful(src) - elif opcode == 'copy': if dest == REG_A: raise ForbiddenWriteError("{} cannot be used as destination for copy".format(dest)) @@ -563,9 +521,6 @@ class Analyzer(object): context.set_touched(REG_A, FLAG_Z, FLAG_N) context.set_unmeaningful(REG_A, FLAG_Z, FLAG_N) - - elif opcode == 'with-sei': - self.analyze_block(instr.block, context) elif opcode == 'goto': location = instr.location type_ = location.type @@ -591,3 +546,60 @@ class Analyzer(object): context.set_unmeaningful(instr.dest) else: raise NotImplementedError(opcode) + + def analyze_if_op(self, instr, context): + incoming_meaningful = set(context.each_meaningful()) + + context1 = context.clone() + context2 = context.clone() + self.analyze_block(instr.block1, context1) + if instr.block2 is not None: + self.analyze_block(instr.block2, context2) + + outgoing_meaningful = set(context1.each_meaningful()) & set(context2.each_meaningful()) + outgoing_trashes = incoming_meaningful - outgoing_meaningful + + # TODO may we need to deal with touched separately here too? + # probably not; if it wasn't meaningful in the first place, it + # doesn't really matter if you modified it or not, coming out. + for ref in context1.each_meaningful(): + if ref in outgoing_trashes: + continue + context2.assert_meaningful( + ref, exception_class=InconsistentInitializationError, + message='initialized in block 1 but not in block 2 of `if {}`'.format(instr.src) + ) + for ref in context2.each_meaningful(): + if ref in outgoing_trashes: + continue + context1.assert_meaningful( + ref, exception_class=InconsistentInitializationError, + message='initialized in block 2 but not in block 1 of `if {}`'.format(instr.src) + ) + + # merge the contexts. this used to be a method called `set_from` + context._touched = set(context1._touched) | set(context2._touched) + context.set_meaningful(*list(outgoing_meaningful)) + context._writeable = set(context1._writeable) | set(context2._writeable) + + for ref in outgoing_trashes: + context.set_touched(ref) + context.set_unmeaningful(ref) + + def analyze_block_op(self, instr, context): + if instr.opcode == 'repeat': + # it will always be executed at least once, so analyze it having + # been executed the first time. + self.analyze_block(instr.block, context) + if instr.src is not None: # None indicates 'repeat forever' + context.assert_meaningful(instr.src) + + # now analyze it having been executed a second time, with the context + # of it having already been executed. + self.analyze_block(instr.block, context) + if instr.src is not None: + context.assert_meaningful(instr.src) + elif instr.opcode == 'with-sei': + self.analyze_block(instr.block, context) + else: + raise NotImplementedError(opcode) diff --git a/src/sixtypical/ast.py b/src/sixtypical/ast.py index 771fc1d..e992381 100644 --- a/src/sixtypical/ast.py +++ b/src/sixtypical/ast.py @@ -73,10 +73,10 @@ class SingleOp(Instr): class BlockOp(Instr): - value_attrs = ('opcode', 'dest', 'src', 'inverted') + value_attrs = ('opcode', 'src', 'inverted') child_attrs = ('block',) class IfOp(Instr): - value_attrs = ('opcode', 'dest', 'src', 'inverted') + value_attrs = ('src', 'inverted') child_attrs = ('block1', 'block2',) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index 6ea7c4e..8bf37c4 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -1,6 +1,6 @@ # encoding: UTF-8 -from sixtypical.ast import Program, Routine, Block, Instr +from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, BlockOp, IfOp from sixtypical.model import ( ConstantRef, LocationRef, IndexedRef, IndirectRef, AddressRef, TYPE_BIT, TYPE_BYTE, TYPE_WORD, @@ -142,7 +142,17 @@ class Compiler(object): self.compile_instr(instr) def compile_instr(self, instr): - assert isinstance(instr, Instr) + if isinstance(instr, SingleOp): + return self.compile_single_op(instr) + elif isinstance(instr, BlockOp): + return self.compile_block_op(instr) + elif isinstance(instr, IfOp): + return self.compile_if_op(instr) + else: + raise NotImplementedError + + def compile_single_op(self, instr): + opcode = instr.opcode dest = instr.dest src = instr.src @@ -356,53 +366,6 @@ class Compiler(object): self.emitter.emit(JMP(Indirect(label))) else: raise NotImplementedError - elif opcode == 'if': - cls = { - False: { - 'c': BCC, - 'z': BNE, - }, - True: { - 'c': BCS, - 'z': BEQ, - }, - }[instr.inverted].get(src.name) - if cls is None: - raise UnsupportedOpcodeError(instr) - else_label = Label('else_label') - self.emitter.emit(cls(Relative(else_label))) - self.compile_block(instr.block1) - if instr.block2 is not None: - end_label = Label('end_label') - self.emitter.emit(JMP(Absolute(end_label))) - self.emitter.resolve_label(else_label) - self.compile_block(instr.block2) - self.emitter.resolve_label(end_label) - else: - self.emitter.resolve_label(else_label) - elif opcode == 'repeat': - top_label = self.emitter.make_label() - self.compile_block(instr.block) - if src is None: # indicates 'repeat forever' - self.emitter.emit(JMP(Absolute(top_label))) - else: - cls = { - False: { - 'c': BCC, - 'z': BNE, - }, - True: { - 'c': BCS, - 'z': BEQ, - }, - }[instr.inverted].get(src.name) - if cls is None: - raise UnsupportedOpcodeError(instr) - self.emitter.emit(cls(Relative(top_label))) - elif opcode == 'with-sei': - self.emitter.emit(SEI()) - self.compile_block(instr.block) - self.emitter.emit(CLI()) elif opcode == 'copy': if isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndirectRef): if src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType): @@ -532,3 +495,55 @@ class Compiler(object): pass else: raise NotImplementedError(opcode) + + def compile_if_op(self, instr): + cls = { + False: { + 'c': BCC, + 'z': BNE, + }, + True: { + 'c': BCS, + 'z': BEQ, + }, + }[instr.inverted].get(instr.src.name) + if cls is None: + raise UnsupportedOpcodeError(instr) + else_label = Label('else_label') + self.emitter.emit(cls(Relative(else_label))) + self.compile_block(instr.block1) + if instr.block2 is not None: + end_label = Label('end_label') + self.emitter.emit(JMP(Absolute(end_label))) + self.emitter.resolve_label(else_label) + self.compile_block(instr.block2) + self.emitter.resolve_label(end_label) + else: + self.emitter.resolve_label(else_label) + + def compile_block_op(self, instr): + if instr.opcode == 'repeat': + top_label = self.emitter.make_label() + self.compile_block(instr.block) + if instr.src is None: # indicates 'repeat forever' + self.emitter.emit(JMP(Absolute(top_label))) + else: + cls = { + False: { + 'c': BCC, + 'z': BNE, + }, + True: { + 'c': BCS, + 'z': BEQ, + }, + }[instr.inverted].get(instr.src.name) + if cls is None: + raise UnsupportedOpcodeError(instr) + self.emitter.emit(cls(Relative(top_label))) + elif instr.opcode == 'with-sei': + self.emitter.emit(SEI()) + self.compile_block(instr.block) + self.emitter.emit(CLI()) + else: + raise NotImplementedError(opcode) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 8fd1059..c4c67ca 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -352,8 +352,7 @@ class Parser(object): block2 = None if self.scanner.consume('else'): block2 = self.block() - return IfOp(opcode='if', dest=None, src=src, - block1=block1, block2=block2, inverted=inverted) + return IfOp(src=src, block1=block1, block2=block2, inverted=inverted) elif self.scanner.consume('repeat'): inverted = False src = None @@ -364,8 +363,7 @@ class Parser(object): src = self.locexpr() else: self.scanner.expect('forever') - return BlockOp(opcode='repeat', dest=None, src=src, - block=block, inverted=inverted) + return BlockOp(opcode='repeat', src=src, block=block, inverted=inverted) 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 @@ -417,7 +415,7 @@ class Parser(object): self.scanner.expect("interrupts") self.scanner.expect("off") block = self.block() - return BlockOp(opcode='with-sei', dest=None, src=None, block=block) + return BlockOp(opcode='with-sei', src=None, block=block) elif self.scanner.consume("trash"): dest = self.locexpr() return SingleOp(opcode='trash', src=None, dest=dest) From b579e8b2ebcb5bb9e9bb0b0449e3ec47f6c64bfa Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 5 Mar 2018 12:19:27 +0000 Subject: [PATCH 15/35] Refs have indexes. Instructions do not. --- src/sixtypical/ast.py | 2 +- src/sixtypical/parser.py | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/sixtypical/ast.py b/src/sixtypical/ast.py index e992381..075742f 100644 --- a/src/sixtypical/ast.py +++ b/src/sixtypical/ast.py @@ -69,7 +69,7 @@ class Instr(AST): class SingleOp(Instr): - value_attrs = ('opcode', 'dest', 'src', 'index', 'location',) + value_attrs = ('opcode', 'dest', 'src', 'location',) class BlockOp(Instr): diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index c4c67ca..aedab41 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -317,13 +317,16 @@ class Parser(object): loc = self.locexpr() return AddressRef(loc) else: - loc = self.locexpr(forward=forward) - if not isinstance(loc, basestring): - index = None - if self.scanner.consume('+'): - index = self.locexpr() - loc = IndexedRef(loc, index) - return loc + return self.indexed_locexpr(forward=forward) + + def indexed_locexpr(self, forward=False): + loc = self.locexpr(forward=forward) + if not isinstance(loc, basestring): + index = None + if self.scanner.consume('+'): + index = self.locexpr() + loc = IndexedRef(loc, index) + return loc def statics(self): defns = [] @@ -371,24 +374,21 @@ class Parser(object): dest = self.locexpr() self.scanner.expect(',') src = self.indlocexpr() - return SingleOp(opcode=opcode, dest=dest, src=src, index=None) + return SingleOp(opcode=opcode, dest=dest, src=src) elif self.scanner.token in ("add", "sub", "cmp", "and", "or", "xor"): opcode = self.scanner.token self.scanner.scan() dest = self.locexpr() self.scanner.expect(',') - src = self.locexpr() - index = None - if self.scanner.consume('+'): - index = self.locexpr() - return SingleOp(opcode=opcode, dest=dest, src=src, index=index) + src = self.indexed_locexpr() + return SingleOp(opcode=opcode, dest=dest, src=src) elif self.scanner.token in ("st",): opcode = self.scanner.token self.scanner.scan() src = self.locexpr() self.scanner.expect(',') dest = self.indlocexpr() - return SingleOp(opcode=opcode, dest=dest, src=src, index=None) + return SingleOp(opcode=opcode, dest=dest, src=src) elif self.scanner.token in ("shl", "shr", "inc", "dec"): opcode = self.scanner.token self.scanner.scan() From c85159a9c0a70ba7739cb6ab42d94cb0e26eae36 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 5 Mar 2018 12:34:24 +0000 Subject: [PATCH 16/35] Start trying to clean up the code for compiling `copy`. --- src/sixtypical/compiler.py | 240 +++++++++++++++++++------------------ 1 file changed, 124 insertions(+), 116 deletions(-) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index 8bf37c4..3dac877 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -367,134 +367,142 @@ class Compiler(object): else: raise NotImplementedError elif opcode == 'copy': - if isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndirectRef): - if src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType): - if isinstance(src, ConstantRef): - dest_label = self.get_label(dest.ref.name) - self.emitter.emit(LDA(Immediate(Byte(src.value)))) - self.emitter.emit(STA(IndirectY(dest_label))) - elif isinstance(src, LocationRef): - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.ref.name) - self.emitter.emit(LDA(Absolute(src_label))) - self.emitter.emit(STA(IndirectY(dest_label))) - else: - raise NotImplementedError((src, dest)) + self.compile_copy_op(instr) + elif opcode == 'trash': + pass + else: + raise NotImplementedError(opcode) + + def compile_copy_op(self, instr): + + opcode = instr.opcode + dest = instr.dest + src = instr.src + + if isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndirectRef): + if src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType): + if isinstance(src, ConstantRef): + dest_label = self.get_label(dest.ref.name) + self.emitter.emit(LDA(Immediate(Byte(src.value)))) + self.emitter.emit(STA(IndirectY(dest_label))) + elif isinstance(src, LocationRef): + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.ref.name) + self.emitter.emit(LDA(Absolute(src_label))) + self.emitter.emit(STA(IndirectY(dest_label))) else: raise NotImplementedError((src, dest)) - elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef): - if dest.type == TYPE_BYTE and isinstance(src.ref.type, PointerType): - src_label = self.get_label(src.ref.name) - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(IndirectY(src_label))) - self.emitter.emit(STA(Absolute(dest_label))) - else: - raise NotImplementedError((src, dest)) - elif isinstance(src, AddressRef) and isinstance(dest, LocationRef) and \ - isinstance(src.ref.type, BufferType) and isinstance(dest.type, PointerType): + else: + raise NotImplementedError((src, dest)) + elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef): + if dest.type == TYPE_BYTE and isinstance(src.ref.type, PointerType): src_label = self.get_label(src.ref.name) dest_label = self.get_label(dest.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)))) - elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef): - if src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD): - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.ref.name) - self.emitter.emit(LDA(Absolute(src_label))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) - self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) - elif isinstance(src.type, VectorType) and isinstance(dest.ref.type, TableType) and isinstance(dest.ref.type.of_type, VectorType): - # FIXME this is the exact same as above - can this be simplified? - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.ref.name) - self.emitter.emit(LDA(Absolute(src_label))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) - self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) - elif isinstance(src.type, RoutineType) and isinstance(dest.ref.type, TableType) and isinstance(dest.ref.type.of_type, VectorType): - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.ref.name) - self.emitter.emit(LDA(Immediate(HighAddressByte(src_label)))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) - self.emitter.emit(LDA(Immediate(LowAddressByte(src_label)))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) - else: - raise NotImplementedError - elif isinstance(src, ConstantRef) and isinstance(dest, IndexedRef): - if src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD): - dest_label = self.get_label(dest.ref.name) - self.emitter.emit(LDA(Immediate(Byte(src.low_byte())))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) - self.emitter.emit(LDA(Immediate(Byte(src.high_byte())))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) - else: - raise NotImplementedError - elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef): - if TableType.is_a_table_type(src.ref.type, TYPE_WORD) and dest.type == TYPE_WORD: - src_label = self.get_label(src.ref.name) - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(src_label))) - self.emitter.emit(STA(Absolute(dest_label))) - self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(Offset(src_label, 256)))) - self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) - elif isinstance(dest.type, VectorType) and isinstance(src.ref.type, TableType) and isinstance(src.ref.type.of_type, VectorType): - # FIXME this is the exact same as above - can this be simplified? - src_label = self.get_label(src.ref.name) - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(src_label))) - self.emitter.emit(STA(Absolute(dest_label))) - self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(Offset(src_label, 256)))) - self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) - else: - raise NotImplementedError - - elif not isinstance(src, (ConstantRef, LocationRef)) or not isinstance(dest, LocationRef): + self.emitter.emit(LDA(IndirectY(src_label))) + self.emitter.emit(STA(Absolute(dest_label))) + else: raise NotImplementedError((src, dest)) - elif src.type == TYPE_BYTE and dest.type == TYPE_BYTE: - if isinstance(src, ConstantRef): - raise NotImplementedError - else: - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(Absolute(src_label))) - self.emitter.emit(STA(Absolute(dest_label))) - elif src.type == TYPE_WORD and dest.type == TYPE_WORD: - if isinstance(src, ConstantRef): - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(Immediate(Byte(src.low_byte())))) - self.emitter.emit(STA(Absolute(dest_label))) - self.emitter.emit(LDA(Immediate(Byte(src.high_byte())))) - self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) - else: - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(Absolute(src_label))) - self.emitter.emit(STA(Absolute(dest_label))) - self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) - self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) - elif isinstance(src.type, VectorType) and isinstance(dest.type, VectorType): + elif isinstance(src, AddressRef) and isinstance(dest, LocationRef) and \ + isinstance(src.ref.type, BufferType) and isinstance(dest.type, PointerType): + src_label = self.get_label(src.ref.name) + dest_label = self.get_label(dest.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)))) + elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef): + if src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD): + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.ref.name) + self.emitter.emit(LDA(Absolute(src_label))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) + self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) + elif isinstance(src.type, VectorType) and isinstance(dest.ref.type, TableType) and isinstance(dest.ref.type.of_type, VectorType): + # FIXME this is the exact same as above - can this be simplified? + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.ref.name) + self.emitter.emit(LDA(Absolute(src_label))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) + self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) + elif isinstance(src.type, RoutineType) and isinstance(dest.ref.type, TableType) and isinstance(dest.ref.type.of_type, VectorType): + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.ref.name) + self.emitter.emit(LDA(Immediate(HighAddressByte(src_label)))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) + self.emitter.emit(LDA(Immediate(LowAddressByte(src_label)))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) + else: + raise NotImplementedError + elif isinstance(src, ConstantRef) and isinstance(dest, IndexedRef): + if src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD): + dest_label = self.get_label(dest.ref.name) + self.emitter.emit(LDA(Immediate(Byte(src.low_byte())))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) + self.emitter.emit(LDA(Immediate(Byte(src.high_byte())))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) + else: + raise NotImplementedError + elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef): + if TableType.is_a_table_type(src.ref.type, TYPE_WORD) and dest.type == TYPE_WORD: + src_label = self.get_label(src.ref.name) + dest_label = self.get_label(dest.name) + self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(src_label))) + self.emitter.emit(STA(Absolute(dest_label))) + self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(Offset(src_label, 256)))) + self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) + elif isinstance(dest.type, VectorType) and isinstance(src.ref.type, TableType) and isinstance(src.ref.type.of_type, VectorType): + # FIXME this is the exact same as above - can this be simplified? + src_label = self.get_label(src.ref.name) + dest_label = self.get_label(dest.name) + self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(src_label))) + self.emitter.emit(STA(Absolute(dest_label))) + self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(Offset(src_label, 256)))) + self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) + else: + raise NotImplementedError + + elif not isinstance(src, (ConstantRef, LocationRef)) or not isinstance(dest, LocationRef): + raise NotImplementedError((src, dest)) + elif src.type == TYPE_BYTE and dest.type == TYPE_BYTE: + if isinstance(src, ConstantRef): + raise NotImplementedError + else: + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.name) + self.emitter.emit(LDA(Absolute(src_label))) + self.emitter.emit(STA(Absolute(dest_label))) + elif src.type == TYPE_WORD and dest.type == TYPE_WORD: + if isinstance(src, ConstantRef): + dest_label = self.get_label(dest.name) + self.emitter.emit(LDA(Immediate(Byte(src.low_byte())))) + self.emitter.emit(STA(Absolute(dest_label))) + self.emitter.emit(LDA(Immediate(Byte(src.high_byte())))) + self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) + else: src_label = self.get_label(src.name) dest_label = self.get_label(dest.name) self.emitter.emit(LDA(Absolute(src_label))) self.emitter.emit(STA(Absolute(dest_label))) self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) - elif isinstance(src.type, RoutineType) and isinstance(dest.type, VectorType): - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(Immediate(HighAddressByte(src_label)))) - self.emitter.emit(STA(Absolute(dest_label))) - self.emitter.emit(LDA(Immediate(LowAddressByte(src_label)))) - self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) - else: - raise NotImplementedError(src.type) - elif opcode == 'trash': - pass + elif isinstance(src.type, VectorType) and isinstance(dest.type, VectorType): + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.name) + self.emitter.emit(LDA(Absolute(src_label))) + self.emitter.emit(STA(Absolute(dest_label))) + self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) + self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) + elif isinstance(src.type, RoutineType) and isinstance(dest.type, VectorType): + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.name) + self.emitter.emit(LDA(Immediate(HighAddressByte(src_label)))) + self.emitter.emit(STA(Absolute(dest_label))) + self.emitter.emit(LDA(Immediate(LowAddressByte(src_label)))) + self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) else: - raise NotImplementedError(opcode) + raise NotImplementedError(src.type) def compile_if_op(self, instr): cls = { From c194808e6472c067f3f58d142ea5d54414cbc0b3 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 5 Mar 2018 13:23:09 +0000 Subject: [PATCH 17/35] Label the various cases for compiling a `copy` instruction. --- src/sixtypical/compiler.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index 3dac877..e9a0e8a 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -382,10 +382,12 @@ class Compiler(object): if isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndirectRef): if src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType): if isinstance(src, ConstantRef): + ### copy 123, [ptr] + y dest_label = self.get_label(dest.ref.name) self.emitter.emit(LDA(Immediate(Byte(src.value)))) self.emitter.emit(STA(IndirectY(dest_label))) elif isinstance(src, LocationRef): + ### copy b, [ptr] + y src_label = self.get_label(src.name) dest_label = self.get_label(dest.ref.name) self.emitter.emit(LDA(Absolute(src_label))) @@ -396,6 +398,7 @@ class Compiler(object): raise NotImplementedError((src, dest)) elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef): if dest.type == TYPE_BYTE and isinstance(src.ref.type, PointerType): + ### copy [ptr] + y, b src_label = self.get_label(src.ref.name) dest_label = self.get_label(dest.name) self.emitter.emit(LDA(IndirectY(src_label))) @@ -404,6 +407,7 @@ class Compiler(object): raise NotImplementedError((src, dest)) elif isinstance(src, AddressRef) and isinstance(dest, LocationRef) and \ isinstance(src.ref.type, BufferType) and isinstance(dest.type, PointerType): + ### copy ^buf, ptr src_label = self.get_label(src.ref.name) dest_label = self.get_label(dest.name) self.emitter.emit(LDA(Immediate(HighAddressByte(src_label)))) @@ -412,6 +416,7 @@ class Compiler(object): self.emitter.emit(STA(ZeroPage(Offset(dest_label, 1)))) elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef): if src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD): + ### copy w, wtab + y src_label = self.get_label(src.name) dest_label = self.get_label(dest.ref.name) self.emitter.emit(LDA(Absolute(src_label))) @@ -419,6 +424,7 @@ class Compiler(object): self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) elif isinstance(src.type, VectorType) and isinstance(dest.ref.type, TableType) and isinstance(dest.ref.type.of_type, VectorType): + ### copy vec, vtab + y # FIXME this is the exact same as above - can this be simplified? src_label = self.get_label(src.name) dest_label = self.get_label(dest.ref.name) @@ -427,6 +433,7 @@ class Compiler(object): self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) elif isinstance(src.type, RoutineType) and isinstance(dest.ref.type, TableType) and isinstance(dest.ref.type.of_type, VectorType): + ### copy routine, vtab + y src_label = self.get_label(src.name) dest_label = self.get_label(dest.ref.name) self.emitter.emit(LDA(Immediate(HighAddressByte(src_label)))) @@ -437,6 +444,7 @@ class Compiler(object): raise NotImplementedError elif isinstance(src, ConstantRef) and isinstance(dest, IndexedRef): if src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD): + ### copy 9999, wtab + y dest_label = self.get_label(dest.ref.name) self.emitter.emit(LDA(Immediate(Byte(src.low_byte())))) self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) @@ -446,6 +454,7 @@ class Compiler(object): raise NotImplementedError elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef): if TableType.is_a_table_type(src.ref.type, TYPE_WORD) and dest.type == TYPE_WORD: + ### copy wtab + y, w src_label = self.get_label(src.ref.name) dest_label = self.get_label(dest.name) self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(src_label))) @@ -453,6 +462,7 @@ class Compiler(object): self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(Offset(src_label, 256)))) self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) elif isinstance(dest.type, VectorType) and isinstance(src.ref.type, TableType) and isinstance(src.ref.type.of_type, VectorType): + ### copy vtab + y, vec # FIXME this is the exact same as above - can this be simplified? src_label = self.get_label(src.ref.name) dest_label = self.get_label(dest.name) @@ -469,18 +479,21 @@ class Compiler(object): if isinstance(src, ConstantRef): raise NotImplementedError else: + ### copy b1, b2 src_label = self.get_label(src.name) dest_label = self.get_label(dest.name) self.emitter.emit(LDA(Absolute(src_label))) self.emitter.emit(STA(Absolute(dest_label))) elif src.type == TYPE_WORD and dest.type == TYPE_WORD: if isinstance(src, ConstantRef): + ### copy 9999, w dest_label = self.get_label(dest.name) self.emitter.emit(LDA(Immediate(Byte(src.low_byte())))) self.emitter.emit(STA(Absolute(dest_label))) self.emitter.emit(LDA(Immediate(Byte(src.high_byte())))) self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) else: + ### copy w1, w2 src_label = self.get_label(src.name) dest_label = self.get_label(dest.name) self.emitter.emit(LDA(Absolute(src_label))) @@ -488,6 +501,7 @@ class Compiler(object): self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) elif isinstance(src.type, VectorType) and isinstance(dest.type, VectorType): + ### copy v1, v2 src_label = self.get_label(src.name) dest_label = self.get_label(dest.name) self.emitter.emit(LDA(Absolute(src_label))) @@ -495,6 +509,7 @@ class Compiler(object): self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) elif isinstance(src.type, RoutineType) and isinstance(dest.type, VectorType): + ### copy routine, vec src_label = self.get_label(src.name) dest_label = self.get_label(dest.name) self.emitter.emit(LDA(Immediate(HighAddressByte(src_label)))) From 1180d2fe2afb95723fa0dacc1620f7f5f8ec792f Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 5 Mar 2018 15:22:47 +0000 Subject: [PATCH 18/35] Attempt to flatten the code for generating code for `copy`. --- src/sixtypical/compiler.py | 174 ++++++++++++++++--------------------- 1 file changed, 77 insertions(+), 97 deletions(-) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index e9a0e8a..0559874 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -396,17 +396,13 @@ class Compiler(object): raise NotImplementedError((src, dest)) else: raise NotImplementedError((src, dest)) - elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef): - if dest.type == TYPE_BYTE and isinstance(src.ref.type, PointerType): - ### copy [ptr] + y, b - src_label = self.get_label(src.ref.name) - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(IndirectY(src_label))) - self.emitter.emit(STA(Absolute(dest_label))) - else: - raise NotImplementedError((src, dest)) - elif isinstance(src, AddressRef) and isinstance(dest, LocationRef) and \ - isinstance(src.ref.type, BufferType) and isinstance(dest.type, PointerType): + elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef) and dest.type == TYPE_BYTE and isinstance(src.ref.type, PointerType): + ### copy [ptr] + y, b + src_label = self.get_label(src.ref.name) + dest_label = self.get_label(dest.name) + self.emitter.emit(LDA(IndirectY(src_label))) + self.emitter.emit(STA(Absolute(dest_label))) + elif isinstance(src, AddressRef) and isinstance(dest, LocationRef) and isinstance(src.ref.type, BufferType) and isinstance(dest.type, PointerType): ### copy ^buf, ptr src_label = self.get_label(src.ref.name) dest_label = self.get_label(dest.name) @@ -414,92 +410,76 @@ class Compiler(object): self.emitter.emit(STA(ZeroPage(dest_label))) self.emitter.emit(LDA(Immediate(LowAddressByte(src_label)))) self.emitter.emit(STA(ZeroPage(Offset(dest_label, 1)))) - elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef): - if src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD): - ### copy w, wtab + y - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.ref.name) - self.emitter.emit(LDA(Absolute(src_label))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) - self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) - elif isinstance(src.type, VectorType) and isinstance(dest.ref.type, TableType) and isinstance(dest.ref.type.of_type, VectorType): - ### copy vec, vtab + y - # FIXME this is the exact same as above - can this be simplified? - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.ref.name) - self.emitter.emit(LDA(Absolute(src_label))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) - self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) - elif isinstance(src.type, RoutineType) and isinstance(dest.ref.type, TableType) and isinstance(dest.ref.type.of_type, VectorType): - ### copy routine, vtab + y - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.ref.name) - self.emitter.emit(LDA(Immediate(HighAddressByte(src_label)))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) - self.emitter.emit(LDA(Immediate(LowAddressByte(src_label)))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) - else: - raise NotImplementedError - elif isinstance(src, ConstantRef) and isinstance(dest, IndexedRef): - if src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD): - ### copy 9999, wtab + y - dest_label = self.get_label(dest.ref.name) - self.emitter.emit(LDA(Immediate(Byte(src.low_byte())))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) - self.emitter.emit(LDA(Immediate(Byte(src.high_byte())))) - self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) - else: - raise NotImplementedError - elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef): - if TableType.is_a_table_type(src.ref.type, TYPE_WORD) and dest.type == TYPE_WORD: - ### copy wtab + y, w - src_label = self.get_label(src.ref.name) - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(src_label))) - self.emitter.emit(STA(Absolute(dest_label))) - self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(Offset(src_label, 256)))) - self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) - elif isinstance(dest.type, VectorType) and isinstance(src.ref.type, TableType) and isinstance(src.ref.type.of_type, VectorType): - ### copy vtab + y, vec - # FIXME this is the exact same as above - can this be simplified? - src_label = self.get_label(src.ref.name) - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(src_label))) - self.emitter.emit(STA(Absolute(dest_label))) - self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(Offset(src_label, 256)))) - self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) - else: - raise NotImplementedError - - elif not isinstance(src, (ConstantRef, LocationRef)) or not isinstance(dest, LocationRef): - raise NotImplementedError((src, dest)) - elif src.type == TYPE_BYTE and dest.type == TYPE_BYTE: - if isinstance(src, ConstantRef): - raise NotImplementedError - else: - ### copy b1, b2 - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(Absolute(src_label))) - self.emitter.emit(STA(Absolute(dest_label))) - elif src.type == TYPE_WORD and dest.type == TYPE_WORD: - if isinstance(src, ConstantRef): - ### copy 9999, w - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(Immediate(Byte(src.low_byte())))) - self.emitter.emit(STA(Absolute(dest_label))) - self.emitter.emit(LDA(Immediate(Byte(src.high_byte())))) - self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) - else: - ### copy w1, w2 - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.name) - self.emitter.emit(LDA(Absolute(src_label))) - self.emitter.emit(STA(Absolute(dest_label))) - self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) - self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) + elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef) and src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD): + ### copy w, wtab + y + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.ref.name) + self.emitter.emit(LDA(Absolute(src_label))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) + self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) + elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef) and isinstance(src.type, VectorType) and isinstance(dest.ref.type, TableType) and isinstance(dest.ref.type.of_type, VectorType): + ### copy vec, vtab + y + # FIXME this is the exact same as above - can this be simplified? + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.ref.name) + self.emitter.emit(LDA(Absolute(src_label))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) + self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) + elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef) and isinstance(src.type, RoutineType) and isinstance(dest.ref.type, TableType) and isinstance(dest.ref.type.of_type, VectorType): + ### copy routine, vtab + y + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.ref.name) + self.emitter.emit(LDA(Immediate(HighAddressByte(src_label)))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) + self.emitter.emit(LDA(Immediate(LowAddressByte(src_label)))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) + elif isinstance(src, ConstantRef) and isinstance(dest, IndexedRef) and src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD): + ### copy 9999, wtab + y + dest_label = self.get_label(dest.ref.name) + self.emitter.emit(LDA(Immediate(Byte(src.low_byte())))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label))) + self.emitter.emit(LDA(Immediate(Byte(src.high_byte())))) + self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256)))) + elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef) and TableType.is_a_table_type(src.ref.type, TYPE_WORD) and dest.type == TYPE_WORD: + ### copy wtab + y, w + src_label = self.get_label(src.ref.name) + dest_label = self.get_label(dest.name) + self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(src_label))) + self.emitter.emit(STA(Absolute(dest_label))) + self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(Offset(src_label, 256)))) + self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) + elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef) and isinstance(dest.type, VectorType) and isinstance(src.ref.type, TableType) and isinstance(src.ref.type.of_type, VectorType): + ### copy vtab + y, vec + # FIXME this is the exact same as above - can this be simplified? + src_label = self.get_label(src.ref.name) + dest_label = self.get_label(dest.name) + self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(src_label))) + self.emitter.emit(STA(Absolute(dest_label))) + self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(Offset(src_label, 256)))) + self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) + elif src.type == TYPE_BYTE and dest.type == TYPE_BYTE and not isinstance(src, ConstantRef): + ### copy b1, b2 + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.name) + self.emitter.emit(LDA(Absolute(src_label))) + self.emitter.emit(STA(Absolute(dest_label))) + elif src.type == TYPE_WORD and dest.type == TYPE_WORD and isinstance(src, ConstantRef): + ### copy 9999, w + dest_label = self.get_label(dest.name) + self.emitter.emit(LDA(Immediate(Byte(src.low_byte())))) + self.emitter.emit(STA(Absolute(dest_label))) + self.emitter.emit(LDA(Immediate(Byte(src.high_byte())))) + self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) + elif src.type == TYPE_WORD and dest.type == TYPE_WORD and not isinstance(src, ConstantRef): + ### copy w1, w2 + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.name) + self.emitter.emit(LDA(Absolute(src_label))) + self.emitter.emit(STA(Absolute(dest_label))) + self.emitter.emit(LDA(Absolute(Offset(src_label, 1)))) + self.emitter.emit(STA(Absolute(Offset(dest_label, 1)))) elif isinstance(src.type, VectorType) and isinstance(dest.type, VectorType): ### copy v1, v2 src_label = self.get_label(src.name) From 8a7e3aaebb15977ea72565a552124ecabefa8558 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 5 Mar 2018 15:32:08 +0000 Subject: [PATCH 19/35] Flatten more. --- src/sixtypical/compiler.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index 0559874..55e64c9 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -379,23 +379,17 @@ class Compiler(object): dest = instr.dest src = instr.src - if isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndirectRef): - if src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType): - if isinstance(src, ConstantRef): - ### copy 123, [ptr] + y - dest_label = self.get_label(dest.ref.name) - self.emitter.emit(LDA(Immediate(Byte(src.value)))) - self.emitter.emit(STA(IndirectY(dest_label))) - elif isinstance(src, LocationRef): - ### copy b, [ptr] + y - src_label = self.get_label(src.name) - dest_label = self.get_label(dest.ref.name) - self.emitter.emit(LDA(Absolute(src_label))) - self.emitter.emit(STA(IndirectY(dest_label))) - else: - raise NotImplementedError((src, dest)) - else: - raise NotImplementedError((src, dest)) + if isinstance(src, ConstantRef) and isinstance(dest, IndirectRef) and src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType): + ### copy 123, [ptr] + y + dest_label = self.get_label(dest.ref.name) + self.emitter.emit(LDA(Immediate(Byte(src.value)))) + self.emitter.emit(STA(IndirectY(dest_label))) + elif isinstance(src, LocationRef) and isinstance(dest, IndirectRef) and src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType): + ### copy b, [ptr] + y + src_label = self.get_label(src.name) + dest_label = self.get_label(dest.ref.name) + self.emitter.emit(LDA(Absolute(src_label))) + self.emitter.emit(STA(IndirectY(dest_label))) elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef) and dest.type == TYPE_BYTE and isinstance(src.ref.type, PointerType): ### copy [ptr] + y, b src_label = self.get_label(src.ref.name) From 2fdba72959bec18c80da90ea19788b7e7b8a43c7 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 6 Mar 2018 10:32:52 +0000 Subject: [PATCH 20/35] Re-organize the example programs. --- eg/{proto-game.60p => c64/demo-game.60p} | 0 eg/{ => c64}/intr1.60p | 0 eg/{ => c64}/joystick.60p | 0 eg/{ => c64}/screen1.60p | 0 eg/{ => c64}/screen2.60p | 2 +- eg/rudiments/README.md | 9 +++++++++ eg/{ => rudiments}/add-fail.60p | 0 eg/{ => rudiments}/add-pass.60p | 0 eg/{ => rudiments}/add-word.60p | 0 eg/{ => rudiments}/bad-vector.60p | 0 eg/{ => rudiments}/buffer.60p | 0 eg/{ => rudiments}/call.60p | 0 eg/{ => rudiments}/conditional.60p | 0 eg/{ => rudiments}/conditional2.60p | 0 eg/{ => rudiments}/copy.60p | 0 eg/{ => rudiments}/example.60p | 0 eg/{ => rudiments}/forever.60p | 0 eg/{ => rudiments}/goto.60p | 0 eg/{ => rudiments}/if.60p | 0 eg/{loop.p60 => rudiments/loop.60p} | 0 eg/{memloc.p60 => rudiments/memloc.60p} | 0 eg/{ => rudiments}/new-style-routine.60p | 0 eg/{ => rudiments}/print.60p | 0 eg/rudiments/range-error.60p | 9 +++++++++ eg/{ => rudiments}/vector-table.60p | 0 eg/{ => rudiments}/vector.60p | 0 eg/{ => rudiments}/word-table.60p | 0 27 files changed, 19 insertions(+), 1 deletion(-) rename eg/{proto-game.60p => c64/demo-game.60p} (100%) rename eg/{ => c64}/intr1.60p (100%) rename eg/{ => c64}/joystick.60p (100%) rename eg/{ => c64}/screen1.60p (100%) rename eg/{ => c64}/screen2.60p (93%) create mode 100644 eg/rudiments/README.md rename eg/{ => rudiments}/add-fail.60p (100%) rename eg/{ => rudiments}/add-pass.60p (100%) rename eg/{ => rudiments}/add-word.60p (100%) rename eg/{ => rudiments}/bad-vector.60p (100%) rename eg/{ => rudiments}/buffer.60p (100%) rename eg/{ => rudiments}/call.60p (100%) rename eg/{ => rudiments}/conditional.60p (100%) rename eg/{ => rudiments}/conditional2.60p (100%) rename eg/{ => rudiments}/copy.60p (100%) rename eg/{ => rudiments}/example.60p (100%) rename eg/{ => rudiments}/forever.60p (100%) rename eg/{ => rudiments}/goto.60p (100%) rename eg/{ => rudiments}/if.60p (100%) rename eg/{loop.p60 => rudiments/loop.60p} (100%) rename eg/{memloc.p60 => rudiments/memloc.60p} (100%) rename eg/{ => rudiments}/new-style-routine.60p (100%) rename eg/{ => rudiments}/print.60p (100%) create mode 100644 eg/rudiments/range-error.60p rename eg/{ => rudiments}/vector-table.60p (100%) rename eg/{ => rudiments}/vector.60p (100%) rename eg/{ => rudiments}/word-table.60p (100%) diff --git a/eg/proto-game.60p b/eg/c64/demo-game.60p similarity index 100% rename from eg/proto-game.60p rename to eg/c64/demo-game.60p diff --git a/eg/intr1.60p b/eg/c64/intr1.60p similarity index 100% rename from eg/intr1.60p rename to eg/c64/intr1.60p diff --git a/eg/joystick.60p b/eg/c64/joystick.60p similarity index 100% rename from eg/joystick.60p rename to eg/c64/joystick.60p diff --git a/eg/screen1.60p b/eg/c64/screen1.60p similarity index 100% rename from eg/screen1.60p rename to eg/c64/screen1.60p diff --git a/eg/screen2.60p b/eg/c64/screen2.60p similarity index 93% rename from eg/screen2.60p rename to eg/c64/screen2.60p index eb45824..43df36d 100644 --- a/eg/screen2.60p +++ b/eg/c64/screen2.60p @@ -1,7 +1,7 @@ // Displays 256 hearts at the top of the Commodore 64's screen. // Define where the screen starts in memory: -byte table screen @ 1024 +byte table[256] screen @ 1024 routine main // These are the values that will be written to by this routine: diff --git a/eg/rudiments/README.md b/eg/rudiments/README.md new file mode 100644 index 0000000..c561673 --- /dev/null +++ b/eg/rudiments/README.md @@ -0,0 +1,9 @@ +This directory contains example sources which demonstrate +the rudiments of SixtyPical. + +Some are meant to fail and produce an error message. + +They are not meant to be specific to any architecture, but +many do assume the existence of a routine at 65490 which +outputs the value of the accumulator as an ASCII character, +simply for the purposes of producing some observable output. diff --git a/eg/add-fail.60p b/eg/rudiments/add-fail.60p similarity index 100% rename from eg/add-fail.60p rename to eg/rudiments/add-fail.60p diff --git a/eg/add-pass.60p b/eg/rudiments/add-pass.60p similarity index 100% rename from eg/add-pass.60p rename to eg/rudiments/add-pass.60p diff --git a/eg/add-word.60p b/eg/rudiments/add-word.60p similarity index 100% rename from eg/add-word.60p rename to eg/rudiments/add-word.60p diff --git a/eg/bad-vector.60p b/eg/rudiments/bad-vector.60p similarity index 100% rename from eg/bad-vector.60p rename to eg/rudiments/bad-vector.60p diff --git a/eg/buffer.60p b/eg/rudiments/buffer.60p similarity index 100% rename from eg/buffer.60p rename to eg/rudiments/buffer.60p diff --git a/eg/call.60p b/eg/rudiments/call.60p similarity index 100% rename from eg/call.60p rename to eg/rudiments/call.60p diff --git a/eg/conditional.60p b/eg/rudiments/conditional.60p similarity index 100% rename from eg/conditional.60p rename to eg/rudiments/conditional.60p diff --git a/eg/conditional2.60p b/eg/rudiments/conditional2.60p similarity index 100% rename from eg/conditional2.60p rename to eg/rudiments/conditional2.60p diff --git a/eg/copy.60p b/eg/rudiments/copy.60p similarity index 100% rename from eg/copy.60p rename to eg/rudiments/copy.60p diff --git a/eg/example.60p b/eg/rudiments/example.60p similarity index 100% rename from eg/example.60p rename to eg/rudiments/example.60p diff --git a/eg/forever.60p b/eg/rudiments/forever.60p similarity index 100% rename from eg/forever.60p rename to eg/rudiments/forever.60p diff --git a/eg/goto.60p b/eg/rudiments/goto.60p similarity index 100% rename from eg/goto.60p rename to eg/rudiments/goto.60p diff --git a/eg/if.60p b/eg/rudiments/if.60p similarity index 100% rename from eg/if.60p rename to eg/rudiments/if.60p diff --git a/eg/loop.p60 b/eg/rudiments/loop.60p similarity index 100% rename from eg/loop.p60 rename to eg/rudiments/loop.60p diff --git a/eg/memloc.p60 b/eg/rudiments/memloc.60p similarity index 100% rename from eg/memloc.p60 rename to eg/rudiments/memloc.60p diff --git a/eg/new-style-routine.60p b/eg/rudiments/new-style-routine.60p similarity index 100% rename from eg/new-style-routine.60p rename to eg/rudiments/new-style-routine.60p diff --git a/eg/print.60p b/eg/rudiments/print.60p similarity index 100% rename from eg/print.60p rename to eg/rudiments/print.60p diff --git a/eg/rudiments/range-error.60p b/eg/rudiments/range-error.60p new file mode 100644 index 0000000..32b61a1 --- /dev/null +++ b/eg/rudiments/range-error.60p @@ -0,0 +1,9 @@ +byte table[8] message : "WHAT?" + +routine main + inputs message + outputs x, a, z, n +{ + ld x, 9 + ld a, message + x +} diff --git a/eg/vector-table.60p b/eg/rudiments/vector-table.60p similarity index 100% rename from eg/vector-table.60p rename to eg/rudiments/vector-table.60p diff --git a/eg/vector.60p b/eg/rudiments/vector.60p similarity index 100% rename from eg/vector.60p rename to eg/rudiments/vector.60p diff --git a/eg/word-table.60p b/eg/rudiments/word-table.60p similarity index 100% rename from eg/word-table.60p rename to eg/rudiments/word-table.60p From 9ad34ed34f09175bdda810941bef903d711cb28a Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 6 Mar 2018 10:43:23 +0000 Subject: [PATCH 21/35] Refine the AST classes even more. --- src/sixtypical/analyzer.py | 43 ++++++++++++-------------- src/sixtypical/ast.py | 14 ++++++--- src/sixtypical/compiler.py | 62 +++++++++++++++++++------------------- src/sixtypical/parser.py | 8 ++--- 4 files changed, 64 insertions(+), 63 deletions(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 9c9589e..9f07485 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -1,6 +1,6 @@ # encoding: UTF-8 -from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, BlockOp, IfOp +from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, If, Repeat, WithInterruptsOff from sixtypical.model import ( TYPE_BYTE, TYPE_WORD, TableType, BufferType, PointerType, VectorType, RoutineType, @@ -313,11 +313,13 @@ class Analyzer(object): def analyze_instr(self, instr, context): if isinstance(instr, SingleOp): - return self.analyze_single_op(instr, context) - elif isinstance(instr, BlockOp): - return self.analyze_block_op(instr, context) - elif isinstance(instr, IfOp): - return self.analyze_if_op(instr, context) + self.analyze_single_op(instr, context) + elif isinstance(instr, If): + self.analyze_if(instr, context) + elif isinstance(instr, Repeat): + self.analyze_repeat(instr, context) + elif isinstance(instr, WithInterruptsOff): + self.analyze_block(instr.block, context) else: raise NotImplementedError @@ -547,7 +549,7 @@ class Analyzer(object): else: raise NotImplementedError(opcode) - def analyze_if_op(self, instr, context): + def analyze_if(self, instr, context): incoming_meaningful = set(context.each_meaningful()) context1 = context.clone() @@ -586,20 +588,15 @@ class Analyzer(object): context.set_touched(ref) context.set_unmeaningful(ref) - def analyze_block_op(self, instr, context): - if instr.opcode == 'repeat': - # it will always be executed at least once, so analyze it having - # been executed the first time. - self.analyze_block(instr.block, context) - if instr.src is not None: # None indicates 'repeat forever' - context.assert_meaningful(instr.src) + def analyze_repeat(self, instr, context): + # it will always be executed at least once, so analyze it having + # been executed the first time. + self.analyze_block(instr.block, context) + if instr.src is not None: # None indicates 'repeat forever' + context.assert_meaningful(instr.src) - # now analyze it having been executed a second time, with the context - # of it having already been executed. - self.analyze_block(instr.block, context) - if instr.src is not None: - context.assert_meaningful(instr.src) - elif instr.opcode == 'with-sei': - self.analyze_block(instr.block, context) - else: - raise NotImplementedError(opcode) + # now analyze it having been executed a second time, with the context + # of it having already been executed. + self.analyze_block(instr.block, context) + if instr.src is not None: + context.assert_meaningful(instr.src) diff --git a/src/sixtypical/ast.py b/src/sixtypical/ast.py index 075742f..d4ab321 100644 --- a/src/sixtypical/ast.py +++ b/src/sixtypical/ast.py @@ -72,11 +72,15 @@ class SingleOp(Instr): value_attrs = ('opcode', 'dest', 'src', 'location',) -class BlockOp(Instr): - value_attrs = ('opcode', 'src', 'inverted') +class If(Instr): + value_attrs = ('src', 'inverted') + child_attrs = ('block1', 'block2',) + + +class Repeat(Instr): + value_attrs = ('src', 'inverted') child_attrs = ('block',) -class IfOp(Instr): - value_attrs = ('src', 'inverted') - child_attrs = ('block1', 'block2',) +class WithInterruptsOff(Instr): + child_attrs = ('block',) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index 55e64c9..7e160c3 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -1,6 +1,6 @@ # encoding: UTF-8 -from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, BlockOp, IfOp +from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, If, Repeat, WithInterruptsOff from sixtypical.model import ( ConstantRef, LocationRef, IndexedRef, IndirectRef, AddressRef, TYPE_BIT, TYPE_BYTE, TYPE_WORD, @@ -144,10 +144,12 @@ class Compiler(object): def compile_instr(self, instr): if isinstance(instr, SingleOp): return self.compile_single_op(instr) - elif isinstance(instr, BlockOp): - return self.compile_block_op(instr) - elif isinstance(instr, IfOp): - return self.compile_if_op(instr) + elif isinstance(instr, If): + return self.compile_if(instr) + elif isinstance(instr, Repeat): + return self.compile_repeat(instr) + elif isinstance(instr, WithInterruptsOff): + return self.compile_with_interrupts_off(instr) else: raise NotImplementedError @@ -493,7 +495,7 @@ class Compiler(object): else: raise NotImplementedError(src.type) - def compile_if_op(self, instr): + def compile_if(self, instr): cls = { False: { 'c': BCC, @@ -518,29 +520,27 @@ class Compiler(object): else: self.emitter.resolve_label(else_label) - def compile_block_op(self, instr): - if instr.opcode == 'repeat': - top_label = self.emitter.make_label() - self.compile_block(instr.block) - if instr.src is None: # indicates 'repeat forever' - self.emitter.emit(JMP(Absolute(top_label))) - else: - cls = { - False: { - 'c': BCC, - 'z': BNE, - }, - True: { - 'c': BCS, - 'z': BEQ, - }, - }[instr.inverted].get(instr.src.name) - if cls is None: - raise UnsupportedOpcodeError(instr) - self.emitter.emit(cls(Relative(top_label))) - elif instr.opcode == 'with-sei': - self.emitter.emit(SEI()) - self.compile_block(instr.block) - self.emitter.emit(CLI()) + def compile_repeat(self, instr): + top_label = self.emitter.make_label() + self.compile_block(instr.block) + if instr.src is None: # indicates 'repeat forever' + self.emitter.emit(JMP(Absolute(top_label))) else: - raise NotImplementedError(opcode) + cls = { + False: { + 'c': BCC, + 'z': BNE, + }, + True: { + 'c': BCS, + 'z': BEQ, + }, + }[instr.inverted].get(instr.src.name) + if cls is None: + raise UnsupportedOpcodeError(instr) + self.emitter.emit(cls(Relative(top_label))) + + def compile_with_interrupts_off(self, instr): + self.emitter.emit(SEI()) + self.compile_block(instr.block) + self.emitter.emit(CLI()) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index aedab41..6b3ec84 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -1,6 +1,6 @@ # encoding: UTF-8 -from sixtypical.ast import Program, Defn, Routine, Block, SingleOp, BlockOp, IfOp +from sixtypical.ast import Program, Defn, Routine, Block, SingleOp, If, Repeat, WithInterruptsOff from sixtypical.model import ( TYPE_BIT, TYPE_BYTE, TYPE_WORD, RoutineType, VectorType, TableType, BufferType, PointerType, @@ -355,7 +355,7 @@ class Parser(object): block2 = None if self.scanner.consume('else'): block2 = self.block() - return IfOp(src=src, block1=block1, block2=block2, inverted=inverted) + return If(src=src, block1=block1, block2=block2, inverted=inverted) elif self.scanner.consume('repeat'): inverted = False src = None @@ -366,7 +366,7 @@ class Parser(object): src = self.locexpr() else: self.scanner.expect('forever') - return BlockOp(opcode='repeat', src=src, block=block, inverted=inverted) + return Repeat(src=src, block=block, inverted=inverted) 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 @@ -415,7 +415,7 @@ class Parser(object): self.scanner.expect("interrupts") self.scanner.expect("off") block = self.block() - return BlockOp(opcode='with-sei', src=None, block=block) + return WithInterruptsOff(block=block) elif self.scanner.consume("trash"): dest = self.locexpr() return SingleOp(opcode='trash', src=None, dest=dest) From e44c8023143caafdb7b402ec98767220f127d706 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 6 Mar 2018 12:23:57 +0000 Subject: [PATCH 22/35] Syntax errors have line numbers in them now. --- eg/rudiments/word-table.60p | 2 +- src/sixtypical/ast.py | 3 +- src/sixtypical/parser.py | 65 +++++++++++++++++++----------------- src/sixtypical/scanner.py | 22 +++++++++--- tests/SixtyPical Analysis.md | 2 +- 5 files changed, 56 insertions(+), 38 deletions(-) diff --git a/eg/rudiments/word-table.60p b/eg/rudiments/word-table.60p index a0dc978..ef40dd8 100644 --- a/eg/rudiments/word-table.60p +++ b/eg/rudiments/word-table.60p @@ -1,5 +1,5 @@ word one -word table many +word table[256] many routine main inputs one, many diff --git a/src/sixtypical/ast.py b/src/sixtypical/ast.py index d4ab321..0ab9f33 100644 --- a/src/sixtypical/ast.py +++ b/src/sixtypical/ast.py @@ -5,7 +5,8 @@ class AST(object): child_attrs = () value_attrs = () - def __init__(self, **kwargs): + def __init__(self, line_number, **kwargs): + self.line_number = line_number self.attrs = {} for attr in self.children_attrs: self.attrs[attr] = kwargs.pop(attr, []) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 6b3ec84..4caf862 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -6,7 +6,7 @@ from sixtypical.model import ( RoutineType, VectorType, TableType, BufferType, PointerType, LocationRef, ConstantRef, IndirectRef, IndexedRef, AddressRef, ) -from sixtypical.scanner import Scanner +from sixtypical.scanner import Scanner, SixtyPicalSyntaxError class SymEntry(object): @@ -30,6 +30,9 @@ class Parser(object): self.symbols[token] = SymEntry(None, LocationRef(TYPE_BIT, token)) self.backpatch_instrs = [] + def syntax_error(self, msg): + raise SixtyPicalSyntaxError(self.scanner.line_number, msg) + def soft_lookup(self, name): if name in self.current_statics: return self.current_statics[name].model @@ -40,7 +43,7 @@ class Parser(object): def lookup(self, name): model = self.soft_lookup(name) if model is None: - raise SyntaxError('Undefined symbol "%s"' % name) + self.syntax_error('Undefined symbol "{}"'.format(name)) return model # --- grammar productions @@ -56,7 +59,7 @@ class Parser(object): defn = self.defn() name = defn.name if name in self.symbols: - raise SyntaxError('Symbol "%s" already declared' % name) + self.syntax_error('Symbol "%s" already declared' % name) self.symbols[name] = SymEntry(defn, defn.location) defns.append(defn) while self.scanner.on('define', 'routine'): @@ -68,7 +71,7 @@ class Parser(object): routine = self.legacy_routine() name = routine.name if name in self.symbols: - raise SyntaxError('Symbol "%s" already declared' % name) + self.syntax_error('Symbol "%s" already declared' % name) self.symbols[name] = SymEntry(routine, routine.location) routines.append(routine) self.scanner.check_type('EOF') @@ -84,26 +87,26 @@ class Parser(object): if instr.opcode in ('call', 'goto'): name = instr.location if name not in self.symbols: - raise SyntaxError('Undefined routine "%s"' % name) + self.syntax_error('Undefined routine "%s"' % name) if not isinstance(self.symbols[name].model.type, (RoutineType, VectorType)): - raise SyntaxError('Illegal call of non-executable "%s"' % name) + self.syntax_error('Illegal call of non-executable "%s"' % name) instr.location = self.symbols[name].model if instr.opcode in ('copy',) and isinstance(instr.src, basestring): name = instr.src if name not in self.symbols: - raise SyntaxError('Undefined routine "%s"' % name) + self.syntax_error('Undefined routine "%s"' % name) if not isinstance(self.symbols[name].model.type, (RoutineType, VectorType)): - raise SyntaxError('Illegal copy of non-executable "%s"' % name) + self.syntax_error('Illegal copy of non-executable "%s"' % name) instr.src = self.symbols[name].model - return Program(defns=defns, routines=routines) + return Program(self.scanner.line_number, defns=defns, routines=routines) def typedef(self): self.scanner.expect('typedef') type_ = self.defn_type() name = self.defn_name() if name in self.typedefs: - raise SyntaxError('Type "%s" already declared' % name) + self.syntax_error('Type "%s" already declared' % name) self.typedefs[name] = type_ return type_ @@ -127,11 +130,11 @@ class Parser(object): self.scanner.scan() if initial is not None and addr is not None: - raise SyntaxError("Definition cannot have both initial value and explicit address") + self.syntax_error("Definition cannot have both initial value and explicit address") location = LocationRef(type_, name) - return Defn(name=name, addr=addr, initial=initial, location=location) + return Defn(self.scanner.line_number, name=name, addr=addr, initial=initial, location=location) def defn_size(self): self.scanner.expect('[') @@ -147,7 +150,7 @@ class Parser(object): if self.scanner.consume('table'): size = self.defn_size() if size <= 0 or size > 256: - raise SyntaxError("Table size must be > 0 and <= 256") + self.syntax_error("Table size must be > 0 and <= 256") type_ = TableType(type_, size) return type_ @@ -167,7 +170,7 @@ class Parser(object): elif self.scanner.consume('vector'): type_ = self.defn_type_term() if not isinstance(type_, RoutineType): - raise SyntaxError("Vectors can only be of a routine, not %r" % type_) + self.syntax_error("Vectors can only be of a routine, not %r" % type_) type_ = VectorType(type_) elif self.scanner.consume('routine'): (inputs, outputs, trashes) = self.constraints() @@ -181,7 +184,7 @@ class Parser(object): type_name = self.scanner.token self.scanner.scan() if type_name not in self.typedefs: - raise SyntaxError("Undefined type '%s'" % type_name) + self.syntax_error("Undefined type '%s'" % type_name) type_ = self.typedefs[type_name] return type_ @@ -220,6 +223,7 @@ class Parser(object): addr = None location = LocationRef(type_, name) return Routine( + self.scanner.line_number, name=name, block=block, addr=addr, location=location ) @@ -227,7 +231,7 @@ class Parser(object): def routine(self, name): type_ = self.defn_type() if not isinstance(type_, RoutineType): - raise SyntaxError("Can only define a routine, not %r" % type_) + self.syntax_error("Can only define a routine, not %r" % type_) statics = [] if self.scanner.consume('@'): self.scanner.check_type('integer literal') @@ -244,6 +248,7 @@ class Parser(object): addr = None location = LocationRef(type_, name) return Routine( + self.scanner.line_number, name=name, block=block, addr=addr, location=location, statics=statics ) @@ -253,7 +258,7 @@ class Parser(object): for defn in statics: name = defn.name if name in self.symbols or name in self.current_statics: - raise SyntaxError('Symbol "%s" already declared' % name) + self.syntax_error('Symbol "%s" already declared' % name) c[name] = SymEntry(defn, defn.location) return c @@ -333,7 +338,7 @@ class Parser(object): while self.scanner.consume('static'): defn = self.defn() if defn.initial is None: - raise SyntaxError("Static definition {} must have initial value".format(defn)) + self.syntax_error("Static definition {} must have initial value".format(defn)) defns.append(defn) return defns @@ -343,7 +348,7 @@ class Parser(object): while not self.scanner.on('}'): instrs.append(self.instr()) self.scanner.expect('}') - return Block(instrs=instrs) + return Block(self.scanner.line_number, instrs=instrs) def instr(self): if self.scanner.consume('if'): @@ -355,7 +360,7 @@ class Parser(object): block2 = None if self.scanner.consume('else'): block2 = self.block() - return If(src=src, block1=block1, block2=block2, inverted=inverted) + return If(self.scanner.line_number, src=src, block1=block1, block2=block2, inverted=inverted) elif self.scanner.consume('repeat'): inverted = False src = None @@ -366,7 +371,7 @@ class Parser(object): src = self.locexpr() else: self.scanner.expect('forever') - return Repeat(src=src, block=block, inverted=inverted) + return Repeat(self.scanner.line_number, src=src, block=block, inverted=inverted) 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 @@ -374,32 +379,32 @@ class Parser(object): dest = self.locexpr() self.scanner.expect(',') src = self.indlocexpr() - return SingleOp(opcode=opcode, dest=dest, src=src) + return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src) elif self.scanner.token in ("add", "sub", "cmp", "and", "or", "xor"): opcode = self.scanner.token self.scanner.scan() dest = self.locexpr() self.scanner.expect(',') src = self.indexed_locexpr() - return SingleOp(opcode=opcode, dest=dest, src=src) + return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src) elif self.scanner.token in ("st",): opcode = self.scanner.token self.scanner.scan() src = self.locexpr() self.scanner.expect(',') dest = self.indlocexpr() - return SingleOp(opcode=opcode, dest=dest, src=src) + return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src) elif self.scanner.token in ("shl", "shr", "inc", "dec"): opcode = self.scanner.token self.scanner.scan() dest = self.locexpr() - return SingleOp(opcode=opcode, dest=dest, src=None) + return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=None) elif self.scanner.token in ("call", "goto"): opcode = self.scanner.token self.scanner.scan() name = self.scanner.token self.scanner.scan() - instr = SingleOp(opcode=opcode, location=name, dest=None, src=None) + instr = SingleOp(self.scanner.line_number, opcode=opcode, location=name, dest=None, src=None) self.backpatch_instrs.append(instr) return instr elif self.scanner.token in ("copy",): @@ -408,16 +413,16 @@ class Parser(object): src = self.indlocexpr(forward=True) self.scanner.expect(',') dest = self.indlocexpr() - instr = SingleOp(opcode=opcode, dest=dest, src=src) + instr = SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src) self.backpatch_instrs.append(instr) return instr elif self.scanner.consume("with"): self.scanner.expect("interrupts") self.scanner.expect("off") block = self.block() - return WithInterruptsOff(block=block) + return WithInterruptsOff(self.scanner.line_number, block=block) elif self.scanner.consume("trash"): dest = self.locexpr() - return SingleOp(opcode='trash', src=None, dest=dest) + return SingleOp(self.scanner.line_number, opcode='trash', src=None, dest=dest) else: - raise ValueError('bad opcode "%s"' % self.scanner.token) + self.syntax_error('bad opcode "%s"' % self.scanner.token) diff --git a/src/sixtypical/scanner.py b/src/sixtypical/scanner.py index fd48b3b..72bca62 100644 --- a/src/sixtypical/scanner.py +++ b/src/sixtypical/scanner.py @@ -3,11 +3,20 @@ import re +class SixtyPicalSyntaxError(ValueError): + def __init__(self, line_number, message): + super(SixtyPicalSyntaxError, self).__init__(line_number, message) + + def __str__(self): + return "Line {}: {}".format(self.args[0], self.args[1]) + + class Scanner(object): def __init__(self, text): self.text = text self.token = None self.type = None + self.line_number = 1 self.scan() def scan_pattern(self, pattern, type, token_group=1, rest_group=2): @@ -19,6 +28,7 @@ class Scanner(object): self.type = type self.token = match.group(token_group) self.text = match.group(rest_group) + self.line_number += self.token.count('\n') return True def scan(self): @@ -46,14 +56,15 @@ class Scanner(object): if self.scan_pattern(r'.', 'unknown character'): return else: - raise AssertionError("this should never happen, self.text=(%s)" % self.text) + raise AssertionError("this should never happen, self.text=({})".format(self.text)) def expect(self, token): if self.token == token: self.scan() else: - raise SyntaxError("Expected '%s', but found '%s'" % - (token, self.token)) + raise SixtyPicalSyntaxError(self.scanner.line_number, "Expected '{}', but found '{}'".format( + token, self.token + )) def on(self, *tokens): return self.token in tokens @@ -63,8 +74,9 @@ class Scanner(object): def check_type(self, type): if not self.type == type: - raise SyntaxError("Expected %s, but found %s ('%s')" % - (type, self.type, self.token)) + raise SixtyPicalSyntaxError(self.scanner.line_number, "Expected {}, but found '{}'".format( + self.type, self.token + )) def consume(self, token): if self.token == token: diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index 4cd21c6..84111c8 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -2323,7 +2323,7 @@ A vector in a vector table cannot be directly called. | copy bar, many + x | call many + x | } - ? ValueError + ? SyntaxError ### typedef ### From 42cbd73a2021a22d08ebefaa5e244fc26606c779 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 6 Mar 2018 13:13:18 +0000 Subject: [PATCH 23/35] Show line numbers in static analysis errors (clumsily.) --- src/sixtypical/analyzer.py | 56 ++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 9f07485..07ec9b4 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -10,7 +10,11 @@ from sixtypical.model import ( class StaticAnalysisError(ValueError): - pass + def __init__(self, line_number, message): + super(StaticAnalysisError, self).__init__(line_number, message) + + def __str__(self): + return "{} (Line {})".format(self.args[1], self.args[0]) class UnmeaningfulReadError(StaticAnalysisError): @@ -94,19 +98,19 @@ class Context(object): for ref in inputs: if ref.is_constant(): - raise ConstantConstraintError('%s in %s' % (ref.name, routine.name)) + raise ConstantConstraintError(self.routine.line_number, '%s in %s' % (ref.name, routine.name)) self._range[ref] = ref.max_range() output_names = set() for ref in outputs: if ref.is_constant(): - raise ConstantConstraintError('%s in %s' % (ref.name, routine.name)) + raise ConstantConstraintError(self.routine.line_number, '%s in %s' % (ref.name, routine.name)) output_names.add(ref.name) self._writeable.add(ref) for ref in trashes: if ref.is_constant(): - raise ConstantConstraintError('%s in %s' % (ref.name, routine.name)) + raise ConstantConstraintError(self.routine.line_number, '%s in %s' % (ref.name, routine.name)) if ref.name in output_names: - raise InconsistentConstraintsError('%s in %s' % (ref.name, routine.name)) + raise InconsistentConstraintsError(self.routine.line_number, '%s in %s' % (ref.name, routine.name)) self._writeable.add(ref) def __str__(self): @@ -142,7 +146,7 @@ class Context(object): message = '%s in %s' % (ref.name, self.routine.name) if kwargs.get('message'): message += ' (%s)' % kwargs['message'] - raise exception_class(message) + raise exception_class(self.routine.line_number, message) elif isinstance(ref, IndexedRef): self.assert_meaningful(ref.ref, **kwargs) self.assert_meaningful(ref.index, **kwargs) @@ -159,7 +163,7 @@ class Context(object): message = '%s in %s' % (ref.name, self.routine.name) if kwargs.get('message'): message += ' (%s)' % kwargs['message'] - raise exception_class(message) + raise exception_class(self.routine.line_number, message) def assert_in_range(self, inside, outside): # FIXME there's a bit of I'm-not-sure-the-best-way-to-do-this-ness, here... @@ -176,7 +180,7 @@ class Context(object): outside_range = (0, outside.type.size-1) if inside_range[0] < outside_range[0] or inside_range[1] > outside_range[1]: - raise RangeExceededError( + raise RangeExceededError(self.routine.line_number, "Possible range of {} {} exceeds acceptable range of {} {}".format( inside, inside_range, outside, outside_range ) @@ -236,7 +240,7 @@ class Analyzer(object): def assert_type(self, type, *locations): for location in locations: if location.type != type: - raise TypeMismatchError('%s in %s' % + raise TypeMismatchError(self.current_routine.line_number, '%s in %s' % (location.name, self.current_routine.name) ) @@ -293,7 +297,7 @@ class Analyzer(object): # even if we goto another routine, we can't trash an output. for ref in trashed: if ref in type_.outputs: - raise UnmeaningfulOutputError('%s in %s' % (ref.name, routine.name)) + raise UnmeaningfulOutputError(routine.line_number, '%s in %s' % (ref.name, routine.name)) if not self.has_encountered_goto: for ref in type_.outputs: @@ -301,7 +305,7 @@ class Analyzer(object): for ref in context.each_touched(): if ref not in type_.outputs and ref not in type_.trashes and not routine_has_static(routine, ref): message = '%s in %s' % (ref.name, routine.name) - raise ForbiddenWriteError(message) + raise ForbiddenWriteError(routine.line_number, message) self.current_routine = None def analyze_block(self, block, context): @@ -334,7 +338,7 @@ class Analyzer(object): if TableType.is_a_table_type(src.ref.type, TYPE_BYTE) and dest.type == TYPE_BYTE: pass else: - raise TypeMismatchError('%s and %s in %s' % + raise TypeMismatchError(instr.line_number, '%s and %s in %s' % (src.ref.name, dest.name, self.current_routine.name) ) context.assert_meaningful(src, src.index) @@ -344,10 +348,10 @@ class Analyzer(object): if isinstance(src.ref.type, PointerType) and dest.type == TYPE_BYTE: pass else: - raise TypeMismatchError((src, dest)) + raise TypeMismatchError(instr.line_number, (src, dest)) context.assert_meaningful(src.ref, REG_Y) elif src.type != dest.type: - raise TypeMismatchError('%s and %s in %s' % + raise TypeMismatchError(instr.line_number, '%s and %s in %s' % (src.name, dest.name, self.current_routine.name) ) else: @@ -359,7 +363,7 @@ class Analyzer(object): if src.type == TYPE_BYTE and TableType.is_a_table_type(dest.ref.type, TYPE_BYTE): pass else: - raise TypeMismatchError((src, dest)) + raise TypeMismatchError(instr.line_number, (src, dest)) context.assert_meaningful(dest.index) context.assert_in_range(dest.index, dest.ref) context.set_written(dest.ref) @@ -368,11 +372,11 @@ class Analyzer(object): if isinstance(dest.ref.type, PointerType) and src.type == TYPE_BYTE: pass else: - raise TypeMismatchError((src, dest)) + raise TypeMismatchError(instr.line_number, (src, dest)) context.assert_meaningful(dest.ref, REG_Y) context.set_written(dest.ref) elif src.type != dest.type: - raise TypeMismatchError('%r and %r in %s' % + raise TypeMismatchError(instr.line_number, '%r and %r in %s' % (src, dest, self.current_routine.name) ) else: @@ -443,7 +447,7 @@ class Analyzer(object): context.set_unmeaningful(ref) elif opcode == 'copy': if dest == REG_A: - raise ForbiddenWriteError("{} cannot be used as destination for copy".format(dest)) + raise ForbiddenWriteError(instr.line_number, "{} cannot be used as destination for copy".format(dest)) # 1. check that their types are compatible @@ -451,17 +455,17 @@ class Analyzer(object): if isinstance(src.ref.type, BufferType) and isinstance(dest.type, PointerType): pass else: - raise TypeMismatchError((src, dest)) + raise TypeMismatchError(instr.line_number, (src, dest)) elif isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndirectRef): if src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType): pass else: - raise TypeMismatchError((src, dest)) + raise TypeMismatchError(instr.line_number, (src, dest)) elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef): if isinstance(src.ref.type, PointerType) and dest.type == TYPE_BYTE: pass else: - raise TypeMismatchError((src, dest)) + raise TypeMismatchError(instr.line_number, (src, dest)) elif isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndexedRef): if src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD): @@ -473,7 +477,7 @@ class Analyzer(object): RoutineType.executable_types_compatible(src.type, dest.ref.type.of_type)): pass else: - raise TypeMismatchError((src, dest)) + raise TypeMismatchError(instr.line_number, (src, dest)) context.assert_in_range(dest.index, dest.ref) elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef): @@ -483,7 +487,7 @@ class Analyzer(object): RoutineType.executable_types_compatible(src.ref.type.of_type, dest.type.of_type)): pass else: - raise TypeMismatchError((src, dest)) + raise TypeMismatchError(instr.line_number, (src, dest)) context.assert_in_range(src.index, src.ref) elif isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, LocationRef): @@ -494,9 +498,9 @@ class Analyzer(object): self.assert_affected_within('outputs', src.type, dest.type.of_type) self.assert_affected_within('trashes', src.type, dest.type.of_type) else: - raise TypeMismatchError((src, dest)) + raise TypeMismatchError(instr.line_number, (src, dest)) else: - raise TypeMismatchError((src, dest)) + raise TypeMismatchError(instr.line_number, (src, dest)) # 2. check that the context is meaningful @@ -528,7 +532,7 @@ class Analyzer(object): type_ = location.type if not isinstance(type_, (RoutineType, VectorType)): - raise TypeMismatchError(location) + raise TypeMismatchError(instr.line_number, location) # assert that the dest routine's inputs are all initialized if isinstance(type_, VectorType): From 7023fb9c1db1046eb92a416ca4c5c0db6ebefe28 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 6 Mar 2018 14:15:53 +0000 Subject: [PATCH 24/35] Update notes. --- HISTORY.md | 3 +++ README.md | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index a9f9b14..c4f6e9e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -8,6 +8,9 @@ History of SixtyPical to a table falls within the defined size of that table. * The reference analyzer's ability to prove this is currently fairly weak, but it does exist. +* Cleaned up the internals of the reference implementation (incl. the AST) + and re-organized the example programs in the `eg` subdirectory. +* Most errors produced by the reference implementation now include a line number. 0.12 ---- diff --git a/README.md b/README.md index fe63f7e..34ddc1d 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,6 @@ is probably NP-complete. But doing it adeuqately is probably not that hard. * `static` pointers -- currently not possible because pointers must be zero-page, thus `@`, thus uninitialized. * Question the value of the "consistent initialization" principle for `if` statement analysis. * `interrupt` routines -- to indicate that "the supervisor" has stored values on the stack, so we can trash them. -* Error messages that include the line number of the source code. * Add absolute addressing in shl/shr, absolute-indexed for add, sub, etc. * Automatic tail-call optimization (could be tricky, w/constraints?) * Possibly `ld x, [ptr] + y`, possibly `st x, [ptr] + y`. From 1eaec602e3122de750bc86118c2b395370160d99 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 6 Mar 2018 14:39:47 +0000 Subject: [PATCH 25/35] The exception object now contains an AST node, renders name. --- src/sixtypical/analyzer.py | 82 +++++++++++----------- tests/SixtyPical Analysis.md | 130 +++++++++++++++++------------------ 2 files changed, 104 insertions(+), 108 deletions(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 07ec9b4..d3b977b 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -10,11 +10,16 @@ from sixtypical.model import ( class StaticAnalysisError(ValueError): - def __init__(self, line_number, message): - super(StaticAnalysisError, self).__init__(line_number, message) + def __init__(self, ast, message): + super(StaticAnalysisError, self).__init__(ast, message) def __str__(self): - return "{} (Line {})".format(self.args[1], self.args[0]) + ast = self.args[0] + message = self.args[1] + if isinstance(ast, Routine): + return "{} (in {}, line {})".format(message, ast.name, ast.line_number) + else: + return "{} (line {})".format(message, ast.line_number) class UnmeaningfulReadError(StaticAnalysisError): @@ -98,19 +103,19 @@ class Context(object): for ref in inputs: if ref.is_constant(): - raise ConstantConstraintError(self.routine.line_number, '%s in %s' % (ref.name, routine.name)) + raise ConstantConstraintError(self.routine, ref.name) self._range[ref] = ref.max_range() output_names = set() for ref in outputs: if ref.is_constant(): - raise ConstantConstraintError(self.routine.line_number, '%s in %s' % (ref.name, routine.name)) + raise ConstantConstraintError(self.routine, ref.name) output_names.add(ref.name) self._writeable.add(ref) for ref in trashes: if ref.is_constant(): - raise ConstantConstraintError(self.routine.line_number, '%s in %s' % (ref.name, routine.name)) + raise ConstantConstraintError(self.routine, ref.name) if ref.name in output_names: - raise InconsistentConstraintsError(self.routine.line_number, '%s in %s' % (ref.name, routine.name)) + raise InconsistentConstraintsError(self.routine, ref.name) self._writeable.add(ref) def __str__(self): @@ -143,10 +148,10 @@ class Context(object): pass elif isinstance(ref, LocationRef): if ref not in self._range: - message = '%s in %s' % (ref.name, self.routine.name) + message = ref.name if kwargs.get('message'): message += ' (%s)' % kwargs['message'] - raise exception_class(self.routine.line_number, message) + raise exception_class(self.routine, message) elif isinstance(ref, IndexedRef): self.assert_meaningful(ref.ref, **kwargs) self.assert_meaningful(ref.index, **kwargs) @@ -160,10 +165,10 @@ class Context(object): if routine_has_static(self.routine, ref): continue if ref not in self._writeable: - message = '%s in %s' % (ref.name, self.routine.name) + message = ref.name if kwargs.get('message'): message += ' (%s)' % kwargs['message'] - raise exception_class(self.routine.line_number, message) + raise exception_class(self.routine, message) def assert_in_range(self, inside, outside): # FIXME there's a bit of I'm-not-sure-the-best-way-to-do-this-ness, here... @@ -180,7 +185,7 @@ class Context(object): outside_range = (0, outside.type.size-1) if inside_range[0] < outside_range[0] or inside_range[1] > outside_range[1]: - raise RangeExceededError(self.routine.line_number, + raise RangeExceededError(self.routine, "Possible range of {} {} exceeds acceptable range of {} {}".format( inside, inside_range, outside, outside_range ) @@ -240,9 +245,7 @@ class Analyzer(object): def assert_type(self, type, *locations): for location in locations: if location.type != type: - raise TypeMismatchError(self.current_routine.line_number, '%s in %s' % - (location.name, self.current_routine.name) - ) + raise TypeMismatchError(self.current_routine, location.name) def assert_affected_within(self, name, affecting_type, limiting_type): assert name in ('inputs', 'outputs', 'trashes') @@ -251,13 +254,13 @@ class Analyzer(object): overage = affected - limited_to if not overage: return - message = 'in %s: %s for %s are %s\n\nbut %s affects %s\n\nwhich exceeds it by: %s ' % ( - self.current_routine.name, name, + message = '%s for %s are %s\n\nbut %s affects %s\n\nwhich exceeds it by: %s ' % ( + name, limiting_type, LocationRef.format_set(limited_to), affecting_type, LocationRef.format_set(affected), LocationRef.format_set(overage) ) - raise IncompatibleConstraintsError(message) + raise IncompatibleConstraintsError(self.current_routine, message) def analyze_program(self, program): assert isinstance(program, Program) @@ -297,22 +300,21 @@ class Analyzer(object): # even if we goto another routine, we can't trash an output. for ref in trashed: if ref in type_.outputs: - raise UnmeaningfulOutputError(routine.line_number, '%s in %s' % (ref.name, routine.name)) + raise UnmeaningfulOutputError(routine, ref.name) if not self.has_encountered_goto: for ref in type_.outputs: context.assert_meaningful(ref, exception_class=UnmeaningfulOutputError) for ref in context.each_touched(): if ref not in type_.outputs and ref not in type_.trashes and not routine_has_static(routine, ref): - message = '%s in %s' % (ref.name, routine.name) - raise ForbiddenWriteError(routine.line_number, message) + raise ForbiddenWriteError(routine, ref.name) self.current_routine = None def analyze_block(self, block, context): assert isinstance(block, Block) for i in block.instrs: if self.has_encountered_goto: - raise IllegalJumpError(i) + raise IllegalJumpError(i, i) self.analyze_instr(i, context) def analyze_instr(self, instr, context): @@ -338,9 +340,7 @@ class Analyzer(object): if TableType.is_a_table_type(src.ref.type, TYPE_BYTE) and dest.type == TYPE_BYTE: pass else: - raise TypeMismatchError(instr.line_number, '%s and %s in %s' % - (src.ref.name, dest.name, self.current_routine.name) - ) + raise TypeMismatchError(instr, '{} and {}'.format(src.ref.name, dest.name)) context.assert_meaningful(src, src.index) context.assert_in_range(src.index, src.ref) elif isinstance(src, IndirectRef): @@ -348,12 +348,10 @@ class Analyzer(object): if isinstance(src.ref.type, PointerType) and dest.type == TYPE_BYTE: pass else: - raise TypeMismatchError(instr.line_number, (src, dest)) + raise TypeMismatchError(instr, (src, dest)) context.assert_meaningful(src.ref, REG_Y) elif src.type != dest.type: - raise TypeMismatchError(instr.line_number, '%s and %s in %s' % - (src.name, dest.name, self.current_routine.name) - ) + raise TypeMismatchError(instr, '{} and {}'.format(src.name, dest.name)) else: context.assert_meaningful(src) context.copy_range(src, dest) @@ -363,7 +361,7 @@ class Analyzer(object): if src.type == TYPE_BYTE and TableType.is_a_table_type(dest.ref.type, TYPE_BYTE): pass else: - raise TypeMismatchError(instr.line_number, (src, dest)) + raise TypeMismatchError(instr, (src, dest)) context.assert_meaningful(dest.index) context.assert_in_range(dest.index, dest.ref) context.set_written(dest.ref) @@ -372,13 +370,11 @@ class Analyzer(object): if isinstance(dest.ref.type, PointerType) and src.type == TYPE_BYTE: pass else: - raise TypeMismatchError(instr.line_number, (src, dest)) + raise TypeMismatchError(instr, (src, dest)) context.assert_meaningful(dest.ref, REG_Y) context.set_written(dest.ref) elif src.type != dest.type: - raise TypeMismatchError(instr.line_number, '%r and %r in %s' % - (src, dest, self.current_routine.name) - ) + raise TypeMismatchError(instr, '{} and {}'.format(src, name)) else: context.set_written(dest) context.assert_meaningful(src) @@ -447,7 +443,7 @@ class Analyzer(object): context.set_unmeaningful(ref) elif opcode == 'copy': if dest == REG_A: - raise ForbiddenWriteError(instr.line_number, "{} cannot be used as destination for copy".format(dest)) + raise ForbiddenWriteError(instr, "{} cannot be used as destination for copy".format(dest)) # 1. check that their types are compatible @@ -455,17 +451,17 @@ class Analyzer(object): if isinstance(src.ref.type, BufferType) and isinstance(dest.type, PointerType): pass else: - raise TypeMismatchError(instr.line_number, (src, dest)) + raise TypeMismatchError(instr, (src, dest)) elif isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndirectRef): if src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType): pass else: - raise TypeMismatchError(instr.line_number, (src, dest)) + raise TypeMismatchError(instr, (src, dest)) elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef): if isinstance(src.ref.type, PointerType) and dest.type == TYPE_BYTE: pass else: - raise TypeMismatchError(instr.line_number, (src, dest)) + raise TypeMismatchError(instr, (src, dest)) elif isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndexedRef): if src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD): @@ -477,7 +473,7 @@ class Analyzer(object): RoutineType.executable_types_compatible(src.type, dest.ref.type.of_type)): pass else: - raise TypeMismatchError(instr.line_number, (src, dest)) + raise TypeMismatchError(instr, (src, dest)) context.assert_in_range(dest.index, dest.ref) elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef): @@ -487,7 +483,7 @@ class Analyzer(object): RoutineType.executable_types_compatible(src.ref.type.of_type, dest.type.of_type)): pass else: - raise TypeMismatchError(instr.line_number, (src, dest)) + raise TypeMismatchError(instr, (src, dest)) context.assert_in_range(src.index, src.ref) elif isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, LocationRef): @@ -498,9 +494,9 @@ class Analyzer(object): self.assert_affected_within('outputs', src.type, dest.type.of_type) self.assert_affected_within('trashes', src.type, dest.type.of_type) else: - raise TypeMismatchError(instr.line_number, (src, dest)) + raise TypeMismatchError(instr, (src, dest)) else: - raise TypeMismatchError(instr.line_number, (src, dest)) + raise TypeMismatchError(instr, (src, dest)) # 2. check that the context is meaningful @@ -532,7 +528,7 @@ class Analyzer(object): type_ = location.type if not isinstance(type_, (RoutineType, VectorType)): - raise TypeMismatchError(instr.line_number, location) + raise TypeMismatchError(instr, location) # assert that the dest routine's inputs are all initialized if isinstance(type_, VectorType): diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index 84111c8..42d2173 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -42,7 +42,7 @@ If a routine declares it outputs a location, that location should be initialized | { | ld x, 0 | } - ? UnmeaningfulOutputError: a in main + ? UnmeaningfulOutputError: a | routine main | inputs a @@ -73,7 +73,7 @@ If a routine modifies a location, it needs to either output it or trash it. | { | ld x, 0 | } - ? ForbiddenWriteError: x in main + ? ForbiddenWriteError: x | routine main | outputs x, z, n @@ -96,7 +96,7 @@ This is true regardless of whether it's an input or not. | { | ld x, 0 | } - ? ForbiddenWriteError: x in main + ? ForbiddenWriteError: x | routine main | inputs x @@ -127,14 +127,14 @@ If a routine trashes a location, this must be declared. | { | trash x | } - ? ForbiddenWriteError: x in foo + ? ForbiddenWriteError: x | routine foo | outputs x | { | trash x | } - ? UnmeaningfulOutputError: x in foo + ? UnmeaningfulOutputError: x If a routine causes a location to be trashed, this must be declared in the caller. @@ -162,7 +162,7 @@ If a routine causes a location to be trashed, this must be declared in the calle | { | call trash_x | } - ? ForbiddenWriteError: x in foo + ? ForbiddenWriteError: x | routine trash_x | trashes x, z, n @@ -176,7 +176,7 @@ If a routine causes a location to be trashed, this must be declared in the calle | { | call trash_x | } - ? UnmeaningfulOutputError: x in foo + ? UnmeaningfulOutputError: x (in foo, line 12) If a routine reads or writes a user-define memory location, it needs to declare that too. @@ -214,7 +214,7 @@ Can't `ld` from a memory location that isn't initialized. | { | ld a, x | } - ? UnmeaningfulReadError: x in main + ? UnmeaningfulReadError: x Can't `ld` to a memory location that doesn't appear in (outputs ∪ trashes). @@ -246,14 +246,14 @@ Can't `ld` to a memory location that doesn't appear in (outputs ∪ trashes). | { | ld a, 0 | } - ? ForbiddenWriteError: a in main + ? ForbiddenWriteError: a | routine main | trashes a, n | { | ld a, 0 | } - ? ForbiddenWriteError: z in main + ? ForbiddenWriteError: z Can't `ld` a `word` type. @@ -265,7 +265,7 @@ Can't `ld` a `word` type. | { | ld a, foo | } - ? TypeMismatchError: foo and a in main + ? TypeMismatchError: foo and a ### st ### @@ -286,7 +286,7 @@ Can't `st` from a memory location that isn't initialized. | { | st x, lives | } - ? UnmeaningfulReadError: x in main + ? UnmeaningfulReadError: x Can't `st` to a memory location that doesn't appear in (outputs ∪ trashes). @@ -312,7 +312,7 @@ Can't `st` to a memory location that doesn't appear in (outputs ∪ trashes). | { | st 0, lives | } - ? ForbiddenWriteError: lives in main + ? ForbiddenWriteError: lives Can't `st` a `word` type. @@ -646,7 +646,7 @@ Can't `add` from or to a memory location that isn't initialized. | st off, c | add a, lives | } - ? UnmeaningfulReadError: lives in main + ? UnmeaningfulReadError: lives | byte lives | routine main @@ -657,7 +657,7 @@ Can't `add` from or to a memory location that isn't initialized. | st off, c | add a, lives | } - ? UnmeaningfulReadError: a in main + ? UnmeaningfulReadError: a Can't `add` to a memory location that isn't writeable. @@ -668,7 +668,7 @@ Can't `add` to a memory location that isn't writeable. | st off, c | add a, 0 | } - ? ForbiddenWriteError: a in main + ? ForbiddenWriteError: a You can `add` a word constant to a word memory location. @@ -694,7 +694,7 @@ You can `add` a word constant to a word memory location. | st off, c | add score, 1999 | } - ? UnmeaningfulOutputError: a in main + ? UnmeaningfulOutputError: a To be sure, `add`ing a word constant to a word memory location trashes `a`. @@ -707,7 +707,7 @@ To be sure, `add`ing a word constant to a word memory location trashes `a`. | st off, c | add score, 1999 | } - ? ForbiddenWriteError: a in main + ? ForbiddenWriteError: a You can `add` a word memory location to another word memory location. @@ -735,7 +735,7 @@ You can `add` a word memory location to another word memory location. | st off, c | add score, delta | } - ? ForbiddenWriteError: a in main + ? ForbiddenWriteError: a You can `add` a word memory location, or a constant, to a pointer. @@ -765,7 +765,7 @@ You can `add` a word memory location, or a constant, to a pointer. | add ptr, delta | add ptr, word 1 | } - ? ForbiddenWriteError: a in main + ? ForbiddenWriteError: a ### sub ### @@ -790,7 +790,7 @@ Can't `sub` from or to a memory location that isn't initialized. | st off, c | sub a, lives | } - ? UnmeaningfulReadError: lives in main + ? UnmeaningfulReadError: lives | byte lives | routine main @@ -801,7 +801,7 @@ Can't `sub` from or to a memory location that isn't initialized. | st off, c | sub a, lives | } - ? UnmeaningfulReadError: a in main + ? UnmeaningfulReadError: a Can't `sub` to a memory location that isn't writeable. @@ -812,7 +812,7 @@ Can't `sub` to a memory location that isn't writeable. | st off, c | sub a, 0 | } - ? ForbiddenWriteError: a in main + ? ForbiddenWriteError: a You can `sub` a word constant from a word memory location. @@ -838,7 +838,7 @@ You can `sub` a word constant from a word memory location. | st on, c | sub score, 1999 | } - ? UnmeaningfulOutputError: a in main + ? UnmeaningfulOutputError: a You can `sub` a word memory location from another word memory location. @@ -866,7 +866,7 @@ You can `sub` a word memory location from another word memory location. | st off, c | sub score, delta | } - ? ForbiddenWriteError: a in main + ? ForbiddenWriteError: a ### inc ### @@ -878,7 +878,7 @@ Location must be initialized and writeable. | { | inc x | } - ? UnmeaningfulReadError: x in main + ? UnmeaningfulReadError: x | routine main | inputs x @@ -886,7 +886,7 @@ Location must be initialized and writeable. | { | inc x | } - ? ForbiddenWriteError: x in main + ? ForbiddenWriteError: x | routine main | inputs x @@ -908,7 +908,7 @@ Can't `inc` a `word` type. | { | inc foo | } - ? TypeMismatchError: foo in main + ? TypeMismatchError: foo ### dec ### @@ -920,7 +920,7 @@ Location must be initialized and writeable. | { | dec x | } - ? UnmeaningfulReadError: x in main + ? UnmeaningfulReadError: x | routine main | inputs x @@ -928,7 +928,7 @@ Location must be initialized and writeable. | { | dec x | } - ? ForbiddenWriteError: x in main + ? ForbiddenWriteError: x | routine main | inputs x @@ -950,7 +950,7 @@ Can't `dec` a `word` type. | { | dec foo | } - ? TypeMismatchError: foo in main + ? TypeMismatchError: foo ### cmp ### @@ -970,14 +970,14 @@ Some rudimentary tests for cmp. | { | cmp a, 4 | } - ? ForbiddenWriteError: c in main + ? ForbiddenWriteError: c | routine main | trashes z, c, n | { | cmp a, 4 | } - ? UnmeaningfulReadError: a in main + ? UnmeaningfulReadError: a ### and ### @@ -997,14 +997,14 @@ Some rudimentary tests for and. | { | and a, 4 | } - ? ForbiddenWriteError: a in main + ? ForbiddenWriteError: a | routine main | trashes z, n | { | and a, 4 | } - ? UnmeaningfulReadError: a in main + ? UnmeaningfulReadError: a ### or ### @@ -1024,14 +1024,14 @@ Writing unit tests on a train. Wow. | { | or a, 4 | } - ? ForbiddenWriteError: a in main + ? ForbiddenWriteError: a | routine main | trashes z, n | { | or a, 4 | } - ? UnmeaningfulReadError: a in main + ? UnmeaningfulReadError: a ### xor ### @@ -1051,14 +1051,14 @@ Writing unit tests on a train. Wow. | { | xor a, 4 | } - ? ForbiddenWriteError: a in main + ? ForbiddenWriteError: a | routine main | trashes z, n | { | xor a, 4 | } - ? UnmeaningfulReadError: a in main + ? UnmeaningfulReadError: a ### shl ### @@ -1078,7 +1078,7 @@ Some rudimentary tests for shl. | { | shl a | } - ? ForbiddenWriteError: a in main + ? ForbiddenWriteError: a | routine main | inputs a @@ -1086,7 +1086,7 @@ Some rudimentary tests for shl. | { | shl a | } - ? UnmeaningfulReadError: c in main + ? UnmeaningfulReadError: c ### shr ### @@ -1106,7 +1106,7 @@ Some rudimentary tests for shr. | { | shr a | } - ? ForbiddenWriteError: a in main + ? ForbiddenWriteError: a | routine main | inputs a @@ -1114,7 +1114,7 @@ Some rudimentary tests for shr. | { | shr a | } - ? UnmeaningfulReadError: c in main + ? UnmeaningfulReadError: c ### call ### @@ -1134,7 +1134,7 @@ initialized. | { | call foo | } - ? UnmeaningfulReadError: x in main + ? UnmeaningfulReadError: x Note that if you call a routine that trashes a location, you also trash it. @@ -1153,7 +1153,7 @@ Note that if you call a routine that trashes a location, you also trash it. | ld x, 0 | call foo | } - ? ForbiddenWriteError: lives in main + ? ForbiddenWriteError: lives | byte lives | @@ -1190,7 +1190,7 @@ You can't output a value that the thing you called trashed. | ld x, 0 | call foo | } - ? UnmeaningfulOutputError: lives in main + ? UnmeaningfulOutputError: lives ...unless you write to it yourself afterwards. @@ -1241,7 +1241,7 @@ calling it. | call foo | ld a, x | } - ? UnmeaningfulReadError: x in main + ? UnmeaningfulReadError: x If a routine trashes locations, they are uninitialized in the caller after calling it. @@ -1266,7 +1266,7 @@ calling it. | call foo | ld a, x | } - ? UnmeaningfulReadError: x in main + ? UnmeaningfulReadError: x Calling an extern is just the same as calling a defined routine with the same constraints. @@ -1294,7 +1294,7 @@ same constraints. | { | call chrout | } - ? UnmeaningfulReadError: a in main + ? UnmeaningfulReadError: a | routine chrout | inputs a @@ -1308,7 +1308,7 @@ same constraints. | call chrout | ld x, a | } - ? UnmeaningfulReadError: a in main + ? UnmeaningfulReadError: a ### trash ### @@ -1334,7 +1334,7 @@ Trash does nothing except indicate that we do not care about the value anymore. | ld a, 0 | trash a | } - ? UnmeaningfulOutputError: a in foo + ? UnmeaningfulOutputError: a | routine foo | inputs a @@ -1345,7 +1345,7 @@ Trash does nothing except indicate that we do not care about the value anymore. | trash a | st a, x | } - ? UnmeaningfulReadError: a in foo + ? UnmeaningfulReadError: a ### if ### @@ -1510,7 +1510,7 @@ trashes {`a`, `b`}. | trash x | } | } - ? ForbiddenWriteError: x in foo + ? ForbiddenWriteError: x (in foo, line 10) | routine foo | inputs a, x, z @@ -1522,7 +1522,7 @@ trashes {`a`, `b`}. | trash x | } | } - ? ForbiddenWriteError: a in foo + ? ForbiddenWriteError: a (in foo, line 10) ### repeat ### @@ -1575,7 +1575,7 @@ initialized at the start. | cmp x, 10 | } until z | } - ? UnmeaningfulReadError: y in main + ? UnmeaningfulReadError: y And if you trash the test expression (i.e. `z` in the below) inside the loop, this is an error too. @@ -1592,7 +1592,7 @@ this is an error too. | copy one, two | } until z | } - ? UnmeaningfulReadError: z in main + ? UnmeaningfulReadError: z The body of `repeat forever` can be empty. @@ -1624,7 +1624,7 @@ Can't `copy` from a memory location that isn't initialized. | { | copy x, lives | } - ? UnmeaningfulReadError: x in main + ? UnmeaningfulReadError: x Can't `copy` to a memory location that doesn't appear in (outputs ∪ trashes). @@ -1652,7 +1652,7 @@ Can't `copy` to a memory location that doesn't appear in (outputs ∪ trashes). | { | copy 0, lives | } - ? ForbiddenWriteError: lives in main + ? ForbiddenWriteError: lives a, z, and n are trashed, and must be declared as such @@ -1662,7 +1662,7 @@ a, z, and n are trashed, and must be declared as such | { | copy 0, lives | } - ? ForbiddenWriteError: n in main + ? ForbiddenWriteError: n a, z, and n are trashed, and must not be declared as outputs. @@ -1672,7 +1672,7 @@ a, z, and n are trashed, and must not be declared as outputs. | { | copy 0, lives | } - ? UnmeaningfulOutputError: n in main + ? UnmeaningfulOutputError: n Unless of course you subsequently initialize them. @@ -1901,7 +1901,7 @@ as an input to, an output of, or as a trashed value of a routine. | { | copy foo, vec | } - ? ConstantConstraintError: foo in main + ? ConstantConstraintError: foo | vector routine | inputs x @@ -1923,7 +1923,7 @@ as an input to, an output of, or as a trashed value of a routine. | { | copy foo, vec | } - ? ConstantConstraintError: foo in main + ? ConstantConstraintError: foo | vector routine | inputs x @@ -1945,7 +1945,7 @@ as an input to, an output of, or as a trashed value of a routine. | { | copy foo, vec | } - ? ConstantConstraintError: foo in main + ? ConstantConstraintError: foo You can copy the address of a routine into a vector, if that vector is declared appropriately. @@ -2074,7 +2074,7 @@ Calling the vector does indeed trash the things the vector says it does. | copy bar, foo | call foo | } - ? UnmeaningfulOutputError: x in main + ? UnmeaningfulOutputError: x `goto`, if present, must be in tail position (the final instruction in a routine.) @@ -2200,7 +2200,7 @@ vector says it does. | call sub | ld a, x | } - ? UnmeaningfulReadError: x in main + ? UnmeaningfulReadError: x | vector routine | outputs x From bda0202dee41566034ad635d43aa540d5b95cd6c Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 6 Mar 2018 15:42:12 +0000 Subject: [PATCH 26/35] Confirm that AND clips the range and INC/DEC invalidate it. --- README.md | 8 ++++-- src/sixtypical/analyzer.py | 11 +++++++ tests/SixtyPical Analysis.md | 56 ++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 34ddc1d..f55f6ef 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,6 @@ Documentation * [SixtyPical specification](doc/SixtyPical.md) * [SixtyPical revision history](HISTORY.md) * [Literate test suite for SixtyPical syntax](tests/SixtyPical%20Syntax.md) -* [Literate test suite for SixtyPical execution](tests/SixtyPical%20Execution.md) * [Literate test suite for SixtyPical analysis](tests/SixtyPical%20Analysis.md) * [Literate test suite for SixtyPical compilation](tests/SixtyPical%20Compilation.md) * [6502 Opcodes used/not used in SixtyPical](doc/6502%20Opcodes.md) @@ -78,11 +77,14 @@ are trashed inside the block. ### Re-order routines and optimize tail-calls to fallthroughs Not because it saves 3 bytes, but because it's a neat trick. Doing it optimally -is probably NP-complete. But doing it adeuqately is probably not that hard. +is probably NP-complete. But doing it adequately is probably not that hard. + +### Different preludes for different architectures + +`--prelude=c64-basic` ### And at some point... -* Confirm that `and` can be used to restrict the range of table reads/writes. * `low` and `high` address operators - to turn `word` type into `byte`. * `const`s that can be used in defining the size of tables, etc. * Tests, and implementation, ensuring a routine can be assigned to a vector of "wider" type diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index d3b977b..88088aa 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -194,6 +194,7 @@ class Context(object): 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: @@ -220,6 +221,10 @@ class Context(object): src_range = src.max_range() self._range[dest] = src_range + def invalidate_range(self, ref): + self.assert_meaningful(ref) + self._range[ref] = ref.max_range() + def set_unmeaningful(self, *refs): for ref in refs: if ref in self._range: @@ -377,6 +382,7 @@ class Analyzer(object): raise TypeMismatchError(instr, '{} and {}'.format(src, name)) else: context.set_written(dest) + # FIXME: context.copy_range(src, dest) ? context.assert_meaningful(src) elif opcode == 'add': context.assert_meaningful(src, dest, FLAG_C) @@ -395,6 +401,7 @@ class Analyzer(object): context.set_unmeaningful(REG_A) else: self.assert_type(TYPE_WORD, dest) + context.invalidate_range(dest) elif opcode == 'sub': context.assert_meaningful(src, dest, FLAG_C) if src.type == TYPE_BYTE: @@ -405,10 +412,12 @@ class Analyzer(object): context.set_written(dest, FLAG_Z, FLAG_N, FLAG_C, FLAG_V) context.set_touched(REG_A) context.set_unmeaningful(REG_A) + context.invalidate_range(dest) elif opcode in ('inc', 'dec'): self.assert_type(TYPE_BYTE, dest) context.assert_meaningful(dest) context.set_written(dest, FLAG_Z, FLAG_N) + context.invalidate_range(dest) elif opcode == 'cmp': self.assert_type(TYPE_BYTE, src, dest) context.assert_meaningful(src, dest) @@ -425,10 +434,12 @@ class Analyzer(object): self.assert_type(TYPE_BYTE, src, dest) context.assert_meaningful(src, dest) context.set_written(dest, FLAG_Z, FLAG_N) + context.invalidate_range(dest) elif opcode in ('shl', 'shr'): self.assert_type(TYPE_BYTE, dest) context.assert_meaningful(dest, FLAG_C) context.set_written(dest, FLAG_Z, FLAG_N, FLAG_C) + context.invalidate_range(dest) elif opcode == 'call': type = instr.location.type if isinstance(type, VectorType): diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index 42d2173..47a9124 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -623,6 +623,62 @@ This applies to `copy` as well. | } ? 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 + | + | routine main + | 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 + +Test for "clipping", but not enough. + + | word one: 77 + | word table[32] many + | + | routine main + | 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 + +If you alter the value after "clipping" it, the range can +no longer be guaranteed. + + | word one: 77 + | word table[32] many + | + | routine main + | 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 + ### add ### Can't `add` from or to a memory location that isn't initialized. From 1d1612761b661be066e4afe083ccd02646d93c1c Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 6 Mar 2018 15:52:21 +0000 Subject: [PATCH 27/35] Make the demo game compilable again. --- eg/c64/demo-game.60p | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/eg/c64/demo-game.60p b/eg/c64/demo-game.60p index 7ce80de..afcda06 100644 --- a/eg/c64/demo-game.60p +++ b/eg/c64/demo-game.60p @@ -385,6 +385,13 @@ define game_state_title_screen game_state_routine { ld y, 0 repeat { + + // First we "clip" the index to 0-31 to ensure we don't + // read outside the bounds of the table: + ld a, y + and a, 31 + ld y, a + ld a, press_fire_msg + y st on, c From 731b83d20f636c277036957690cee6e293d88561 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 6 Mar 2018 15:52:55 +0000 Subject: [PATCH 28/35] Update copyright dates. --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 35e493b..9d5dd32 100644 --- a/LICENSE +++ b/LICENSE @@ -7,7 +7,7 @@ covered by the following BSD-compatible license, modelled after the ----------------------------------------------------------------------------- - Copyright (c)2014-2015 Chris Pressey, Cat's Eye Technologies. + Copyright (c)2014-2018 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-2015, Chris Pressey, Cat's Eye Technologies. + Copyright (c)2014-2018, Chris Pressey, Cat's Eye Technologies. All rights reserved. Redistribution and use in source and binary forms, with or without From 8298ad38f9a307e9a25c9e05fd44be87e04fecaf Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 6 Mar 2018 16:04:02 +0000 Subject: [PATCH 29/35] Expand on what the range-checking includes in this version. --- HISTORY.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index c4f6e9e..d990c54 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -7,7 +7,15 @@ History of SixtyPical * 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. * The reference analyzer's ability to prove this is currently fairly weak, - but it does exist. + but it does exist: + * Loading a constant into a memory location means we know the range + is exactly that one constant value. + * `AND`ing a memory location with a value means the range of the + memory location cannot exceed the range of the value. + * Doing arithmetic on a memory location invalidates our knowledge + of its range. + * Copying a value from one memory location to another copies the + known range as well. * Cleaned up the internals of the reference implementation (incl. the AST) and re-organized the example programs in the `eg` subdirectory. * Most errors produced by the reference implementation now include a line number. From dbba7cc7b9ce9cac276a035f3e530b728ffc20bf Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 6 Mar 2018 16:28:34 +0000 Subject: [PATCH 30/35] Use ArgumentParser instead of OptionParser. --- bin/sixtypical | 55 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index 62b3c6a..e084d13 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -13,7 +13,7 @@ sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src')) # ----------------------------------------------------------------- # import codecs -from optparse import OptionParser +from argparse import ArgumentParser from pprint import pprint import sys import traceback @@ -25,28 +25,43 @@ from sixtypical.compiler import Compiler if __name__ == '__main__': - optparser = OptionParser(__doc__.strip()) + argparser = ArgumentParser(__doc__.strip()) - optparser.add_option("--analyze-only", - action="store_true", - help="Only parse and analyze the program; do not compile it.") - optparser.add_option("--basic-prelude", - action="store_true", - help="Insert a Commodore BASIC 2.0 snippet before the program " - "so that it can be LOADed and RUN on Commodore platforms.") - optparser.add_option("--debug", - action="store_true", - help="Display debugging information when analyzing and compiling.") - optparser.add_option("--parse-only", - action="store_true", - help="Only parse the program; do not analyze or compile it.") - optparser.add_option("--traceback", - action="store_true", - help="When an error occurs, display a full Python traceback.") + argparser.add_argument( + 'filenames', metavar='FILENAME', type=str, nargs='+', + help="The SixtyPical source files to compile." + ) + argparser.add_argument( + "--analyze-only", + action="store_true", + help="Only parse and analyze the program; do not compile it." + ) + argparser.add_argument( + "--basic-prelude", + action="store_true", + help="Insert a Commodore BASIC 2.0 snippet before the program " + "so that it can be LOADed and RUN on Commodore platforms." + ) + argparser.add_argument( + "--debug", + action="store_true", + help="Display debugging information when analyzing and compiling." + ) + argparser.add_argument( + "--parse-only", + action="store_true", + help="Only parse the program; do not analyze or compile it." + ) + argparser.add_argument( + "--traceback", + action="store_true", + help="When an error occurs, display a full Python traceback." + ) - (options, args) = optparser.parse_args(sys.argv[1:]) + options, unknown = argparser.parse_known_args(sys.argv[1:]) + remainder = ' '.join(unknown) - for filename in args: + for filename in options.filenames: text = open(filename).read() try: From 72efecbb1a59e5f9ed879b65f5dbd241a23a93d3 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 6 Mar 2018 17:00:39 +0000 Subject: [PATCH 31/35] Support different preludes for different archs (c64 and vic20 now.) --- README.md | 4 ---- bin/sixtypical | 17 ++++++++++++----- loadngo.sh => loadngo-c64.sh | 4 ++-- tests/SixtyPical Compilation.md | 2 +- tests/appliances/bin/dcc6502-adapter | 2 +- 5 files changed, 16 insertions(+), 13 deletions(-) rename loadngo.sh => loadngo-c64.sh (65%) diff --git a/README.md b/README.md index f55f6ef..788616f 100644 --- a/README.md +++ b/README.md @@ -79,10 +79,6 @@ are trashed inside the block. Not because it saves 3 bytes, but because it's a neat trick. Doing it optimally is probably NP-complete. But doing it adequately is probably not that hard. -### Different preludes for different architectures - -`--prelude=c64-basic` - ### And at some point... * `low` and `high` address operators - to turn `word` type into `byte`. diff --git a/bin/sixtypical b/bin/sixtypical index e084d13..72aabef 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -37,10 +37,11 @@ if __name__ == '__main__': help="Only parse and analyze the program; do not compile it." ) argparser.add_argument( - "--basic-prelude", - action="store_true", - help="Insert a Commodore BASIC 2.0 snippet before the program " - "so that it can be LOADed and RUN on Commodore platforms." + "--prelude", type=str, + help="Insert a snippet before the compiled program " + "so that it can be LOADed and RUN on a certain platforms. " + "Also sets the origin. " + "Options are: c64 or vic20." ) argparser.add_argument( "--debug", @@ -93,10 +94,16 @@ if __name__ == '__main__': fh = sys.stdout start_addr = 0xc000 prelude = [] - if options.basic_prelude: + if options.prelude == 'c64': start_addr = 0x0801 prelude = [0x10, 0x08, 0xc9, 0x07, 0x9e, 0x32, 0x30, 0x36, 0x31, 0x00, 0x00, 0x00] + elif options.prelude == 'vic20': + start_addr = 0x1001 + prelude = [0x0b, 0x10, 0xc9, 0x07, 0x9e, 0x34, + 0x31, 0x30, 0x39, 0x00, 0x00, 0x00] + else: + raise NotImplementedError # we are outputting a .PRG, so we output the load address first # we don't use the Emitter for this b/c not part of addr space diff --git a/loadngo.sh b/loadngo-c64.sh similarity index 65% rename from loadngo.sh rename to loadngo-c64.sh index 0a3401e..b28509e 100755 --- a/loadngo.sh +++ b/loadngo-c64.sh @@ -5,11 +5,11 @@ if [ "X$X64" = "X" ]; then fi SRC=$1 if [ "X$1" = "X" ]; then - echo "Usage: ./loadngo.sh " + echo "Usage: ./loadngo-c64.sh " exit 1 fi OUT=/tmp/a-out.prg -bin/sixtypical --traceback --basic-prelude $SRC > $OUT || exit 1 +bin/sixtypical --traceback --prelude=c64 $SRC > $OUT || exit 1 ls -la $OUT if [ -e vicerc ]; then $X64 -config vicerc $OUT diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index 831b94e..2ead442 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -7,7 +7,7 @@ SixtyPical to 6502 machine code. [Falderal]: http://catseye.tc/node/Falderal -> Functionality "Compile SixtyPical program" is implemented by - -> shell command "bin/sixtypical --basic-prelude --traceback %(test-body-file) >/tmp/foo && tests/appliances/bin/dcc6502-adapter shell command "bin/sixtypical --prelude=c64 --traceback %(test-body-file) >/tmp/foo && tests/appliances/bin/dcc6502-adapter Tests for functionality "Compile SixtyPical program" diff --git a/tests/appliances/bin/dcc6502-adapter b/tests/appliances/bin/dcc6502-adapter index d92339b..e11b6aa 100755 --- a/tests/appliances/bin/dcc6502-adapter +++ b/tests/appliances/bin/dcc6502-adapter @@ -1,6 +1,6 @@ #!/usr/bin/env python -# script that allows the binary output of sixtypical --basic-prelude --compile to be +# script that allows the binary output of sixtypical --prelude=c64 --compile to be # disassembled by https://github.com/tcarmelveilleux/dcc6502 import sys From 95fb2bb8f6956bd4a9edf4389d70a7677ab13202 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 7 Mar 2018 13:27:57 +0000 Subject: [PATCH 32/35] Make loadngo.sh able to handle both C64 (x64) and VIC-20 (xvic). --- eg/rudiments/README.md | 3 +++ loadngo-c64.sh | 19 ------------------- loadngo.sh | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 19 deletions(-) delete mode 100755 loadngo-c64.sh create mode 100755 loadngo.sh diff --git a/eg/rudiments/README.md b/eg/rudiments/README.md index c561673..51230f4 100644 --- a/eg/rudiments/README.md +++ b/eg/rudiments/README.md @@ -7,3 +7,6 @@ They are not meant to be specific to any architecture, but many do assume the existence of a routine at 65490 which outputs the value of the accumulator as an ASCII character, simply for the purposes of producing some observable output. +(This is an address of a KERNAL routine which does this +on both the Commodore 64 and the Commodore VIC-20, so these +sources should be usable on these architectures.) diff --git a/loadngo-c64.sh b/loadngo-c64.sh deleted file mode 100755 index b28509e..0000000 --- a/loadngo-c64.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -if [ "X$X64" = "X" ]; then - X64=x64 -fi -SRC=$1 -if [ "X$1" = "X" ]; then - echo "Usage: ./loadngo-c64.sh " - exit 1 -fi -OUT=/tmp/a-out.prg -bin/sixtypical --traceback --prelude=c64 $SRC > $OUT || exit 1 -ls -la $OUT -if [ -e vicerc ]; then - $X64 -config vicerc $OUT -else - $X64 $OUT -fi -rm -f $OUT diff --git a/loadngo.sh b/loadngo.sh new file mode 100755 index 0000000..b3d0e1d --- /dev/null +++ b/loadngo.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +usage="Usage: loadngo.sh (c64|vic20) [--dry-run] " + +arch="$1" +shift 1 +if [ "X$arch" = "Xc64" ]; then + prelude='c64' + if [ -e vicerc ]; then + emu="x64 -config vicerc" + else + emu="x64" + fi +elif [ "X$arch" = "Xvic20" ]; then + prelude='vic20' + if [ -e vicerc ]; then + emu="xvic -config vicerc" + else + emu="xvic" + fi +else + echo $usage && exit 1 +fi + +if [ "X$1" = "X--dry-run" ]; then + shift 1 + emu='echo' +fi + +src="$1" +if [ "X$src" = "X" ]; then + echo $usage && exit 1 +fi + +### do it ### + +out=/tmp/a-out.prg +bin/sixtypical --traceback --prelude=$prelude $src > $out || exit 1 +ls -la $out +$emu $out +rm -f $out From 2b9c457ffc7bd0613735627bfd31f93aec57566a Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 8 Mar 2018 13:24:00 +0000 Subject: [PATCH 33/35] Try to improve the description in the documentation, and notes. --- HISTORY.md | 2 ++ README.md | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index d990c54..08e004d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -19,6 +19,8 @@ History of SixtyPical * Cleaned up the internals of the reference implementation (incl. the AST) and re-organized the example programs in the `eg` subdirectory. * Most errors produced by the reference implementation now include a line number. +* Compiler supports multiple preludes, specifically both Commodore 64 and + Commodore VIC-20; the `loadngo.sh` script supports both architectures too. 0.12 ---- diff --git a/README.md b/README.md index 788616f..0b396a9 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,26 @@ SixtyPical _Version 0.13. Work-in-progress, everything is subject to change._ -SixtyPical is a very low-level programming language, similar to 6502 assembly, -with static analysis through abstract interpretation. +**SixtyPical** is a 6502-assembly-like programming language with advanced +static analysis. + +"6502-assembly-like" means that it has similar restrictions as programming +in 6502 assembly (e.g. the programmer must choose the registers that +values will be stored in) and is concomittantly easy for a compiler to +translate it to 6502 machine language code. + +"Advanced static analysis" includes _abstract interpretation_, where we +go through the program step by step, tracking not just the changes that +happen during a _specific_ execution of the program, but _sets_ of changes +that could _possibly_ happen in any run of the program. This lets us +determine that certain things can never happen, which we can present as +safety guarantees. In practice, this means it catches things like * you forgot to clear carry before adding something to the accumulator * a subroutine that you call trashes a register you thought was preserved +* you tried to read or write a byte beyond the end of a byte array * you tried to write the address of something that was not a routine, to a jump vector @@ -17,7 +30,8 @@ and suchlike. It also provides some convenient operations and abstractions based on common machine-language programming idioms, such as * copying values from one register to another (via a third register when - there are no underlying instructions that directly support it) + there are no underlying instructions that directly support it); this + includes 16-bit values, which are copied in two steps * explicit tail calls * indirect subroutine calls From af09bd88759578b7f477639223edeae49efdd4ed Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 8 Mar 2018 13:36:30 +0000 Subject: [PATCH 34/35] Add screenshot. --- README.md | 17 +++++++++++++++++ eg/c64/{screen2.60p => hearts.60p} | 0 images/hearts.png | Bin 0 -> 10392 bytes 3 files changed, 17 insertions(+) rename eg/c64/{screen2.60p => hearts.60p} (100%) create mode 100644 images/hearts.png diff --git a/README.md b/README.md index 0b396a9..eb6d894 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,21 @@ based on common machine-language programming idioms, such as The reference implementation can analyze and compile SixtyPical programs to 6502 machine code. +Quick Start +----------- + +If you have the [VICE][] emulator installed, from this directory, you can run + + ./loadngo.sh c64 eg/c64/hearts.60p + +and it will compile the [hearts.60p source code](eg/c64/hearts.60p) and +automatically start it in the `x64` emulator, and you should see: + +![Screenshot of result of running hearts.60p](https://raw.github.com/catseye/SixtyPical/master/images/hearts.png) + +You can try the `loadngo.sh` script on other sources in the `eg` directory +tree. There is an entire small game(-like program) in [demo-game.60p](eg/c64/demo-game.60p). + Documentation ------------- @@ -108,3 +123,5 @@ is probably NP-complete. But doing it adequately is probably not that hard. * Automatic tail-call optimization (could be tricky, w/constraints?) * Possibly `ld x, [ptr] + y`, possibly `st x, [ptr] + y`. * Maybe even `copy [ptra] + y, [ptrb] + y`, which can be compiled to indirect LDA then indirect STA! + +[VICE]: http://vice-emu.sourceforge.net/ diff --git a/eg/c64/screen2.60p b/eg/c64/hearts.60p similarity index 100% rename from eg/c64/screen2.60p rename to eg/c64/hearts.60p diff --git a/images/hearts.png b/images/hearts.png new file mode 100644 index 0000000000000000000000000000000000000000..355a50654438f71ee5ca437c42f512d196fddfc1 GIT binary patch literal 10392 zcmd6NbyQp3(=N0WN^vMw+)HtHC{|ju#T|k>4OSeAJ4K3HTco%Lf&~u_#odBKa1V0R z_ulu)Z+(B?^<}NB?Cdi$Yo3|s%Zq$dB)HiwNX@ayNNhHw|Yiw=Vz}OB8D-XGcp8S92FjOD9(wXSbuL z5J?miT9glR(wd%G@FlNgLuF8{}MTm1fQBUpS>Le0#R z6J(k0u-x+X62xCPNG3Ra#26`?9)Vu!@w!u7h9Y4Y70EsG3~V9y5uChnNqCaIsIDG`0X zSV1T>PsOlhgG>+SylfifI1M)B_fvp=SG?2p0uz)C{izbT7B3dBCNV)*S@{J5D!k;m zu4N9OzIk_hJs)WyM@M)!jAvl;MY?iWPEpQ!aj-;P&UUp0081wGZVcbQK*oB$(>3wT zPmz9`5nHyCyW4Z>sD+V#cy7W{Qc8}GuEC@^6f05i$F|I>ELjjB&=xR@tA^vXc+&3l zB1~Fzi65p*$2?rHGS3LMXp2wqOI%yZdSI~ zlE6`Fz$#`cwToHt(|5-!zHs zhWke+eSOG}2Hx8)vhl5nyy-<-G^ZHv!TFV4%o3`6fxd|UjjNWEh9 zR@oPGGCVxouPNDg1_sX8#5o5jhQL;aPG>zM@5X*^`?>YroA8fGG+WsRVw4m zirW4u6Z;pn1OXTtL6gaNV-W3cPTm9BC&TGylO7DTkdhlU3AbE20u z)CMMZ`o{k70te%SUUbF#@VSe=(d*>~YEw1Nj^X5n> z@V-?iC4b=M{zeDBR5ZS5Vr%pr)Yq=ZJL_6l8N38SF(dy3Sfxcj?r=WLU@dg`WHmn^ zpqTS457ilJ05aYiJxcUi$c+(ZW*>3t>9SftAXY5)N-JDqF_&!(sy=$yA2msb-8ILu zvp&YnWtd|1U8|Rd6P5cQoRa)>cJd(RVR5--aF01NUn~23nlaI^xh~RVTZvzQ=^a!b z?80>7*>Z^2dEKT)pI~7*^8y_M!-~otafz^^+zz9t$OV>pi=PCuyf$$mKiL`t1L4~@ zsw><8ae3e@18~jV@Bk%iRY7W(fuBUJ1ef*ZLzFDXqWAWK*Ia(T(4XqCZNt=UsL9Pq zGN#o!H}PUStwv;mgkMIx%Z~B5T#p;`oi3A22~77Y*p}}b&g><$;f!>~W7@y7Nci)U z8E(v=DiE#}Pa12@?bOk4)e+bsDWGrCmf(rQX(%8JOT1 zDAqI-huqqA)Hr5jt|u#64iLxV2V0B-ybK6u+Q0H=AMs(Upw~Q8?4kE}*LdmxwbCnns!C*PMs9BHzp9DZb*cxci%bL%m?p3#zn(h8 zDwMZR8vdE`G6z5y!ov&3b}A0pe^#&a2G?j%-x}c-x{NbyxM)|)w}0~E-s^C7e4tm;vAEk6DHoHQ(;wT$qr=(EZqgRx0+z#9z9RJZ z-gIfN*&GxBo^E8BFS!g37EL~G2l!J!QtpZu|polt#9^%F+@Zx{49G+JbH`sG0mq|4_h?GGBd?ACjp1IUtA);o$fF?CRM_86?k?=5oLE= ze{%wc-dy>#bzV_RD$>>IO*p}N)$uz`c^Lis%UkvvnhV7_0e#7sqTK+~s?H|Td+LDW zQ#RcdtF`U8(|sD>bhQ9$uKpDHbG#n#gW7UOG?!t26bKFdqv#Fe)qP|5Imq5~`PL&V zulcuHv3$x!*SHbDdyYVvp&Ws4iEo3kHzDiTTd{GNr2v z$RJJ2ouVM8%k$G#@zY<<$LWu=A6n|uHDqx|{WfXN#vL%Hdq%_+E_TjPArkz zH^0n%auhT)2xl%vO19^@pQsnh8JXkY;*NZlx+8~{>{AAr`(AD9Re+1X7j7Gm>RZ!5 zm++A1jSXiq!>j%LV!L<2}ZWeuRYYW)h3nNv3~v%DhNu5}_Me zhJ_1pQ^1vx@2X{Xpqiux?Gz}g)c8zL8+y1J=X}0i8yO8MFJePqRmY?v9OPWzT50|* z8b=Y7REXG{IdM9A9&l5M=-ACU_Ef&VP`N^2W6IKSfzp^xc)>Bz6Z0|#SAxcmBV;U( z>og*Kheg3MDKQj;StA5#s4O+q_Qq#UlGio!Y}iuW4u@t7Y%4=470q7TEYux*DJLzY zQmL`kdne*6v?<5Ko)lB%kH83d;KHKCzz{HETS_fU|ogAJEYAA(mo?vVR^HgTYaCe@J%s(IW{<>4?H|LPU=D@zw>RR&(w1}FOTNC8fAr_Duoy{wD_<;5Zyd&vVWzFK1 zP7M;4oAN)}-yKV;d`^E{s)Ju2p+~lRuj=6lxQ}wikO#EAxnClQA~PGzaNEeeDV0W3?{0;$di~d;r48!5eG33moDrX~6a*cat z{=uuJ8co8z%$u?7#b#UXY5CKJW0rkKPOp9@M(W+Q4AM$e+OzgAA|Ej$Sj_p#7~)mT z(ecqV;%Rujf=*uJ`1~@V7-`mJ7MA8RfgoVBf^`KGo@Bp50glOY@s_IyNe-p?YE*QP zE7Yg0`%sZWr{zSV<-SQU2a!Q~Qji<)eSEVL7E3yJ-7E!_k{XMLINf0)(wx%A$n6c> z*ig4h%FB3AL_K?`a#x9Ac-i`j)=w^#uNkaN5q{zxldzN?!TuC7o(0&>i1A&jyD8Ix z4R4{>)6erzub;GaeH*6x_A4Eajg~}?L~Rr-#F;JBmNiYog$Mb4I?6*@q&i1RRk_ZO zW%0z(V#3v~VtAVsTk;%K4z0A${D_VQGX%rc*@4=lTnW1&bhRutgS;ZTpTXiJnpZ5( zF7YUQuN#MbDs{U;-{oL|%GYEeqTP;c;)09{Rv(jsWvNp(W8xh3n8Ai|ZlVIW^&MZo zil~iqW1rqUNwFLjCE+#~g7(cI8-<5xw(8`2tfna#I zISEy{ZP@*{u|RI@LN`ubGR-aVX?*5kPX-F}Ip$55_o9H)oqIL)mcyI;d7W`57CqpZ z>-#4TUJQ@tV4*mwzP1p^?FRgAFn6iPA2Rff{ONBHQ3mtW7y46i@1s#+QGeb`Rp6`W z-COIsD7@J;?F+oPHLz!(z&w~}@~~8;(<%c}r9p$p+Fy0>q6+XDWL`f%X-{n%j8hhy zX0CZ5j%rKut19&X;C+!?KGu@pVcO6jhvUgVdJ z=}dBv!msgY?HJc-`~E0$ZrgTO8=Up()OJs|Uw)s`JD@e3HftW0m)D~Bfae?A+d)y| z`=W-LyYtcO0U?^OLVhR5frxZV#T#(E`?Zdbh~G8s(=y9Xg9V1ohir?rY=eb54)o8s zKZp2B<-;aF-S)O>3Yo#(Kyz>3AL`Qd;NRiP{1SB6|0oI$H|8wLd^tQv<$Jd8(|>DP zKvFx@A2cVvkEYW)qd{&QHz7ZprC58?+2iS>s(hw_I#u|MOev-4HGHJT#)}0QNQ8Gf z<$NDvY|dm<`6@F{_De(%mN185m1xhOr%%zQxd$_;ZsVmo2yzC|U7vFKJdDp1LQBCv zpXKEmg-0p3@zvh@5Qv7->4t|;9h93{z_a@Xy zRfNS&aTlA*a9_XCi@Szlc+0sT4eVKHpyoXK8}Z9}IQBxDu`SlTqk0q)4ryx$ z7|7Qi{pB3S0zO)M!qq6KGGqUk5rBXb`kO6^IzRV(dLZ^pr!uTQz?bysWMxpnJBMD4 zba>FDf5mr$H*Uj0+b!;U`U8-9al0B$pBWo)N+5Z5%M0=ggdl1m+e2fxl8F1%vyN2u ztxlK@1JL^xn(`x{ow-k@@2 zkjW4M`oqpJ*Vgma7XY$vSlE*D)K8j(VdaXdvaFDoC8qvjxkhM zIqrrm_nQPfNMB~NKAq|Zi+hH2F%fm~7v3-k&S(haGXQF~CulZJ4#nO=u&n661}m%$ zeWG3f#*AG?vOEPnuBg@AzeSG^Tu%oq2Agck=K6no8qC>mx#Te4m}phmS1cP}%Yx;2 z!V9_XhTim!OeBBxd=n?6%Q$CImEUjvxGgn=0lCJP(1VDI;ALAt8;-V?Xb?>?5C(m3Ddp@x?*Lile6flhHH(%mOQeIV-fK+E;J=ft6{CX_X;v|_s zcB$uj;F@SpabpU*MST;;G8b@XwKdTomRCt zt;e<0dEiyy2c78QDsqPBj!~O0q-~#bpe+GFxpoS*tg$zHvpC;2k6uS-w=i2x0?7iY#qV+PHLSRLr+J6h=%RC$|IZ=0bD z3N<&du7=8MLJ0}d16-X__^FBn*M>KQ0PF+_HzZ-Mb#svseRpGM9Q1^rGVxx*zju!!w#M1)-EuJWZ1yYhVCit{A(pAY&lMN>JMNxsZpTuVmv5Ds&&PL*ia zm-tQF;~j_%-NxF#?}lwV#-X*pFW@Q`ej*%eZ%ySjUt;8VOV9By3QliA8 z&2D?JrCi#_61$SPWx5zI<;dKVGsFs>#r}^Q*^v30bv!d8#GQ^WEw!39#584ea2P9^ z@kH??!!z1I>zacTK&XtEvmyd40s=Lkm2amSj7>vE!`2F7`#OjHeOk$KHQEJ!k z1tS}MxunhCG1d>H6jmOoI?{}z`E&`MH|n{}U+y>#>0d}kN601L=8?YT%KwX}w@i!X zT=)|_R1*nrA+7#Q-f!%0KD0_DcRrAq{VSBswHK`YcNS@&6i**%7#{Rj^U2P?ihIGx zd-L^$G*{ri&LJ%Tf6ikSBmD#M*l$Pw9|TGNH3Ds#y1yZajE%_xai9<{+z^U zMe(Nl8(Tpuqy=cC-G7Bytp?xce+j{TKmFq;5*D8R{|pwX1yb|>j%Z!Pd%0n2n)4QS z;Hrg%s>0sY`O2fUQH&A|GA_xS5^Z0*~odMC;~uM2NI|SxC-4Ee$Ba8C}gBcIO~j@ zX_6W_LW|Y5v~<|C)pEChi+T+TXO2LJi!7RC@B2lJAw<-rb~p5~_jxh0K~ zloGj75jFSmmD3TO2knPy!ogG1#vA}2)1rzX!CL49M z;tZ7z_KU}>0w@VJ(lv@4_cHH9Vfau}prE4=3CE9`*s?pd+#a<{juO>L2?t<$H;zLO zCGLOOCx7(Y^*x=mjiW5Da5gK?ceTYRw0-iiLUXp>o~y^~Lp?#I0IcdJF*aGz?_kj@ zVk?E;e%hmY;Qe%c_g6q5OeZN?BP}scekAjq@>rwe%NN9#5|ZtO(bHxNBtgF%hMQLg%k$*x8=53}6GYF?9*RTMna>_o!*QiQp0v4ngs)bFHbU6JufX5EL&sCS95eI4v zyz;*)q>-I3q0hdX6PL+`Ko39pZwpn#{7c{e9F}|{a&t|RfU^vDa$C4xy-49O;RUb_ zx)gLK#cN$fq}f7D7eIxRDQv_ZQoumpiswUjm<g=%t<%u`5!s<4pIA13q0BROisV@h(e<4k=bUD3FZz4ROy)QTZZo&RZ zzqZIf?N~Q^3h9~n2rjYqmoGIAJ?oK!A2(m99nNBK@*P>}AgG2f6)h{44)!FrPp*Wp zC)_23Ms~k2Lcqw_!7TdlQoVM57%NX$oFrbdvd-e*;2!KFpDK3j5kmV|MKNXAWI-RT z9&@TKgy@*`aCJ$DXDnA9s9zL_&jO5W-8LFwGw=3Q#++c8&-?0Gq5UEF_PF^o;i3up5?m@{bAhl1?2XO?-S2L3FYhlH3yKNI3+avquzQMK!m zVkGY=S`$BFW8hMob!|BbnoM_8KoP7Ydrn*q_s11>G6atfiJnqM*60Fy z5&PcM3O)#HachSy)v-7nRi%rpEcRKi=ledPZk(gBA^{4gnWY4kd*%0e@)mS9efN1p zM$4EJ$`aiL3Vm9O@<2Tcw-*#jCz>` zknWU#tFtrkx26uwdEI^tw^Zv8eGRw3`m6J6$(>p_T%}$edF6aR=4|q>ZW7`7+!yAd zDUAC%QJ$PwV=Dr^wB)?ehY6tTrLjs{SqLC4cIrAZu2=oYrMUEtoMUG2ysar9E#n62ULhUvLQ9D z+6Z>#e-=K4VesI#>w&|jD|o2u0-m=EjoX{=tDQ=M>YOTukV7en-$J)?7A5bDf-AnP zEW0k@=_5EI`X;7-+mE>)d3eMjq}66HB=#k#_qy>D`dDAeDWe zH(HL@WY^mo_E*y-D90&xJ;8mWsFvp3!EbE4Wu_{EpZcHWVA5?~dK;d1pBYkoV(>q0S2YeYkLTsdZkxjlNY=UfDIl$PAy#2%A z96IT6r%>e;6n|Eo`@AT^x(M-!S0aCdabI^Ctc~n?)`_>sZuhg84v`1sUA0BdMA&x= z?Cep$LT)4(*Ixl~^pW;R(2$KWAwvD{2J!WO4(+rJ}_v$LFT@9M!WL&xU66j3%0C!amZyNNy06%&Q@4U+5GSJRYDs3EYI7h z!_74+mSUQq!MuB(1g16*?cyj99`}j5l;?2J4Pf=j*`dqj8u6Eqg~RCRlaNLvOYigG zLm|<MeR4Aj%{x9yVb2I;!)T^HvYy^2x1+%$NH0UC z(-0y3|CaZP3OFZb&HWUoY}M{8*6 zC|*&{&J3nc1Wz%=C1|)2hG>U50MD@@`kNfCDvX!JF2`R#%# ze57o+vZ}gy_C-${(Wq(%%1jzk8ncn>I_jvKy<34WRHr6=*!=2N``Nqjco9K+p|V2o zKZ~N?#O4V8oOh~Y6L1WPnz#c-Nx`47T|Rn-O4J=Li)tau)d zi2~T4@2W%K7POR^c|eEH`Xw+?y{fZ3O#OTwTPb8hZR{G6wS#rmV|dRNg51frLRH6o z>eM9q>3JVtIpKH(+&{HNk+xwJg5eeN6$a~FM<3&gl@%#Tai>gy8f#+y5y4LO-~4ppzwf!n|6k9hY%oO=f*fbcB%Pr|1YX{X^$TA7&PXcASqVAY sBTtBU#s0HI{MUu!zvXOc`y)nznY=hZX>ud-)k2gH@~UzbGNwWQ1Ck;cg#Z8m literal 0 HcmV?d00001 From d4adddc7348f45c732be90896568521035c20532 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 8 Mar 2018 14:26:09 +0000 Subject: [PATCH 35/35] This repository hasn't had a Mercurial counterpart for a while. --- .hgignore | 3 --- .hgtags | 11 ----------- 2 files changed, 14 deletions(-) delete mode 100644 .hgignore delete mode 100644 .hgtags diff --git a/.hgignore b/.hgignore deleted file mode 100644 index bb6d589..0000000 --- a/.hgignore +++ /dev/null @@ -1,3 +0,0 @@ -syntax: glob - -*.pyc diff --git a/.hgtags b/.hgtags deleted file mode 100644 index d8a6bfa..0000000 --- a/.hgtags +++ /dev/null @@ -1,11 +0,0 @@ -923e42a2d0c156f3eeea006301a067e693658fe1 0.1 -6490aea10e20c346a2f1598f698a781799b522ba 0.1-2014.1230 -9ad29480d9bb8425445504ff90fc14a1b1862789 0.2 -5d95f1d75a3226cb117e6b33abc5bdb04e161ef8 0.3 -d30f05a8bb46a2e6c5c61424204a4c8db6365723 0.4 -7a39b84bb002a0938109d705e0faf90d9fd8c424 0.6 -7a39b84bb002a0938109d705e0faf90d9fd8c424 0.6 -0000000000000000000000000000000000000000 0.6 -19c782179db9ed786d6e93a57d53bb31ac355dc4 0.5 -0000000000000000000000000000000000000000 0.6 -f89772f47de989c54434ec11935693c8b8d0f46d 0.6