From 3f0e36a67c4c1f229778b87a2f5d883e862b8290 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 27 Mar 2018 12:36:33 +0100 Subject: [PATCH 01/42] Add symbolic constants. --- HISTORY.md | 6 ++++ README.md | 1 - doc/SixtyPical.md | 14 ++++---- src/sixtypical/parser.py | 70 +++++++++++++++++++++++--------------- tests/SixtyPical Syntax.md | 25 ++++++++++++++ 5 files changed, 81 insertions(+), 35 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 98440e2..a73ba45 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,12 @@ History of SixtyPical ===================== +0.15 +---- + +* Symbolic constants can be defined with the `const` keyword, and can + be used in most places where literal values can be used. + 0.14 ---- diff --git a/README.md b/README.md index c5f30a8..8456707 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,6 @@ is probably NP-complete. But doing it adequately is probably not that hard. ### And at some point... * `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 * Related: can we simply view a (small) part of a buffer as a byte table? If not, why not? * Related: add constant to buffer to get new buffer. (Or to table, but... well, maybe.) diff --git a/doc/SixtyPical.md b/doc/SixtyPical.md index 22b5604..1955fe8 100644 --- a/doc/SixtyPical.md +++ b/doc/SixtyPical.md @@ -1,7 +1,7 @@ SixtyPical ========== -This document describes the SixtyPical programming language version 0.14, +This document describes the SixtyPical programming language version 0.15, both its static semantics (the capabilities and limits of the static analyses it defines) and its runtime semantics (with reference to the semantics of 6502 machine code.) @@ -555,9 +555,10 @@ The block is always executed as least once. Grammar ------- - Program ::= {TypeDefn} {Defn} {Routine}. + Program ::= {ConstDefn | TypeDefn} {Defn} {Routine}. + ConstDefn::= "const" Ident Const. TypeDefn::= "typedef" Type Ident. - Defn ::= Type Ident [Constraints] (":" Literal | "@" LitWord). + Defn ::= Type Ident [Constraints] (":" Const | "@" LitWord). Type ::= TypeTerm ["table" TypeSize]. TypeExpr::= "byte" | "word" @@ -573,12 +574,13 @@ Grammar | "routine" Ident Constraints (Block | "@" LitWord) . LocExprs::= LocExpr {"," LocExpr}. - LocExpr ::= Register | Flag | Literal | Ident. + LocExpr ::= Register | Flag | Const | Ident. Register::= "a" | "x" | "y". Flag ::= "c" | "z" | "n" | "v". + Const ::= Literal | Ident. Literal ::= LitByte | LitWord | LitBit. LitByte ::= "0" ... "255". - LitWord ::= "0" ... "65535". + LitWord ::= ["word"] "0" ... "65535". LitBit ::= "on" | "off". Block ::= "{" {Instr} "}". Instr ::= "ld" LocExpr "," LocExpr ["+" LocExpr] @@ -598,6 +600,6 @@ Grammar | "copy" LocExpr "," LocExpr ["+" LocExpr] | "if" ["not"] LocExpr Block ["else" Block] | "repeat" Block ("until" ["not"] LocExpr | "forever") - | "for" LocExpr ("up"|"down") "to" Literal Block + | "for" LocExpr ("up"|"down") "to" Const Block | "with" "interrupts" LitBit Block . diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 2391d05..22ab9ec 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -24,6 +24,7 @@ class Parser(object): self.symbols = {} # token -> SymEntry self.current_statics = {} # token -> SymEntry self.typedefs = {} # token -> Type AST + self.consts = {} # token -> Loc for token in ('a', 'x', 'y'): self.symbols[token] = SymEntry(None, LocationRef(TYPE_BYTE, token)) for token in ('c', 'z', 'n', 'v'): @@ -51,8 +52,11 @@ class Parser(object): def program(self): defns = [] routines = [] - while self.scanner.on('typedef'): - typedef = self.typedef() + while self.scanner.on('typedef', 'const'): + if self.scanner.on('typedef'): + self.typedef() + if self.scanner.on('const'): + self.defn_const() typenames = ['byte', 'word', 'table', 'vector', 'buffer', 'pointer'] # 'routine', typenames.extend(self.typedefs.keys()) while self.scanner.on(*typenames): @@ -110,6 +114,15 @@ class Parser(object): self.typedefs[name] = type_ return type_ + def defn_const(self): + self.scanner.expect('const') + name = self.defn_name() + if name in self.consts: + self.syntax_error('Const "%s" already declared' % name) + loc = self.const() + self.consts[name] = loc + return loc + def defn(self): type_ = self.defn_type() name = self.defn_name() @@ -118,10 +131,9 @@ class Parser(object): if self.scanner.consume(':'): if isinstance(type_, TableType) and self.scanner.on_type('string literal'): initial = self.scanner.token + self.scanner.scan() else: - self.scanner.check_type('integer literal') - initial = int(self.scanner.token) - self.scanner.scan() + initial = self.const().value addr = None if self.scanner.consume('@'): @@ -136,21 +148,31 @@ class Parser(object): return Defn(self.scanner.line_number, name=name, addr=addr, initial=initial, location=location) - def literal_int(self): - self.scanner.check_type('integer literal') - c = int(self.scanner.token) - self.scanner.scan() - return c - - def literal_int_const(self): - value = self.literal_int() - type_ = TYPE_WORD if value > 255 else TYPE_BYTE - loc = ConstantRef(type_, value) - return loc + def const(self): + if self.scanner.token in ('on', 'off'): + loc = ConstantRef(TYPE_BIT, 1 if self.scanner.token == 'on' else 0) + self.scanner.scan() + return loc + elif self.scanner.on_type('integer literal'): + value = int(self.scanner.token) + self.scanner.scan() + type_ = TYPE_WORD if value > 255 else TYPE_BYTE + loc = ConstantRef(type_, value) + return loc + elif self.scanner.consume('word'): + loc = ConstantRef(TYPE_WORD, int(self.scanner.token)) + self.scanner.scan() + return loc + elif self.scanner.token in self.consts: + loc = self.consts[self.scanner.token] + self.scanner.scan() + return loc + else: + self.syntax_error('bad constant "%s"' % self.scanner.token) def defn_size(self): self.scanner.expect('[') - size = self.literal_int() + size = self.const().value self.scanner.expect(']') return size @@ -294,16 +316,8 @@ class Parser(object): return accum def locexpr(self, forward=False): - if self.scanner.token in ('on', 'off'): - loc = ConstantRef(TYPE_BIT, 1 if self.scanner.token == 'on' else 0) - self.scanner.scan() - return loc - elif self.scanner.on_type('integer literal'): - return self.literal_int_const() - elif self.scanner.consume('word'): - loc = ConstantRef(TYPE_WORD, int(self.scanner.token)) - self.scanner.scan() - return loc + if self.scanner.token in ('on', 'off', 'word') or self.scanner.token in self.consts or self.scanner.on_type('integer literal'): + return self.const() elif forward: name = self.scanner.token self.scanner.scan() @@ -387,7 +401,7 @@ class Parser(object): else: self.syntax_error('expected "up" or "down", found "%s"' % self.scanner.token) self.scanner.expect('to') - final = self.literal_int_const() + final = self.const() block = self.block() return For(self.scanner.line_number, dest=dest, direction=direction, final=final, block=block) elif self.scanner.token in ("ld",): diff --git a/tests/SixtyPical Syntax.md b/tests/SixtyPical Syntax.md index 2add5ea..fd2b432 100644 --- a/tests/SixtyPical Syntax.md +++ b/tests/SixtyPical Syntax.md @@ -228,6 +228,31 @@ Can't have two typedefs with the same name. | } ? SyntaxError +Constants. + + | const lives 3 + | const days lives + | const w1 1000 + | const w2 word 0 + | + | typedef byte table[days] them + | + | byte lark: lives + | + | routine main { + | ld a, lives + | } + = ok + +Can't have two constants with the same name. + + | const w1 1000 + | const w1 word 0 + | + | routine main { + | } + ? SyntaxError + Explicit memory address. | byte screen @ 1024 From d9e625db302843753ee68ef5d9f6706ef20e25c8 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 27 Mar 2018 15:55:29 +0100 Subject: [PATCH 02/42] Clean up driver code, add filename to error messages. --- README.md | 2 +- bin/sixtypical | 132 +++++++++++++++++++------------------- src/sixtypical/parser.py | 8 +-- src/sixtypical/scanner.py | 20 +++--- 4 files changed, 82 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 8456707..8fe4e74 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ SixtyPical ========== -_Version 0.14. Work-in-progress, everything is subject to change._ +_Version 0.15. Work-in-progress, everything is subject to change._ **SixtyPical** is a 6502-like programming language with advanced static analysis. diff --git a/bin/sixtypical b/bin/sixtypical index 710926a..0f621b6 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -24,6 +24,66 @@ from sixtypical.emitter import Emitter, Byte, Word from sixtypical.compiler import Compiler +def process_input_files(filenames, options): + programs = [] + + for filename in options.filenames: + text = open(filename).read() + parser = Parser(text, filename) + program = parser.program() + programs.append(program) + + if options.parse_only: + return + + #program = merge_programs(programs) + program = programs[0] + + analyzer = Analyzer(debug=options.debug) + analyzer.analyze_program(program) + + if options.analyze_only: + return + + fh = sys.stdout + + if options.origin.startswith('0x'): + start_addr = int(options.origin, 16) + else: + start_addr = int(options.origin, 10) + + output_format = options.output_format + + prelude = [] + if options.prelude == 'c64': + output_format = 'prg' + start_addr = 0x0801 + prelude = [0x10, 0x08, 0xc9, 0x07, 0x9e, 0x32, + 0x30, 0x36, 0x31, 0x00, 0x00, 0x00] + elif options.prelude == 'vic20': + output_format = 'prg' + start_addr = 0x1001 + prelude = [0x0b, 0x10, 0xc9, 0x07, 0x9e, 0x34, + 0x31, 0x30, 0x39, 0x00, 0x00, 0x00] + elif options.prelude: + raise NotImplementedError("Unknown prelude: {}".format(options.prelude)) + + # If we are outputting a .PRG, we output the load address first. + # We don't use the Emitter for this b/c not part of addr space. + if output_format == 'prg': + fh.write(Word(start_addr).serialize(0)) + + emitter = Emitter(start_addr) + for byte in prelude: + emitter.emit(Byte(byte)) + compiler = Compiler(emitter) + compiler.compile_program(program) + if options.debug: + pprint(emitter.accum) + else: + emitter.serialize(fh) + + if __name__ == '__main__': argparser = ArgumentParser(__doc__.strip()) @@ -72,69 +132,11 @@ if __name__ == '__main__': options, unknown = argparser.parse_known_args(sys.argv[1:]) remainder = ' '.join(unknown) - for filename in options.filenames: - text = open(filename).read() - - try: - parser = Parser(text) - program = parser.program() - except Exception as e: - if options.traceback: - raise - else: - traceback.print_exception(e.__class__, e, None) - sys.exit(1) - - if options.parse_only: - sys.exit(0) - - try: - analyzer = Analyzer(debug=options.debug) - analyzer.analyze_program(program) - except Exception as e: - if options.traceback: - raise - else: - traceback.print_exception(e.__class__, e, None) - sys.exit(1) - - if options.analyze_only: - sys.exit(0) - - fh = sys.stdout - - if options.origin.startswith('0x'): - start_addr = int(options.origin, 16) + try: + process_input_files(options.filenames, options) + except Exception as e: + if options.traceback: + raise else: - start_addr = int(options.origin, 10) - - output_format = options.output_format - - prelude = [] - if options.prelude == 'c64': - output_format = 'prg' - start_addr = 0x0801 - prelude = [0x10, 0x08, 0xc9, 0x07, 0x9e, 0x32, - 0x30, 0x36, 0x31, 0x00, 0x00, 0x00] - elif options.prelude == 'vic20': - output_format = 'prg' - start_addr = 0x1001 - prelude = [0x0b, 0x10, 0xc9, 0x07, 0x9e, 0x34, - 0x31, 0x30, 0x39, 0x00, 0x00, 0x00] - elif options.prelude: - raise NotImplementedError("Unknown prelude: {}".format(options.prelude)) - - # If we are outputting a .PRG, we output the load address first. - # We don't use the Emitter for this b/c not part of addr space. - if output_format == 'prg': - fh.write(Word(start_addr).serialize(0)) - - emitter = Emitter(start_addr) - for byte in prelude: - emitter.emit(Byte(byte)) - compiler = Compiler(emitter) - compiler.compile_program(program) - if options.debug: - pprint(emitter.accum) - else: - emitter.serialize(fh) + traceback.print_exception(e.__class__, e, None) + sys.exit(1) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 22ab9ec..c2bb76e 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, SixtyPicalSyntaxError +from sixtypical.scanner import Scanner class SymEntry(object): @@ -19,8 +19,8 @@ class SymEntry(object): class Parser(object): - def __init__(self, text): - self.scanner = Scanner(text) + def __init__(self, text, filename): + self.scanner = Scanner(text, filename) self.symbols = {} # token -> SymEntry self.current_statics = {} # token -> SymEntry self.typedefs = {} # token -> Type AST @@ -32,7 +32,7 @@ class Parser(object): self.backpatch_instrs = [] def syntax_error(self, msg): - raise SixtyPicalSyntaxError(self.scanner.line_number, msg) + self.scanner.syntax_error(msg) def soft_lookup(self, name): if name in self.current_statics: diff --git a/src/sixtypical/scanner.py b/src/sixtypical/scanner.py index d15ac4f..a85da67 100644 --- a/src/sixtypical/scanner.py +++ b/src/sixtypical/scanner.py @@ -4,16 +4,17 @@ import re class SixtyPicalSyntaxError(ValueError): - def __init__(self, line_number, message): - super(SixtyPicalSyntaxError, self).__init__(line_number, message) + def __init__(self, filename, line_number, message): + super(SixtyPicalSyntaxError, self).__init__(filename, line_number, message) def __str__(self): - return "Line {}: {}".format(self.args[0], self.args[1]) + return "{}, line {}: {}".format(self.args[0], self.args[1], self.args[2]) class Scanner(object): - def __init__(self, text): + def __init__(self, text, filename): self.text = text + self.filename = filename self.token = None self.type = None self.line_number = 1 @@ -62,9 +63,7 @@ class Scanner(object): if self.token == token: self.scan() else: - raise SixtyPicalSyntaxError(self.line_number, "Expected '{}', but found '{}'".format( - token, self.token - )) + self.syntax_error("Expected '{}', but found '{}'".format(token, self.token)) def on(self, *tokens): return self.token in tokens @@ -74,9 +73,7 @@ class Scanner(object): def check_type(self, type): if not self.type == type: - raise SixtyPicalSyntaxError(self.line_number, "Expected {}, but found '{}'".format( - self.type, self.token - )) + self.syntax_error("Expected {}, but found '{}'".format(self.type, self.token)) def consume(self, token): if self.token == token: @@ -84,3 +81,6 @@ class Scanner(object): return True else: return False + + def syntax_error(self, msg): + raise SixtyPicalSyntaxError(self.filename, self.line_number, msg) From 6744ad29a9e3d75b997ae32ef179aaa8f4193131 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 27 Mar 2018 16:23:22 +0100 Subject: [PATCH 03/42] Beginnings of modularity. --- README.md | 3 +- bin/sixtypical | 24 ++++++++++-- eg/rudiments/vector-inc.60p | 18 +++++++++ eg/rudiments/vector-main.60p | 19 ++++++++++ src/sixtypical/parser.py | 73 ++++++++++++++++++++---------------- 5 files changed, 99 insertions(+), 38 deletions(-) create mode 100644 eg/rudiments/vector-inc.60p create mode 100644 eg/rudiments/vector-main.60p diff --git a/README.md b/README.md index 8fe4e74..712795f 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ is probably NP-complete. But doing it adequately is probably not that hard. * Tests, and implementation, ensuring a routine can be assigned to a vector of "wider" type * Related: can we simply view a (small) part of a buffer as a byte table? If not, why not? * Related: add constant to buffer to get new buffer. (Or to table, but... well, maybe.) -* Check that the buffer being read or written to through pointer, appears in approporiate inputs or outputs set. +* Check that the buffer being read or written to through pointer, appears in appropriate inputs or outputs set. (Associate each pointer with the buffer it points into.) * `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. @@ -94,6 +94,5 @@ is probably NP-complete. But doing it adequately is probably not that hard. * 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! * Optimize `ld a, z` and `st a, z` to zero-page operations if address of z < 256. -* Include files? [VICE]: http://vice-emu.sourceforge.net/ diff --git a/bin/sixtypical b/bin/sixtypical index 0f621b6..299ba47 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -18,26 +18,42 @@ from pprint import pprint import sys import traceback -from sixtypical.parser import Parser +from sixtypical.parser import Parser, ParsingContext from sixtypical.analyzer import Analyzer from sixtypical.emitter import Emitter, Byte, Word from sixtypical.compiler import Compiler +def merge_programs(programs): + """Assumes that the programs do not have any conflicts.""" + + from sixtypical.ast import Program + + full = Program(1, defns=[], routines=[]) + for p in programs: + full.defns.extend(p.defns) + full.routines.extend(p.routines) + + return full + + def process_input_files(filenames, options): + context = ParsingContext() + programs = [] for filename in options.filenames: text = open(filename).read() - parser = Parser(text, filename) + parser = Parser(context, text, filename) + if options.debug: + print(context) program = parser.program() programs.append(program) if options.parse_only: return - #program = merge_programs(programs) - program = programs[0] + program = merge_programs(programs) analyzer = Analyzer(debug=options.debug) analyzer.analyze_program(program) diff --git a/eg/rudiments/vector-inc.60p b/eg/rudiments/vector-inc.60p new file mode 100644 index 0000000..6e97d74 --- /dev/null +++ b/eg/rudiments/vector-inc.60p @@ -0,0 +1,18 @@ +routine chrout + inputs a + trashes a + @ 65490 + +routine printa + trashes a, z, n +{ + ld a, 65 + call chrout +} + +routine printb + trashes a, z, n +{ + ld a, 66 + call chrout +} diff --git a/eg/rudiments/vector-main.60p b/eg/rudiments/vector-main.60p new file mode 100644 index 0000000..bffdf5e --- /dev/null +++ b/eg/rudiments/vector-main.60p @@ -0,0 +1,19 @@ +vector routine + trashes a, z, n + print + +// routine printb +// trashes a, z, n +// { +// ld a, 66 +// call chrout +// } + +routine main + trashes print, a, z, n +{ + copy printa, print + call print + copy printb, print + call print +} diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index c2bb76e..084af49 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -18,27 +18,36 @@ class SymEntry(object): return "%s(%r, %r)" % (self.__class__.__name__, self.ast_node, self.model) -class Parser(object): - def __init__(self, text, filename): - self.scanner = Scanner(text, filename) +class ParsingContext(object): + def __init__(self): self.symbols = {} # token -> SymEntry - self.current_statics = {} # token -> SymEntry + self.statics = {} # token -> SymEntry self.typedefs = {} # token -> Type AST self.consts = {} # token -> Loc + for token in ('a', 'x', 'y'): self.symbols[token] = SymEntry(None, LocationRef(TYPE_BYTE, token)) for token in ('c', 'z', 'n', 'v'): self.symbols[token] = SymEntry(None, LocationRef(TYPE_BIT, token)) + + def __str__(self): + return "Symbols: {}\nStatics: {}\nTypedefs: {}\nConsts: {}".format(self.symbols, self.statics, self.typedefs, self.consts) + + +class Parser(object): + def __init__(self, context, text, filename): + self.context = context + self.scanner = Scanner(text, filename) self.backpatch_instrs = [] def syntax_error(self, msg): self.scanner.syntax_error(msg) def soft_lookup(self, name): - if name in self.current_statics: - return self.current_statics[name].model - if name in self.symbols: - return self.symbols[name].model + if name in self.context.statics: + return self.context.statics[name].model + if name in self.context.symbols: + return self.context.symbols[name].model return None def lookup(self, name): @@ -58,13 +67,13 @@ class Parser(object): if self.scanner.on('const'): self.defn_const() typenames = ['byte', 'word', 'table', 'vector', 'buffer', 'pointer'] # 'routine', - typenames.extend(self.typedefs.keys()) + typenames.extend(self.context.typedefs.keys()) while self.scanner.on(*typenames): defn = self.defn() name = defn.name - if name in self.symbols: + if name in self.context.symbols: self.syntax_error('Symbol "%s" already declared' % name) - self.symbols[name] = SymEntry(defn, defn.location) + self.context.symbols[name] = SymEntry(defn, defn.location) defns.append(defn) while self.scanner.on('define', 'routine'): if self.scanner.consume('define'): @@ -74,14 +83,14 @@ class Parser(object): else: routine = self.legacy_routine() name = routine.name - if name in self.symbols: + if name in self.context.symbols: self.syntax_error('Symbol "%s" already declared' % name) - self.symbols[name] = SymEntry(routine, routine.location) + self.context.symbols[name] = SymEntry(routine, routine.location) routines.append(routine) self.scanner.check_type('EOF') # now backpatch the executable types. - #for type_name, type_ in self.typedefs.iteritems(): + #for type_name, type_ in self.context.typedefs.iteritems(): # type_.backpatch_constraint_labels(lambda w: self.lookup(w)) for defn in defns: defn.location.type.backpatch_constraint_labels(lambda w: self.lookup(w)) @@ -90,18 +99,18 @@ class Parser(object): for instr in self.backpatch_instrs: if instr.opcode in ('call', 'goto'): name = instr.location - if name not in self.symbols: + if name not in self.context.symbols: self.syntax_error('Undefined routine "%s"' % name) - if not isinstance(self.symbols[name].model.type, (RoutineType, VectorType)): + if not isinstance(self.context.symbols[name].model.type, (RoutineType, VectorType)): self.syntax_error('Illegal call of non-executable "%s"' % name) - instr.location = self.symbols[name].model + instr.location = self.context.symbols[name].model if instr.opcode in ('copy',) and isinstance(instr.src, basestring): name = instr.src - if name not in self.symbols: + if name not in self.context.symbols: self.syntax_error('Undefined routine "%s"' % name) - if not isinstance(self.symbols[name].model.type, (RoutineType, VectorType)): + if not isinstance(self.context.symbols[name].model.type, (RoutineType, VectorType)): self.syntax_error('Illegal copy of non-executable "%s"' % name) - instr.src = self.symbols[name].model + instr.src = self.context.symbols[name].model return Program(self.scanner.line_number, defns=defns, routines=routines) @@ -109,18 +118,18 @@ class Parser(object): self.scanner.expect('typedef') type_ = self.defn_type() name = self.defn_name() - if name in self.typedefs: + if name in self.context.typedefs: self.syntax_error('Type "%s" already declared' % name) - self.typedefs[name] = type_ + self.context.typedefs[name] = type_ return type_ def defn_const(self): self.scanner.expect('const') name = self.defn_name() - if name in self.consts: + if name in self.context.consts: self.syntax_error('Const "%s" already declared' % name) loc = self.const() - self.consts[name] = loc + self.context.consts[name] = loc return loc def defn(self): @@ -163,8 +172,8 @@ class Parser(object): loc = ConstantRef(TYPE_WORD, int(self.scanner.token)) self.scanner.scan() return loc - elif self.scanner.token in self.consts: - loc = self.consts[self.scanner.token] + elif self.scanner.token in self.context.consts: + loc = self.context.consts[self.scanner.token] self.scanner.scan() return loc else: @@ -215,9 +224,9 @@ class Parser(object): else: type_name = self.scanner.token self.scanner.scan() - if type_name not in self.typedefs: + if type_name not in self.context.typedefs: self.syntax_error("Undefined type '%s'" % type_name) - type_ = self.typedefs[type_name] + type_ = self.context.typedefs[type_name] return type_ @@ -273,9 +282,9 @@ class Parser(object): else: statics = self.statics() - self.current_statics = self.compose_statics_dict(statics) + self.context.statics = self.compose_statics_dict(statics) block = self.block() - self.current_statics = {} + self.context.statics = {} addr = None location = LocationRef(type_, name) @@ -289,7 +298,7 @@ class Parser(object): c = {} for defn in statics: name = defn.name - if name in self.symbols or name in self.current_statics: + if name in self.context.symbols or name in self.context.statics: self.syntax_error('Symbol "%s" already declared' % name) c[name] = SymEntry(defn, defn.location) return c @@ -316,7 +325,7 @@ class Parser(object): return accum def locexpr(self, forward=False): - if self.scanner.token in ('on', 'off', 'word') or self.scanner.token in self.consts or self.scanner.on_type('integer literal'): + if self.scanner.token in ('on', 'off', 'word') or self.scanner.token in self.context.consts or self.scanner.on_type('integer literal'): return self.const() elif forward: name = self.scanner.token From c707105cd3c91447d7decb3eca2b042d343ce9a2 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 27 Mar 2018 16:49:20 +0100 Subject: [PATCH 04/42] Describe the behaviour just implemented. --- HISTORY.md | 2 ++ eg/rudiments/vector-inc.60p | 3 +++ eg/rudiments/vector-main.60p | 3 +++ 3 files changed, 8 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index a73ba45..b3aad96 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,8 @@ History of SixtyPical * Symbolic constants can be defined with the `const` keyword, and can be used in most places where literal values can be used. +* Specifying multiple SixtyPical source files will produce a single + compiled result from their combination. 0.14 ---- diff --git a/eg/rudiments/vector-inc.60p b/eg/rudiments/vector-inc.60p index 6e97d74..69a69c7 100644 --- a/eg/rudiments/vector-inc.60p +++ b/eg/rudiments/vector-inc.60p @@ -1,3 +1,6 @@ +// This will not compile on its own, because there is no `main`. +// But this and `vector-main.60p` together will compile. + routine chrout inputs a trashes a diff --git a/eg/rudiments/vector-main.60p b/eg/rudiments/vector-main.60p index bffdf5e..b6e45e9 100644 --- a/eg/rudiments/vector-main.60p +++ b/eg/rudiments/vector-main.60p @@ -1,3 +1,6 @@ +// This will not compile on its own, because `printa` and `printb` are not defined. +// But `vector-inc.60p` and this together will compile. + vector routine trashes a, z, n print From f86041721dd3784ce78dfc4b23f8fcdc20ac6a50 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 27 Mar 2018 17:05:41 +0100 Subject: [PATCH 05/42] Try to spell out my idea for an algorithm for this. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 712795f..8b898fb 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,16 @@ 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. +> Every routine is falled through to by zero or more routines. +> Don't consider the main routine. +> For each routine α that is finally-falled through to by a set of routines R(α), +> pick a movable routine β from R, move β in front of α, remove the `jmp` at the end of β and +> mark β as unmovable. +> Note this only works if β finally-falls through. If there are multiple tail +> positions, we can't eliminate all the `jmp`s. +> Note that if β finally-falls through to α it can't finally-fall through to anything +> else, so the sets R(α) should be disjoint for every α. (Right?) + ### And at some point... * `low` and `high` address operators - to turn `word` type into `byte`. From bb0e7aa99262ddfdb527b1f2ff524ac9e6ca2560 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 27 Mar 2018 17:27:09 +0100 Subject: [PATCH 06/42] Track the gotos that we have encountered in a routine. --- src/sixtypical/analyzer.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 6dc3262..3c4394b 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -101,7 +101,7 @@ class Context(object): self._touched = set() self._range = dict() self._writeable = set() - self._has_encountered_goto = False + self._gotos_encountered = set() for ref in inputs: if ref.is_constant(): @@ -273,11 +273,11 @@ class Context(object): for ref in refs: self._writeable.add(ref) - def set_encountered_goto(self): - self._has_encountered_goto = True + def encounter_gotos(self, gotos): + self._gotos_encountered |= gotos - def has_encountered_goto(self): - return self._has_encountered_goto + def encountered_gotos(self): + return self._gotos_encountered class Analyzer(object): @@ -346,7 +346,7 @@ class Analyzer(object): if ref in type_.outputs: raise UnmeaningfulOutputError(routine, ref.name) - if not context.has_encountered_goto(): + if not context.encountered_gotos(): for ref in type_.outputs: context.assert_meaningful(ref, exception_class=UnmeaningfulOutputError) for ref in context.each_touched(): @@ -379,7 +379,7 @@ class Analyzer(object): dest = instr.dest src = instr.src - if context.has_encountered_goto(): + if context.encountered_gotos(): raise IllegalJumpError(instr, instr) if opcode == 'ld': @@ -595,7 +595,7 @@ class Analyzer(object): self.assert_affected_within('outputs', type_, current_type) self.assert_affected_within('trashes', type_, current_type) - context.set_encountered_goto() + context.encounter_gotos(set([instr.location])) elif opcode == 'trash': context.set_touched(instr.dest) context.set_unmeaningful(instr.dest) @@ -636,8 +636,7 @@ class Analyzer(object): context._touched = set(context1._touched) | set(context2._touched) context.set_meaningful(*list(outgoing_meaningful)) context._writeable = set(context1._writeable) | set(context2._writeable) - if context1.has_encountered_goto() or context2.has_encountered_goto(): - context.set_encountered_goto() + context.encounter_gotos(context1.encountered_gotos() | context2.encountered_gotos()) for ref in outgoing_trashes: context.set_touched(ref) From 0093c7b7d9a8bb453398d8d2c6c458ca59b1a85b Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 28 Mar 2018 14:20:53 +0100 Subject: [PATCH 07/42] First cut at support for targetting the Atari 2600. --- HISTORY.md | 2 + bin/sixtypical | 24 +- eg/README.md | 7 + eg/atari2600/atari-2600-example.60p | 168 ++++++++++++++ eg/atari2600/atari-2600-example.oph | 340 ++++++++++++++++++++++++++++ loadngo.sh | 5 +- src/sixtypical/emitter.py | 11 + 7 files changed, 551 insertions(+), 6 deletions(-) create mode 100644 eg/atari2600/atari-2600-example.60p create mode 100644 eg/atari2600/atari-2600-example.oph diff --git a/HISTORY.md b/HISTORY.md index b3aad96..50cdde1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -8,6 +8,8 @@ History of SixtyPical be used in most places where literal values can be used. * Specifying multiple SixtyPical source files will produce a single compiled result from their combination. +* Rudimentary support for Atari 2600 prelude in a 4K cartridge image, + and start of an example program in `eg/atari2600` directory. 0.14 ---- diff --git a/bin/sixtypical b/bin/sixtypical index 299ba47..b296f94 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -81,6 +81,12 @@ def process_input_files(filenames, options): start_addr = 0x1001 prelude = [0x0b, 0x10, 0xc9, 0x07, 0x9e, 0x34, 0x31, 0x30, 0x39, 0x00, 0x00, 0x00] + elif options.prelude == 'atari2600': + output_format = 'crtbb' + start_addr = 0xf000 + prelude = [0x78, 0xd8, 0xa2, 0xff, 0x9a, 0xa9, + 0x00,0x95, 0x00, 0xca, 0xd0, 0xfb] + elif options.prelude: raise NotImplementedError("Unknown prelude: {}".format(options.prelude)) @@ -94,6 +100,14 @@ def process_input_files(filenames, options): emitter.emit(Byte(byte)) compiler = Compiler(emitter) compiler.compile_program(program) + + # If we are outputting a cartridge with boot and BRK address + # at the end, pad to ROM size minus 4 bytes, and emit addresses. + if output_format == 'crtbb': + emitter.pad_to_size(4096 - 4) + emitter.emit(Word(start_addr)) + emitter.emit(Word(start_addr)) + if options.debug: pprint(emitter.accum) else: @@ -119,15 +133,15 @@ if __name__ == '__main__': ) argparser.add_argument( "--output-format", type=str, default='prg', - help="Executable format to produce. Options are: prg (.PRG file " - "for Commodore 8-bit). Default: prg." + help="Executable format to produce. Options are: prg, crtbb. " + "Default: prg." ) argparser.add_argument( "--prelude", type=str, - help="Insert a snippet before the compiled program " - "so that it can be LOADed and RUN on a certain platforms. " + help="Insert a snippet of code before the compiled program so that " + "it can be booted automatically on a particular platform. " "Also sets the origin and format. " - "Options are: c64 or vic20." + "Options are: c64, vic20, atari2600." ) argparser.add_argument( "--debug", diff --git a/eg/README.md b/eg/README.md index 79c0d91..a805a60 100644 --- a/eg/README.md +++ b/eg/README.md @@ -38,4 +38,11 @@ In the [vic20](vic20/) directory are programs that run on the Commodore VIC-20. The directory itself contains some simple demos, for example [hearts.60p](vic20/hearts.60p). +### atari2600 + +In the [vic20](vic20/) directory are programs that run on the +Atari 2600 (4K cartridge). The directory itself contains a simple +demo, [atari-2600-example.60p](atari2600/atari-2600-example.60p). +(Doesn't work yet.) + [Ophis]: http://michaelcmartin.github.io/Ophis/ diff --git a/eg/atari2600/atari-2600-example.60p b/eg/atari2600/atari-2600-example.60p new file mode 100644 index 0000000..04b94a0 --- /dev/null +++ b/eg/atari2600/atari-2600-example.60p @@ -0,0 +1,168 @@ +// atari-2600-example.60p - SixtyPical translation of atari-2600-example.oph + +byte VSYNC @ $00 +byte VBLANK @ $01 +byte WSYNC @ $02 +byte NUSIZ0 @ $04 +byte NUSIZ1 @ $05 +byte COLUPF @ $08 +byte COLUBK @ $09 +byte PF0 @ $0D +byte PF1 @ $0E +byte PF2 @ $0F +byte SWCHA @ $280 +byte INTIM @ $284 +byte TIM64T @ $296 +byte CTRLPF @ $0A +byte COLUP0 @ $06 +byte COLUP1 @ $07 +byte GP0 @ $1B +byte GP1 @ $1C +byte HMOVE @ $2a +byte RESP0 @ $10 +byte RESP1 @ $11 + +byte colour @ $80 +byte luminosity @ $81 +byte joystick_delay @ $82 + +byte table[8] image_data : "ZZZZUUUU" + // %01111110 + // %10000001 + // %10011001 + // %10100101 + // %10000001 + // %10100101 + // %10000001 + // %01111110 + + +define vertical_blank routine + outputs VSYNC, WSYNC, TIM64T + trashes a, x, z, n +{ + ld x, $00 + ld a, $02 + st a, WSYNC + st a, WSYNC + st a, WSYNC + st a, VSYNC + st a, WSYNC + st a, WSYNC + ld a, $2C + st a, TIM64T + ld a, $00 + st a, WSYNC + st a, VSYNC +} + +define display_frame routine + inputs INTIM, image_data + outputs WSYNC, HMOVE, VBLANK, RESP0, GP0, PF0, PF1, PF2, COLUPF, COLUBK + trashes a, x, y, z, n +{ + repeat { + ld a, INTIM + } until z + + //; (After that loop finishes, we know the accumulator must contain 0.) + + st a, WSYNC + st a, HMOVE + st a, VBLANK + + //; + //; Wait for $3f (plus one?) scan lines to pass, by waiting for + //; WSYNC that many times. + //; + + ld x, $3F + repeat { + st a, WSYNC + dec x + } until z // FIXME orig loop used "bpl _wsync_loop" + st a, WSYNC + + //; + //; Delay while the raster scans across the screen. The more + //; we delay here, the more to the right the player will be when + //; we draw it. + //; + + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + //// nop + + //; + //; OK, *now* display the player. + //; + + st a, RESP0 + + //; + //; Loop over the rows of the sprite data, drawing each to the screen + //; over four scan lines. + //; + //; TODO understand this better and describe it! + //; + + ld y, $07 + for y down to 0 { + ld a, image_data + y + st a, GP0 + + st a, WSYNC + st a, WSYNC + st a, WSYNC + st a, WSYNC + } // FIXME original was "dec y; bpl _image_loop" + + ld a, $00 + st a, GP0 + + //; + //; Turn off screen display and clear display registers. + //; + + ld a, $02 + st a, WSYNC + st a, VBLANK + ld a, $00 + st a, PF0 + st a, PF1 + st a, PF2 + st a, COLUPF + st a, COLUBK +} + +define main routine + inputs image_data, INTIM + outputs CTRLPF, colour, luminosity, NUSIZ0, VSYNC, WSYNC, TIM64T, HMOVE, VBLANK, RESP0, GP0, PF0, PF1, PF2, COLUPF, COLUBK + trashes a, x, y, z, n +{ + ld a, $00 + st a, CTRLPF + ld a, $0c + st a, colour + ld a, $0a + st a, luminosity + ld a, $00 + st a, NUSIZ0 + repeat { + call vertical_blank + call display_frame + // call read_joystick + } forever +} diff --git a/eg/atari2600/atari-2600-example.oph b/eg/atari2600/atari-2600-example.oph new file mode 100644 index 0000000..836a64a --- /dev/null +++ b/eg/atari2600/atari-2600-example.oph @@ -0,0 +1,340 @@ +; +; atari-2600-example.oph +; Skeleton code for an Atari 2600 ROM, +; plus an example of reading the joystick. +; By Chris Pressey, November 2, 2012. +; +; This work is in the public domain. See the file UNLICENSE for more info. +; +; Based on Chris Cracknell's Atari 2600 clock (also in the public domain): +; http://everything2.com/title/An+example+of+Atari+2600+source+code +; +; to build and run in Stella: +; ophis atari-2600-example.oph -o example.bin +; stella example.bin +; +; More useful information can be found in the Stella Programmer's Guide: +; http://alienbill.com/2600/101/docs/stella.html +; + +; +; Useful system addresses (TODO: briefly describe each of these.) +; + +.alias VSYNC $00 +.alias VBLANK $01 +.alias WSYNC $02 +.alias NUSIZ0 $04 +.alias NUSIZ1 $05 +.alias COLUPF $08 +.alias COLUBK $09 +.alias PF0 $0D +.alias PF1 $0E +.alias PF2 $0F +.alias SWCHA $280 +.alias INTIM $284 +.alias TIM64T $296 +.alias CTRLPF $0A +.alias COLUP0 $06 +.alias COLUP1 $07 +.alias GP0 $1B +.alias GP1 $1C +.alias HMOVE $2a +.alias RESP0 $10 +.alias RESP1 $11 + +; +; Cartridge ROM occupies the top 4K of memory ($F000-$FFFF). +; Thus, typically, the program will occupy all that space too. +; +; Zero-page RAM we can use with impunity starts at $80 and goes +; upward (at least until $99, but probably further.) +; + +.alias colour $80 +.alias luminosity $81 +.alias joystick_delay $82 + +.org $F000 + +; +; Standard prelude for Atari 2600 cartridge code. +; +; Get various parts of the machine into a known state: +; +; - Disable interrupts +; - Clear the Decimal flag +; - Initialize the Stack Pointer +; - Zero all bytes in Zero Page memory +; + +start: + sei + cld + ldx #$FF + txs + lda #$00 + +zero_loop: + sta $00, x + dex + bne zero_loop + + ; and fall through to... + +; +; Initialization. +; +; - Clear the Playfield Control register. +; - Set the player (sprite) colour to light green (write to COLUP0.) +; - Set the player (sprite) size/repetion to normal (write to NUSIZ0.) +; + + lda #$00 + sta CTRLPF + lda #$0c + sta colour + lda #$0a + sta luminosity + lda #$00 + sta NUSIZ0 + + ; and fall through to... + +; +; Main loop. +; +; A typical main loop consists of: +; - Waiting for the frame to start (vertical blank period) +; - Displaying stuff on the screen (the _display kernel_) +; - Doing any processing you like (reading joysticks, updating program state, +; etc.), as long as you get it all done before the next frame starts! +; + +main: + jsr vertical_blank + jsr display_frame + jsr read_joystick + jmp main + +; +; Vertical blank routine. +; +; In brief: wait until it is time for the next frame of video. +; TODO: describe this in more detail. +; + +vertical_blank: + ldx #$00 + lda #$02 + sta WSYNC + sta WSYNC + sta WSYNC + sta VSYNC + sta WSYNC + sta WSYNC + lda #$2C + sta TIM64T + lda #$00 + sta WSYNC + sta VSYNC + rts + +; +; Display kernal. +; +; First, wait until it's time to display the frame. +; + +.scope +display_frame: + lda INTIM + bne display_frame + +; +; (After that loop finishes, we know the accumulator must contain 0.) +; Wait for the next scanline, zero HMOVE (for some reason; TODO discover +; this), then turn on the screen. +; + + sta WSYNC + sta HMOVE + sta VBLANK + +; +; Actual work in the display kernal is done here. +; +; This is a pathological approach to writing a display kernal. +; This wouldn't be how you'd do things in a game. So be it. +; One day I may improve it. For now, be happy that it displays +; anything at all! +; + +; +; Wait for $3f (plus one?) scan lines to pass, by waiting for +; WSYNC that many times. +; + + ldx #$3F +_wsync_loop: + sta WSYNC + dex + bpl _wsync_loop + sta WSYNC + +; +; Delay while the raster scans across the screen. The more +; we delay here, the more to the right the player will be when +; we draw it. +; + + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + +; +; OK, *now* display the player. +; + + sta RESP0 + +; +; Loop over the rows of the sprite data, drawing each to the screen +; over four scan lines. +; +; TODO understand this better and describe it! +; + + ldy #$07 +_image_loop: + lda image_data, y + sta GP0 + + sta WSYNC + sta WSYNC + sta WSYNC + sta WSYNC + dey + bpl _image_loop + + lda #$00 + sta GP0 + +; +; Turn off screen display and clear display registers. +; + + lda #$02 + sta WSYNC + sta VBLANK + lda #$00 + sta PF0 + sta PF1 + sta PF2 + sta COLUPF + sta COLUBK + + rts +.scend + + +; +; Read the joystick and use it to modify the colour and luminosity +; of the player. +; + +.scope +read_joystick: + lda joystick_delay + beq _continue + + dec joystick_delay + rts + +_continue: + lda SWCHA + and #$f0 + cmp #$e0 + beq _up + cmp #$d0 + beq _down + cmp #$b0 + beq _left + cmp #$70 + beq _right + jmp _tail + +_up: + inc luminosity + jmp _tail +_down: + dec luminosity + jmp _tail +_left: + dec colour + jmp _tail +_right: + inc colour + ;jmp _tail + +_tail: + lda colour + and #$0f + sta colour + + lda luminosity + and #$0f + sta luminosity + + lda colour + clc + rol + rol + rol + rol + ora luminosity + sta COLUP0 + + lda #$06 + sta joystick_delay + + rts +.scend + +; +; Player (sprite) data. +; +; Because we loop over these bytes with the Y register counting *down*, +; this image is stored "upside-down". +; + +image_data: + .byte %01111110 + .byte %10000001 + .byte %10011001 + .byte %10100101 + .byte %10000001 + .byte %10100101 + .byte %10000001 + .byte %01111110 + +; +; Standard postlude for Atari 2600 cartridge code. +; Give BRK and boot vectors that point to the start of the code. +; + +.advance $FFFC + .word start + .word start diff --git a/loadngo.sh b/loadngo.sh index b3d0e1d..ca9f198 100755 --- a/loadngo.sh +++ b/loadngo.sh @@ -1,6 +1,6 @@ #!/bin/sh -usage="Usage: loadngo.sh (c64|vic20) [--dry-run] " +usage="Usage: loadngo.sh (c64|vic20|atari2600) [--dry-run] " arch="$1" shift 1 @@ -18,6 +18,9 @@ elif [ "X$arch" = "Xvic20" ]; then else emu="xvic" fi +elif [ "X$arch" = "Xatari2600" ]; then + prelude='atari2600' + emu='stella' else echo $usage && exit 1 fi diff --git a/src/sixtypical/emitter.py b/src/sixtypical/emitter.py index aa14a59..78dbf95 100644 --- a/src/sixtypical/emitter.py +++ b/src/sixtypical/emitter.py @@ -186,3 +186,14 @@ class Emitter(object): advance the address for the next label, but don't emit anything.""" self.resolve_label(label) self.addr += label.length + + def size(self): + return sum(emittable.size() for emittable in self.accum) + + def pad_to_size(self, size): + self_size = self.size() + if self_size > size: + raise IndexError("Emitter size {} exceeds pad size {}".format(self_size, size)) + num_bytes = size - self_size + if num_bytes > 0: + self.accum.extend([Byte(0)] * num_bytes) From 1f992f8dbdc35a247af29e230ca063f82b1dc6cc Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 28 Mar 2018 14:52:16 +0100 Subject: [PATCH 08/42] Support of NOP opcode. --- HISTORY.md | 1 + eg/atari2600/.gitignore | 2 + eg/atari2600/atari-2600-example.60p | 32 ++++---- eg/atari2600/atari-2600-example.oph | 116 ++++++++++++++-------------- eg/atari2600/build.sh | 9 +++ src/sixtypical/analyzer.py | 2 + src/sixtypical/compiler.py | 3 + src/sixtypical/gen6502.py | 6 ++ src/sixtypical/parser.py | 4 + tests/SixtyPical Analysis.md | 22 ++++-- tests/SixtyPical Compilation.md | 9 +++ tests/SixtyPical Syntax.md | 8 ++ 12 files changed, 134 insertions(+), 80 deletions(-) create mode 100644 eg/atari2600/.gitignore create mode 100755 eg/atari2600/build.sh diff --git a/HISTORY.md b/HISTORY.md index 50cdde1..e2c2dbc 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -8,6 +8,7 @@ History of SixtyPical be used in most places where literal values can be used. * Specifying multiple SixtyPical source files will produce a single compiled result from their combination. +* Added `nop` opcode, which compiles to `NOP` (mainly for timing.) * Rudimentary support for Atari 2600 prelude in a 4K cartridge image, and start of an example program in `eg/atari2600` directory. diff --git a/eg/atari2600/.gitignore b/eg/atari2600/.gitignore new file mode 100644 index 0000000..92ac558 --- /dev/null +++ b/eg/atari2600/.gitignore @@ -0,0 +1,2 @@ +*.bin +*.disasm.txt diff --git a/eg/atari2600/atari-2600-example.60p b/eg/atari2600/atari-2600-example.60p index 04b94a0..59d8179 100644 --- a/eg/atari2600/atari-2600-example.60p +++ b/eg/atari2600/atari-2600-example.60p @@ -26,7 +26,7 @@ byte colour @ $80 byte luminosity @ $81 byte joystick_delay @ $82 -byte table[8] image_data : "ZZZZUUUU" +byte table[8] image_data : "ZZZZUUUU" // [126, 129, 153, 165, 129, 165, 129, 126] // %01111110 // %10000001 // %10011001 @@ -89,21 +89,21 @@ define display_frame routine //; we draw it. //; - //// nop - //// nop - //// nop - //// nop - //// nop - //// nop - //// nop - //// nop - //// nop - //// nop - //// nop - //// nop - //// nop - //// nop - //// nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop //; //; OK, *now* display the player. diff --git a/eg/atari2600/atari-2600-example.oph b/eg/atari2600/atari-2600-example.oph index 836a64a..ae33962 100644 --- a/eg/atari2600/atari-2600-example.oph +++ b/eg/atari2600/atari-2600-example.oph @@ -114,7 +114,7 @@ zero_loop: main: jsr vertical_blank jsr display_frame - jsr read_joystick + ;;; jsr read_joystick jmp main ; @@ -255,63 +255,63 @@ _image_loop: ; of the player. ; -.scope -read_joystick: - lda joystick_delay - beq _continue - - dec joystick_delay - rts - -_continue: - lda SWCHA - and #$f0 - cmp #$e0 - beq _up - cmp #$d0 - beq _down - cmp #$b0 - beq _left - cmp #$70 - beq _right - jmp _tail - -_up: - inc luminosity - jmp _tail -_down: - dec luminosity - jmp _tail -_left: - dec colour - jmp _tail -_right: - inc colour - ;jmp _tail - -_tail: - lda colour - and #$0f - sta colour - - lda luminosity - and #$0f - sta luminosity - - lda colour - clc - rol - rol - rol - rol - ora luminosity - sta COLUP0 - - lda #$06 - sta joystick_delay - - rts -.scend +;;; .scope +;;; read_joystick: +;;; lda joystick_delay +;;; beq _continue +;;; +;;; dec joystick_delay +;;; rts +;;; +;;; _continue: +;;; lda SWCHA +;;; and #$f0 +;;; cmp #$e0 +;;; beq _up +;;; cmp #$d0 +;;; beq _down +;;; cmp #$b0 +;;; beq _left +;;; cmp #$70 +;;; beq _right +;;; jmp _tail +;;; +;;; _up: +;;; inc luminosity +;;; jmp _tail +;;; _down: +;;; dec luminosity +;;; jmp _tail +;;; _left: +;;; dec colour +;;; jmp _tail +;;; _right: +;;; inc colour +;;; ;jmp _tail +;;; +;;; _tail: +;;; lda colour +;;; and #$0f +;;; sta colour +;;; +;;; lda luminosity +;;; and #$0f +;;; sta luminosity +;;; +;;; lda colour +;;; clc +;;; rol +;;; rol +;;; rol +;;; rol +;;; ora luminosity +;;; sta COLUP0 +;;; +;;; lda #$06 +;;; sta joystick_delay +;;; +;;; rts +;;; .scend ; ; Player (sprite) data. diff --git a/eg/atari2600/build.sh b/eg/atari2600/build.sh new file mode 100755 index 0000000..1f141c1 --- /dev/null +++ b/eg/atari2600/build.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +sixtypical --prelude=atari2600 atari-2600-example.60p > atari-2600-example-60p.bin +if [ "x$COMPARE" != "x" ]; then + ophis atari-2600-example.oph -o atari-2600-example.bin + dcc6502 -o 0xf000 -m 200 atari-2600-example.bin > atari-2600-example.bin.disasm.txt + dcc6502 -o 0xf000 -m 200 atari-2600-example-60p.bin > atari-2600-example-60p.bin.disasm.txt + paste atari-2600-example.bin.disasm.txt atari-2600-example-60p.bin.disasm.txt | pr -t -e24 +fi diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 3c4394b..b78cf95 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -599,6 +599,8 @@ class Analyzer(object): elif opcode == 'trash': context.set_touched(instr.dest) context.set_unmeaningful(instr.dest) + elif opcode == 'nop': + pass else: raise NotImplementedError(opcode) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index 11d2e9e..b02e717 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -18,6 +18,7 @@ from sixtypical.gen6502 import ( BCC, BCS, BNE, BEQ, JMP, JSR, RTS, SEI, CLI, + NOP, ) @@ -354,6 +355,8 @@ class Compiler(object): self.compile_copy(instr, instr.src, instr.dest) elif opcode == 'trash': pass + elif opcode == 'nop': + self.emitter.emit(NOP()) else: raise NotImplementedError(opcode) diff --git a/src/sixtypical/gen6502.py b/src/sixtypical/gen6502.py index 8659ab6..c880e18 100644 --- a/src/sixtypical/gen6502.py +++ b/src/sixtypical/gen6502.py @@ -312,6 +312,12 @@ class RTS(Instruction): } +class NOP(Instruction): + opcodes = { + Implied: 0xEA, + } + + class SBC(Instruction): opcodes = { Immediate: 0xe9, diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 084af49..d879392 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -440,6 +440,10 @@ class Parser(object): self.scanner.scan() dest = self.locexpr() return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=None) + elif self.scanner.token in ("nop"): + opcode = self.scanner.token + self.scanner.scan() + return SingleOp(self.scanner.line_number, opcode=opcode, dest=None, src=None) elif self.scanner.token in ("call", "goto"): opcode = self.scanner.token self.scanner.scan() diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index 2cbba37..82ad7ec 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -1010,7 +1010,7 @@ Can't `dec` a `word` type. ### cmp ### -Some rudimentary tests for cmp. +Some rudimentary tests for `cmp`. | routine main | inputs a @@ -1037,7 +1037,7 @@ Some rudimentary tests for cmp. ### and ### -Some rudimentary tests for and. +Some rudimentary tests for `and`. | routine main | inputs a @@ -1064,7 +1064,7 @@ Some rudimentary tests for and. ### or ### -Writing unit tests on a train. Wow. +Some rudimentary tests for `or`. | routine main | inputs a @@ -1091,7 +1091,7 @@ Writing unit tests on a train. Wow. ### xor ### -Writing unit tests on a train. Wow. +Some rudimentary tests for `xor`. | routine main | inputs a @@ -1118,7 +1118,7 @@ Writing unit tests on a train. Wow. ### shl ### -Some rudimentary tests for shl. +Some rudimentary tests for `shl`. | routine main | inputs a, c @@ -1146,7 +1146,7 @@ Some rudimentary tests for shl. ### shr ### -Some rudimentary tests for shr. +Some rudimentary tests for `shr`. | routine main | inputs a, c @@ -1172,6 +1172,16 @@ Some rudimentary tests for shr. | } ? UnmeaningfulReadError: c +### nop ### + +Some rudimentary tests for `nop`. + + | routine main + | { + | nop + | } + = ok + ### call ### When calling a routine, all of the locations it lists as inputs must be diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index 0021f0c..ee26696 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -18,6 +18,15 @@ Null program. | } = $080D RTS +`nop` program. + + | routine main + | { + | nop + | } + = $080D NOP + = $080E RTS + Rudimentary program. | routine main diff --git a/tests/SixtyPical Syntax.md b/tests/SixtyPical Syntax.md index fd2b432..f4b496d 100644 --- a/tests/SixtyPical Syntax.md +++ b/tests/SixtyPical Syntax.md @@ -78,6 +78,14 @@ Trash. | } = ok +`nop`. + + | routine main + | { + | nop + | } + = ok + If with not | routine foo { From 9b912de17c9b5374ad430e153359a4a3cc420229 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 29 Mar 2018 10:31:42 +0100 Subject: [PATCH 09/42] Accessing zero-page with `ld` and `st` generates zero-page opcodes. --- HISTORY.md | 1 + eg/atari2600/atari-2600-example.oph | 1 + src/sixtypical/compiler.py | 24 ++++++++++++++---------- tests/SixtyPical Compilation.md | 16 ++++++++++++++++ 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index e2c2dbc..10a7bf1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -9,6 +9,7 @@ History of SixtyPical * Specifying multiple SixtyPical source files will produce a single compiled result from their combination. * Added `nop` opcode, which compiles to `NOP` (mainly for timing.) +* Accessing zero-page with `ld` and `st` generates zero-page opcodes. * Rudimentary support for Atari 2600 prelude in a 4K cartridge image, and start of an example program in `eg/atari2600` directory. diff --git a/eg/atari2600/atari-2600-example.oph b/eg/atari2600/atari-2600-example.oph index ae33962..8ab1c3f 100644 --- a/eg/atari2600/atari-2600-example.oph +++ b/eg/atari2600/atari-2600-example.oph @@ -116,6 +116,7 @@ main: jsr display_frame ;;; jsr read_joystick jmp main + rts ;;; ; ; Vertical blank routine. diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index b02e717..c4f6d87 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -67,6 +67,12 @@ class Compiler(object): return static_label return self.labels[name] + def absolute_or_zero_page(self, label): + if label.addr and label.addr < 256: + return ZeroPage(label) + else: + return Absolute(label) + # visitor methods def compile_program(self, program): @@ -177,7 +183,7 @@ class Compiler(object): elif isinstance(src, IndirectRef) and isinstance(src.ref.type, PointerType): self.emitter.emit(LDA(IndirectY(self.get_label(src.ref.name)))) else: - self.emitter.emit(LDA(Absolute(self.get_label(src.name)))) + self.emitter.emit(LDA(self.absolute_or_zero_page(self.get_label(src.name)))) elif dest == REG_X: if src == REG_A: self.emitter.emit(TAX()) @@ -186,7 +192,7 @@ class Compiler(object): elif isinstance(src, IndexedRef) and src.index == REG_Y: self.emitter.emit(LDX(AbsoluteY(self.get_label(src.ref.name)))) else: - self.emitter.emit(LDX(Absolute(self.get_label(src.name)))) + self.emitter.emit(LDX(self.absolute_or_zero_page(self.get_label(src.name)))) elif dest == REG_Y: if src == REG_A: self.emitter.emit(TAY()) @@ -195,7 +201,7 @@ class Compiler(object): elif isinstance(src, IndexedRef) and src.index == REG_X: self.emitter.emit(LDY(AbsoluteX(self.get_label(src.ref.name)))) else: - self.emitter.emit(LDY(Absolute(self.get_label(src.name)))) + self.emitter.emit(LDY(self.absolute_or_zero_page(self.get_label(src.name)))) else: raise UnsupportedOpcodeError(instr) elif opcode == 'st': @@ -215,17 +221,15 @@ class Compiler(object): REG_X: AbsoluteX, REG_Y: AbsoluteY, }[dest.index] - label = self.get_label(dest.ref.name) + operand = mode_cls(self.get_label(dest.ref.name)) elif isinstance(dest, IndirectRef) and isinstance(dest.ref.type, PointerType): - mode_cls = IndirectY - label = self.get_label(dest.ref.name) + operand = IndirectY(self.get_label(dest.ref.name)) else: - mode_cls = Absolute - label = self.get_label(dest.name) + operand = self.absolute_or_zero_page(self.get_label(dest.name)) - if op_cls is None or mode_cls is None: + if op_cls is None: raise UnsupportedOpcodeError(instr) - self.emitter.emit(op_cls(mode_cls(label))) + self.emitter.emit(op_cls(operand)) elif opcode == 'add': if dest == REG_A: if isinstance(src, ConstantRef): diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index ee26696..759a474 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -112,6 +112,22 @@ Memory location with explicit address. = $080F STA $0400 = $0812 RTS +Accesses to memory locations in zero-page with `ld` and `st` use zero-page addressing. + + | byte screen @ 100 + | + | routine main + | inputs screen + | outputs screen + | trashes a, z, n + | { + | ld a, screen + | st a, screen + | } + = $080D LDA $64 + = $080F STA $64 + = $0811 RTS + Memory location with initial value. | byte lives : 3 From 2f513f7291f4828bc47265ede9a59c48f495d5a4 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 29 Mar 2018 10:45:18 +0100 Subject: [PATCH 10/42] Initial support for initializing byte tables with list of bytes. --- src/sixtypical/compiler.py | 1 + src/sixtypical/parser.py | 12 +++++++++--- tests/SixtyPical Compilation.md | 24 +++++++++++++++++++++++- tests/SixtyPical Syntax.md | 10 +++++++++- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index c4f6d87..efeeb56 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -122,6 +122,7 @@ class Compiler(object): elif type_ == TYPE_WORD: initial_data = Word(defn.initial) elif TableType.is_a_table_type(type_, TYPE_BYTE): + # FIXME convert defn.initial to a serializable type ... or when parsing. initial_data = Table(defn.initial, type_.size) else: raise NotImplementedError(type_) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index d879392..851002e 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -138,9 +138,15 @@ class Parser(object): initial = None if self.scanner.consume(':'): - if isinstance(type_, TableType) and self.scanner.on_type('string literal'): - initial = self.scanner.token - self.scanner.scan() + if isinstance(type_, TableType): + if self.scanner.on_type('string literal'): + initial = self.scanner.token + self.scanner.scan() + else: + initial = [] + initial.append(self.const()) + while self.scanner.consume(','): + initial.append(self.const()) else: initial = self.const().value diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index 759a474..862c22f 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -162,7 +162,7 @@ Word memory locations with explicit address, initial value. = $081A .byte $BB = $081B .byte $0B -Initialized byte table. Bytes allocated, but beyond the string, are 0's. +Initialized byte table, initialized with ASCII string. Bytes allocated, but beyond the string, are 0's. | byte table[8] message : "WHAT?" | @@ -184,6 +184,28 @@ Initialized byte table. Bytes allocated, but beyond the string, are 0's. = $0819 BRK = $081A BRK +Initialized byte table, initialized with list of byte values. + + | byte table[8] message : 255, 0, 129, 128, 127 + | + | routine main + | inputs message + | outputs x, a, z, n + | { + | ld x, 0 + | ld a, message + x + | } + = $080D LDX #$00 + = $080F LDA $0813,X + = $0812 RTS + = $0813 .byte $57 + = $0814 PHA + = $0815 EOR ($54,X) + = $0817 .byte $3F + = $0818 BRK + = $0819 BRK + = $081A BRK + Some instructions. | byte foo diff --git a/tests/SixtyPical Syntax.md b/tests/SixtyPical Syntax.md index f4b496d..feddf0b 100644 --- a/tests/SixtyPical Syntax.md +++ b/tests/SixtyPical Syntax.md @@ -302,7 +302,7 @@ User-defined locations of other types. | } = ok -Initialized byte table. +Initialized byte table, initialized with ASCII string. | byte table[32] message : "WHAT DO YOU WANT TO DO NEXT?" | @@ -318,6 +318,14 @@ Can't initialize anything but a byte table with a string. | } ? SyntaxError +Initialized byte table, initialized with list of bytes. + + | byte table[8] charmap : 0, 255, 129, 192, 0, 1, 2, 4 + | + | routine main { + | } + = ok + Can't access an undeclared memory location. | routine main { From eadf1eb4aed602cd758d3ad5f817817c9c82def0 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 29 Mar 2018 11:09:02 +0100 Subject: [PATCH 11/42] A `byte` or `word` table can be initialized with a list of constants. --- HISTORY.md | 5 +++-- src/sixtypical/compiler.py | 5 +++-- src/sixtypical/emitter.py | 13 +++++++------ src/sixtypical/parser.py | 4 ++-- tests/SixtyPical Compilation.md | 25 +++++++++++++++++++++---- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 10a7bf1..b4d98c9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,10 +6,11 @@ History of SixtyPical * Symbolic constants can be defined with the `const` keyword, and can be used in most places where literal values can be used. -* Specifying multiple SixtyPical source files will produce a single - compiled result from their combination. * Added `nop` opcode, which compiles to `NOP` (mainly for timing.) * Accessing zero-page with `ld` and `st` generates zero-page opcodes. +* A `byte` or `word` table can be initialized with a list of constants. +* Specifying multiple SixtyPical source files will produce a single + compiled result from their combination. * Rudimentary support for Atari 2600 prelude in a 4K cartridge image, and start of an example program in `eg/atari2600` directory. diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index efeeb56..add19e9 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -122,8 +122,9 @@ class Compiler(object): elif type_ == TYPE_WORD: initial_data = Word(defn.initial) elif TableType.is_a_table_type(type_, TYPE_BYTE): - # FIXME convert defn.initial to a serializable type ... or when parsing. - initial_data = Table(defn.initial, type_.size) + initial_data = Table([Byte(i) for i in defn.initial], type_.size) + elif TableType.is_a_table_type(type_, TYPE_WORD): + initial_data = Table([Word(i) for i in defn.initial], type_.size) else: raise NotImplementedError(type_) label.set_length(initial_data.size()) diff --git a/src/sixtypical/emitter.py b/src/sixtypical/emitter.py index 78dbf95..a1b962c 100644 --- a/src/sixtypical/emitter.py +++ b/src/sixtypical/emitter.py @@ -13,6 +13,8 @@ class Emittable(object): class Byte(Emittable): def __init__(self, value): + if isinstance(value, basestring): + value = ord(value) if value < -127 or value > 255: raise IndexError(value) if value < 0: @@ -49,6 +51,7 @@ class Word(Emittable): class Table(Emittable): def __init__(self, value, size): + """`value` should be an iterable of Emittables.""" # TODO: range-checking self.value = value self._size = size @@ -57,12 +60,10 @@ class Table(Emittable): return self._size def serialize(self, addr=None): - bytes = [] - for b in self.value: - bytes.append(chr(ord(b))) - while len(bytes) < self.size(): - bytes.append(chr(0)) - return ''.join(bytes) + buf = ''.join([emittable.serialize() for emittable in self.value]) + while len(buf) < self.size(): + buf += chr(0) + return buf def __repr__(self): return "%s()" % (self.__class__.__name__) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 851002e..82ba3eb 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -144,9 +144,9 @@ class Parser(object): self.scanner.scan() else: initial = [] - initial.append(self.const()) + initial.append(self.const().value) while self.scanner.consume(','): - initial.append(self.const()) + initial.append(self.const().value) else: initial = self.const().value diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index 862c22f..511ed5c 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -198,14 +198,31 @@ Initialized byte table, initialized with list of byte values. = $080D LDX #$00 = $080F LDA $0813,X = $0812 RTS - = $0813 .byte $57 - = $0814 PHA - = $0815 EOR ($54,X) - = $0817 .byte $3F + = $0813 .byte $FF + = $0814 BRK + = $0815 STA ($80,X) + = $0817 .byte $7F = $0818 BRK = $0819 BRK = $081A BRK +Initialized word table, initialized with list of word values. + + | word table[8] message : 65535, 0, 127 + | + | routine main + | { + | } + = $080D RTS + = $080E .byte $FF + = $080F .byte $FF + = $0810 BRK + = $0811 BRK + = $0812 .byte $7F + = $0813 BRK + = $0814 BRK + = $0815 BRK + Some instructions. | byte foo From ebe53f540c8f8fe8e42a8b63480c0a5fb67ce94b Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 29 Mar 2018 11:46:56 +0100 Subject: [PATCH 12/42] Fix bug when zero page address was $00. --- eg/atari2600/atari-2600-example.60p | 2 +- eg/atari2600/build.sh | 1 + src/sixtypical/compiler.py | 2 +- tests/SixtyPical Compilation.md | 11 ++++++++--- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/eg/atari2600/atari-2600-example.60p b/eg/atari2600/atari-2600-example.60p index 59d8179..fad76c3 100644 --- a/eg/atari2600/atari-2600-example.60p +++ b/eg/atari2600/atari-2600-example.60p @@ -26,7 +26,7 @@ byte colour @ $80 byte luminosity @ $81 byte joystick_delay @ $82 -byte table[8] image_data : "ZZZZUUUU" // [126, 129, 153, 165, 129, 165, 129, 126] +byte table[8] image_data : 126, 129, 153, 165, 129, 165, 129, 126 // %01111110 // %10000001 // %10011001 diff --git a/eg/atari2600/build.sh b/eg/atari2600/build.sh index 1f141c1..1cb7509 100755 --- a/eg/atari2600/build.sh +++ b/eg/atari2600/build.sh @@ -6,4 +6,5 @@ if [ "x$COMPARE" != "x" ]; then dcc6502 -o 0xf000 -m 200 atari-2600-example.bin > atari-2600-example.bin.disasm.txt dcc6502 -o 0xf000 -m 200 atari-2600-example-60p.bin > atari-2600-example-60p.bin.disasm.txt paste atari-2600-example.bin.disasm.txt atari-2600-example-60p.bin.disasm.txt | pr -t -e24 + #diff -ru atari-2600-example.bin.disasm.txt atari-2600-example-60p.bin.disasm.txt fi diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index add19e9..7dedbd4 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -68,7 +68,7 @@ class Compiler(object): return self.labels[name] def absolute_or_zero_page(self, label): - if label.addr and label.addr < 256: + if label.addr is not None and label.addr < 256: return ZeroPage(label) else: return Absolute(label) diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index 511ed5c..629237d 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -114,19 +114,24 @@ Memory location with explicit address. Accesses to memory locations in zero-page with `ld` and `st` use zero-page addressing. + | byte zp @ $00 | byte screen @ 100 | | routine main - | inputs screen - | outputs screen + | inputs screen, zp + | outputs screen, zp | trashes a, z, n | { | ld a, screen | st a, screen + | ld a, zp + | st a, zp | } = $080D LDA $64 = $080F STA $64 - = $0811 RTS + = $0811 LDA $00 + = $0813 STA $00 + = $0815 RTS Memory location with initial value. From 6e8dc9782620aa6f7fcb9f9b63113a2995693569 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 29 Mar 2018 12:11:38 +0100 Subject: [PATCH 13/42] Fork the Atari 2600 Ophis example for easier comparing to 60p. --- eg/atari2600/atari-2600-example.oph | 117 ++++--- eg/atari2600/build.sh | 11 +- .../{atari-2600-example.60p => smiley.60p} | 24 +- eg/atari2600/smiley.oph | 285 ++++++++++++++++++ 4 files changed, 368 insertions(+), 69 deletions(-) rename eg/atari2600/{atari-2600-example.60p => smiley.60p} (86%) create mode 100644 eg/atari2600/smiley.oph diff --git a/eg/atari2600/atari-2600-example.oph b/eg/atari2600/atari-2600-example.oph index 8ab1c3f..836a64a 100644 --- a/eg/atari2600/atari-2600-example.oph +++ b/eg/atari2600/atari-2600-example.oph @@ -114,9 +114,8 @@ zero_loop: main: jsr vertical_blank jsr display_frame - ;;; jsr read_joystick + jsr read_joystick jmp main - rts ;;; ; ; Vertical blank routine. @@ -256,63 +255,63 @@ _image_loop: ; of the player. ; -;;; .scope -;;; read_joystick: -;;; lda joystick_delay -;;; beq _continue -;;; -;;; dec joystick_delay -;;; rts -;;; -;;; _continue: -;;; lda SWCHA -;;; and #$f0 -;;; cmp #$e0 -;;; beq _up -;;; cmp #$d0 -;;; beq _down -;;; cmp #$b0 -;;; beq _left -;;; cmp #$70 -;;; beq _right -;;; jmp _tail -;;; -;;; _up: -;;; inc luminosity -;;; jmp _tail -;;; _down: -;;; dec luminosity -;;; jmp _tail -;;; _left: -;;; dec colour -;;; jmp _tail -;;; _right: -;;; inc colour -;;; ;jmp _tail -;;; -;;; _tail: -;;; lda colour -;;; and #$0f -;;; sta colour -;;; -;;; lda luminosity -;;; and #$0f -;;; sta luminosity -;;; -;;; lda colour -;;; clc -;;; rol -;;; rol -;;; rol -;;; rol -;;; ora luminosity -;;; sta COLUP0 -;;; -;;; lda #$06 -;;; sta joystick_delay -;;; -;;; rts -;;; .scend +.scope +read_joystick: + lda joystick_delay + beq _continue + + dec joystick_delay + rts + +_continue: + lda SWCHA + and #$f0 + cmp #$e0 + beq _up + cmp #$d0 + beq _down + cmp #$b0 + beq _left + cmp #$70 + beq _right + jmp _tail + +_up: + inc luminosity + jmp _tail +_down: + dec luminosity + jmp _tail +_left: + dec colour + jmp _tail +_right: + inc colour + ;jmp _tail + +_tail: + lda colour + and #$0f + sta colour + + lda luminosity + and #$0f + sta luminosity + + lda colour + clc + rol + rol + rol + rol + ora luminosity + sta COLUP0 + + lda #$06 + sta joystick_delay + + rts +.scend ; ; Player (sprite) data. diff --git a/eg/atari2600/build.sh b/eg/atari2600/build.sh index 1cb7509..1430812 100755 --- a/eg/atari2600/build.sh +++ b/eg/atari2600/build.sh @@ -1,10 +1,9 @@ #!/bin/sh -sixtypical --prelude=atari2600 atari-2600-example.60p > atari-2600-example-60p.bin +sixtypical --prelude=atari2600 smiley.60p > smiley-60p.bin if [ "x$COMPARE" != "x" ]; then - ophis atari-2600-example.oph -o atari-2600-example.bin - dcc6502 -o 0xf000 -m 200 atari-2600-example.bin > atari-2600-example.bin.disasm.txt - dcc6502 -o 0xf000 -m 200 atari-2600-example-60p.bin > atari-2600-example-60p.bin.disasm.txt - paste atari-2600-example.bin.disasm.txt atari-2600-example-60p.bin.disasm.txt | pr -t -e24 - #diff -ru atari-2600-example.bin.disasm.txt atari-2600-example-60p.bin.disasm.txt + ophis smiley.oph -o smiley.bin + dcc6502 -o 0xf000 -m 200 smiley.bin > smiley.bin.disasm.txt + dcc6502 -o 0xf000 -m 200 smiley-60p.bin > smiley-60p.bin.disasm.txt + paste smiley.bin.disasm.txt smiley-60p.bin.disasm.txt | pr -t -e24 fi diff --git a/eg/atari2600/atari-2600-example.60p b/eg/atari2600/smiley.60p similarity index 86% rename from eg/atari2600/atari-2600-example.60p rename to eg/atari2600/smiley.60p index fad76c3..c66d966 100644 --- a/eg/atari2600/atari-2600-example.60p +++ b/eg/atari2600/smiley.60p @@ -1,4 +1,5 @@ -// atari-2600-example.60p - SixtyPical translation of atari-2600-example.oph +// smiley.60p - SixtyPical translation of smiley.oph (2018), +// which is itself a stripped-down version of atari-2600-example.oph byte VSYNC @ $00 byte VBLANK @ $01 @@ -147,10 +148,25 @@ define display_frame routine st a, COLUBK } +define colourize_player routine + inputs colour, luminosity + outputs COLUP0 + trashes a, z, c, n +{ + ld a, colour + st off, c + shl a + shl a + shl a + shl a + or a, luminosity + st a, COLUP0 +} + define main routine inputs image_data, INTIM - outputs CTRLPF, colour, luminosity, NUSIZ0, VSYNC, WSYNC, TIM64T, HMOVE, VBLANK, RESP0, GP0, PF0, PF1, PF2, COLUPF, COLUBK - trashes a, x, y, z, n + outputs CTRLPF, colour, luminosity, NUSIZ0, VSYNC, WSYNC, TIM64T, HMOVE, VBLANK, RESP0, GP0, PF0, PF1, PF2, COLUPF, COLUBK, COLUP0 + trashes a, x, y, z, c, n { ld a, $00 st a, CTRLPF @@ -163,6 +179,6 @@ define main routine repeat { call vertical_blank call display_frame - // call read_joystick + call colourize_player } forever } diff --git a/eg/atari2600/smiley.oph b/eg/atari2600/smiley.oph new file mode 100644 index 0000000..19d4335 --- /dev/null +++ b/eg/atari2600/smiley.oph @@ -0,0 +1,285 @@ +; +; smiley.oph (2018) +; stripped-down version of atari-2600-example.oph (2012) +; +; This work is in the public domain. See the file UNLICENSE for more info. +; +; to build and run in Stella: +; ophis smiley.oph -o smiley.bin +; stella smiley.bin + +; +; Useful system addresses (TODO: briefly describe each of these.) +; + +.alias VSYNC $00 +.alias VBLANK $01 +.alias WSYNC $02 +.alias NUSIZ0 $04 +.alias NUSIZ1 $05 +.alias COLUPF $08 +.alias COLUBK $09 +.alias PF0 $0D +.alias PF1 $0E +.alias PF2 $0F +.alias SWCHA $280 +.alias INTIM $284 +.alias TIM64T $296 +.alias CTRLPF $0A +.alias COLUP0 $06 +.alias COLUP1 $07 +.alias GP0 $1B +.alias GP1 $1C +.alias HMOVE $2a +.alias RESP0 $10 +.alias RESP1 $11 + +; +; Cartridge ROM occupies the top 4K of memory ($F000-$FFFF). +; Thus, typically, the program will occupy all that space too. +; +; Zero-page RAM we can use with impunity starts at $80 and goes +; upward (at least until $99, but probably further.) +; + +.alias colour $80 +.alias luminosity $81 +.alias joystick_delay $82 + +.org $F000 + +; +; Standard prelude for Atari 2600 cartridge code. +; +; Get various parts of the machine into a known state: +; +; - Disable interrupts +; - Clear the Decimal flag +; - Initialize the Stack Pointer +; - Zero all bytes in Zero Page memory +; + +start: + sei + cld + ldx #$FF + txs + lda #$00 + +zero_loop: + sta $00, x + dex + bne zero_loop + + ; and fall through to... + +; +; Initialization. +; +; - Clear the Playfield Control register. +; - Set the player (sprite) colour to light green (write to COLUP0.) +; - Set the player (sprite) size/repetion to normal (write to NUSIZ0.) +; + + lda #$00 + sta CTRLPF + lda #$0c + sta colour + lda #$0a + sta luminosity + lda #$00 + sta NUSIZ0 + + ; and fall through to... + +; +; Main loop. +; +; A typical main loop consists of: +; - Waiting for the frame to start (vertical blank period) +; - Displaying stuff on the screen (the _display kernel_) +; - Doing any processing you like (reading joysticks, updating program state, +; etc.), as long as you get it all done before the next frame starts! +; + +main: + jsr vertical_blank + jsr display_frame + jsr colourize_player + jmp main + rts ; NOTE just to pad out to match the SixtyPical version + +; +; Vertical blank routine. +; +; In brief: wait until it is time for the next frame of video. +; TODO: describe this in more detail. +; + +vertical_blank: + ldx #$00 + lda #$02 + sta WSYNC + sta WSYNC + sta WSYNC + sta VSYNC + sta WSYNC + sta WSYNC + lda #$2C + sta TIM64T + lda #$00 + sta WSYNC + sta VSYNC + rts + +; +; Display kernal. +; +; First, wait until it's time to display the frame. +; + +.scope +display_frame: + lda INTIM + bne display_frame + +; +; (After that loop finishes, we know the accumulator must contain 0.) +; Wait for the next scanline, zero HMOVE (for some reason; TODO discover +; this), then turn on the screen. +; + + sta WSYNC + sta HMOVE + sta VBLANK + +; +; Actual work in the display kernal is done here. +; +; This is a pathological approach to writing a display kernal. +; This wouldn't be how you'd do things in a game. So be it. +; One day I may improve it. For now, be happy that it displays +; anything at all! +; + +; +; Wait for $3f (plus one?) scan lines to pass, by waiting for +; WSYNC that many times. +; + + ldx #$3F +_wsync_loop: + sta WSYNC + dex + bpl _wsync_loop + sta WSYNC + +; +; Delay while the raster scans across the screen. The more +; we delay here, the more to the right the player will be when +; we draw it. +; + + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + +; +; OK, *now* display the player. +; + + sta RESP0 + +; +; Loop over the rows of the sprite data, drawing each to the screen +; over four scan lines. +; +; TODO understand this better and describe it! +; + + ldy #$07 +_image_loop: + lda image_data, y + sta GP0 + + sta WSYNC + sta WSYNC + sta WSYNC + sta WSYNC + dey + bpl _image_loop + + lda #$00 + sta GP0 + +; +; Turn off screen display and clear display registers. +; + + lda #$02 + sta WSYNC + sta VBLANK + lda #$00 + sta PF0 + sta PF1 + sta PF2 + sta COLUPF + sta COLUBK + + rts +.scend + +; +; Modify the colour and luminosity of the player. +; + +.scope +colourize_player: + lda colour + clc + rol + rol + rol + rol + ora luminosity + sta COLUP0 + rts +.scend + +; +; Player (sprite) data. +; +; Because we loop over these bytes with the Y register counting *down*, +; this image is stored "upside-down". +; + +image_data: + .byte %01111110 + .byte %10000001 + .byte %10011001 + .byte %10100101 + .byte %10000001 + .byte %10100101 + .byte %10000001 + .byte %01111110 + +; +; Standard postlude for Atari 2600 cartridge code. +; Give BRK and boot vectors that point to the start of the code. +; + +.advance $FFFC + .word start + .word start From b9fb26320c0b0818ce0d4866f4e6150e9ad6d5b9 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 29 Mar 2018 13:33:06 +0100 Subject: [PATCH 14/42] Add some failing tests for looping on the `n` flag. --- eg/atari2600/build.sh | 1 + tests/SixtyPical Analysis.md | 12 +++++++++++ tests/SixtyPical Compilation.md | 38 +++++++++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/eg/atari2600/build.sh b/eg/atari2600/build.sh index 1430812..be05acf 100755 --- a/eg/atari2600/build.sh +++ b/eg/atari2600/build.sh @@ -6,4 +6,5 @@ if [ "x$COMPARE" != "x" ]; then dcc6502 -o 0xf000 -m 200 smiley.bin > smiley.bin.disasm.txt dcc6502 -o 0xf000 -m 200 smiley-60p.bin > smiley-60p.bin.disasm.txt paste smiley.bin.disasm.txt smiley-60p.bin.disasm.txt | pr -t -e24 + #diff -ru smiley.bin.disasm.txt smiley-60p.bin.disasm.txt fi diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index 82ad7ec..2b279a9 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -1669,6 +1669,18 @@ The body of `repeat forever` can be empty. | } = ok +While `repeat` is most often used with `z`, it can also be used with `n`. + + | routine main + | outputs y, n, z + | { + | ld y, 15 + | repeat { + | dec y + | } until n + | } + = ok + ### for ### Basic "open-faced for" loop. We'll start with the "upto" variant. diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index 629237d..489bebe 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -357,7 +357,7 @@ Compiling `if` without `else`. = $0813 LDY #$01 = $0815 RTS -Compiling `repeat`. +Compiling `repeat ... until z`. | routine main | trashes a, y, z, n, c @@ -376,7 +376,7 @@ Compiling `repeat`. = $0813 BNE $080F = $0815 RTS -Compiling `repeat until not`. +Compiling `repeat ... until not z`. | routine main | trashes a, y, z, n, c @@ -395,6 +395,40 @@ Compiling `repeat until not`. = $0813 BEQ $080F = $0815 RTS +Compiling `repeat ... until n`. + + | routine main + | trashes a, y, z, n, c + | { + | ld y, 65 + | repeat { + | ld a, y + | dec y + | } until n + | } + = $080D LDY #$41 + = $080F TYA + = $0810 DEY + = $0811 BPL $080F + = $0813 RTS + +Compiling `repeat ... until not n`. + + | routine main + | trashes a, y, z, n, c + | { + | ld y, 65 + | repeat { + | ld a, y + | inc y + | } until not n + | } + = $080D LDY #$41 + = $080F TYA + = $0810 INY + = $0811 BMI $080F + = $0813 RTS + Compiling `repeat forever`. | routine main From fa1b0cfae13ca0a2362a15198612e88758f67520 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 29 Mar 2018 14:45:28 +0100 Subject: [PATCH 15/42] Support branching and looping on the `n` flag. --- HISTORY.md | 3 ++- eg/atari2600/smiley.60p | 2 +- src/sixtypical/compiler.py | 6 +++++- src/sixtypical/gen6502.py | 12 ++++++++++++ tests/SixtyPical Compilation.md | 4 ++-- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index b4d98c9..b083f05 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -9,10 +9,11 @@ History of SixtyPical * Added `nop` opcode, which compiles to `NOP` (mainly for timing.) * Accessing zero-page with `ld` and `st` generates zero-page opcodes. * A `byte` or `word` table can be initialized with a list of constants. +* Branching and repeating on the `n` flag is now supported. * Specifying multiple SixtyPical source files will produce a single compiled result from their combination. * Rudimentary support for Atari 2600 prelude in a 4K cartridge image, - and start of an example program in `eg/atari2600` directory. + and an example program in `eg/atari2600` directory. 0.14 ---- diff --git a/eg/atari2600/smiley.60p b/eg/atari2600/smiley.60p index c66d966..eebf314 100644 --- a/eg/atari2600/smiley.60p +++ b/eg/atari2600/smiley.60p @@ -81,7 +81,7 @@ define display_frame routine repeat { st a, WSYNC dec x - } until z // FIXME orig loop used "bpl _wsync_loop" + } until n st a, WSYNC //; diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index 7dedbd4..113d8a2 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -15,7 +15,7 @@ from sixtypical.gen6502 import ( CLC, SEC, ADC, SBC, ROL, ROR, INC, INX, INY, DEC, DEX, DEY, CMP, CPX, CPY, AND, ORA, EOR, - BCC, BCS, BNE, BEQ, + BCC, BCS, BNE, BEQ, BPL, BMI, JMP, JSR, RTS, SEI, CLI, NOP, @@ -518,10 +518,12 @@ class Compiler(object): False: { 'c': BCC, 'z': BNE, + 'n': BPL, }, True: { 'c': BCS, 'z': BEQ, + 'n': BMI, }, }[instr.inverted].get(instr.src.name) if cls is None: @@ -548,10 +550,12 @@ class Compiler(object): False: { 'c': BCC, 'z': BNE, + 'n': BPL, }, True: { 'c': BCS, 'z': BEQ, + 'n': BMI, }, }[instr.inverted].get(instr.src.name) if cls is None: diff --git a/src/sixtypical/gen6502.py b/src/sixtypical/gen6502.py index c880e18..f166391 100644 --- a/src/sixtypical/gen6502.py +++ b/src/sixtypical/gen6502.py @@ -160,6 +160,18 @@ class BNE(Instruction): } +class BPL(Instruction): + opcodes = { + Relative: 0x10, + } + + +class BMI(Instruction): + opcodes = { + Relative: 0x30, + } + + class CLC(Instruction): opcodes = { Implied: 0x18 diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index 489bebe..19c7250 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -417,13 +417,13 @@ Compiling `repeat ... until not n`. | routine main | trashes a, y, z, n, c | { - | ld y, 65 + | ld y, 199 | repeat { | ld a, y | inc y | } until not n | } - = $080D LDY #$41 + = $080D LDY #$C7 = $080F TYA = $0810 INY = $0811 BMI $080F From b33998cddce4a2f144590161d6267b14b5002b65 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 29 Mar 2018 15:07:44 +0100 Subject: [PATCH 16/42] First cut at building fallthru map. Needs tests. --- bin/sixtypical | 26 +++++++++++++++++++------- src/sixtypical/analyzer.py | 8 +++++++- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index b296f94..80fc8e6 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -58,6 +58,11 @@ def process_input_files(filenames, options): analyzer = Analyzer(debug=options.debug) analyzer.analyze_program(program) + if options.dump_fallthru_map: + import json + sys.stdout.write(json.dumps(program.fallthru_map, indent=4, sort_keys=True)) + sys.stdout.write("\n") + if options.analyze_only: return @@ -121,11 +126,7 @@ if __name__ == '__main__': '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( "--origin", type=str, default='0xc000', help="Location in memory where the `main` routine will be " @@ -143,16 +144,27 @@ if __name__ == '__main__': "Also sets the origin and format. " "Options are: c64, vic20, atari2600." ) + argparser.add_argument( - "--debug", + "--analyze-only", action="store_true", - help="Display debugging information when analyzing and compiling." + help="Only parse and analyze the program; do not compile it." + ) + argparser.add_argument( + "--dump-fallthru-map", + action="store_true", + help="Dump the fallthru map to stdout after analyzing the program." ) argparser.add_argument( "--parse-only", action="store_true", help="Only parse the program; do not analyze or compile it." ) + argparser.add_argument( + "--debug", + action="store_true", + help="Display debugging information when analyzing and compiling." + ) argparser.add_argument( "--traceback", action="store_true", diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index b78cf95..5418238 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -310,8 +310,13 @@ class Analyzer(object): def analyze_program(self, program): assert isinstance(program, Program) self.routines = {r.location: r for r in program.routines} + fallthru_map = {} for routine in program.routines: - self.analyze_routine(routine) + context = self.analyze_routine(routine) + if context: + for encountered_goto in context.encountered_gotos(): + fallthru_map.setdefault(encountered_goto.name, set()).add(routine.name) + program.fallthru_map = dict([(k, list(v)) for k, v in fallthru_map.iteritems()]) def analyze_routine(self, routine): assert isinstance(routine, Routine) @@ -353,6 +358,7 @@ class Analyzer(object): if ref not in type_.outputs and ref not in type_.trashes and not routine_has_static(routine, ref): raise ForbiddenWriteError(routine, ref.name) self.current_routine = None + return context def analyze_block(self, block, context): assert isinstance(block, Block) From 8f1e35fb394d38a570c0859da083eda9d9546bf3 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 29 Mar 2018 16:44:06 +0100 Subject: [PATCH 17/42] Add some tests, some failing, for expected fallthru maps. --- src/sixtypical/analyzer.py | 2 +- test.sh | 1 + tests/SixtyPical Fallthru.md | 139 +++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 tests/SixtyPical Fallthru.md diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 5418238..277354b 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -316,7 +316,7 @@ class Analyzer(object): if context: for encountered_goto in context.encountered_gotos(): fallthru_map.setdefault(encountered_goto.name, set()).add(routine.name) - program.fallthru_map = dict([(k, list(v)) for k, v in fallthru_map.iteritems()]) + program.fallthru_map = dict([(k, sorted(v)) for k, v in fallthru_map.iteritems()]) def analyze_routine(self, routine): assert isinstance(routine, Routine) diff --git a/test.sh b/test.sh index 931611f..bed307e 100755 --- a/test.sh +++ b/test.sh @@ -3,4 +3,5 @@ falderal --substring-error \ tests/SixtyPical\ Syntax.md \ tests/SixtyPical\ Analysis.md \ + tests/SixtyPical\ Fallthru.md \ tests/SixtyPical\ Compilation.md diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md new file mode 100644 index 0000000..11d701e --- /dev/null +++ b/tests/SixtyPical Fallthru.md @@ -0,0 +1,139 @@ +SixtyPical Fallthru +=================== + +This is a test suite, written in [Falderal][] format, for SixtyPical's +ability to detect which routines make tail calls to other routines, +and thus can be re-arranged to simply "fall through" to them. + +[Falderal]: http://catseye.tc/node/Falderal + + -> Functionality "Dump fallthru map of SixtyPical program" is implemented by + -> shell command "bin/sixtypical --analyze-only --dump-fallthru-map --traceback %(test-body-file)" + + -> Tests for functionality "Dump fallthru map of SixtyPical program" + +A single routine, obviously, falls through to nothing and has nothing fall +through to it. + + | define main routine + | { + | } + = {} + +If main does a `goto foo`, then it can fall through to `foo`. + + | define foo routine trashes a, z, n + | { + | ld a, 0 + | } + | + | define main routine trashes a, z, n + | { + | goto foo + | } + = { + = "foo": [ + = "main" + = ] + = } + +More than one routine can fall through to a routine. + +If main does a `goto foo`, then it can fall through to `foo`. + + | define foo routine trashes a, z, n + | { + | ld a, 0 + | } + | + | define bar routine trashes a, z, n + | { + | ld a, 0 + | goto foo + | } + | + | define main routine trashes a, z, n + | { + | goto foo + | } + = { + = "foo": [ + = "bar", + = "main" + = ] + = } + +There is nothing stopping two routines from tail-calling each +other, but we will only be able to make one of them, at most, +fall through to the other. + + | define foo routine trashes a, z, n + | { + | ld a, 0 + | goto bar + | } + | + | define bar routine trashes a, z, n + | { + | ld a, 0 + | goto foo + | } + | + | define main routine trashes a, z, n + | { + | } + = { + = "bar": [ + = "foo" + = ], + = "foo": [ + = "bar" + = ] + = } + +If a routine does two tail calls (which is possible because they +can be in different branches of an `if`) it cannot fall through to another +routine. + + | define foo routine trashes a, z, n + | { + | ld a, 0 + | } + | + | define bar routine trashes a, z, n + | { + | ld a, 0 + | } + | + | define main routine inputs z trashes a, z, n + | { + | if z { + | goto foo + | } else { + | goto bar + | } + | } + = {} + +Similarly, a tail call to a vector can't be turned into a fallthru, +because we don't necessarily know what actual routine the vector contains. + + | vector routine trashes a, z, n + | vec + | + | define foo routine trashes a, z, n + | { + | ld a, 0 + | } + | + | define bar routine trashes a, z, n + | { + | ld a, 0 + | } + | + | define main routine outputs vec trashes a, z, n + | { + | copy bar, vec + | goto vec + | } + = {} From a759f4414bab8762771f27bf135053a2ac663283 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 29 Mar 2018 16:50:07 +0100 Subject: [PATCH 18/42] Make tests pass again. --- src/sixtypical/analyzer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 277354b..b107ed7 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -314,8 +314,9 @@ class Analyzer(object): for routine in program.routines: context = self.analyze_routine(routine) if context: - for encountered_goto in context.encountered_gotos(): - fallthru_map.setdefault(encountered_goto.name, set()).add(routine.name) + encountered_gotos = list(context.encountered_gotos()) + if len(encountered_gotos) == 1 and isinstance(encountered_gotos[0].type, RoutineType): + fallthru_map.setdefault(encountered_gotos[0].name, set()).add(routine.name) program.fallthru_map = dict([(k, sorted(v)) for k, v in fallthru_map.iteritems()]) def analyze_routine(self, routine): From b63b880b8c6bd345d2b3dcce74cd67da36671a75 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 29 Mar 2018 16:58:50 +0100 Subject: [PATCH 19/42] Update documentation. --- HISTORY.md | 2 ++ README.md | 2 +- eg/README.md | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index b083f05..993e466 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -10,6 +10,8 @@ History of SixtyPical * Accessing zero-page with `ld` and `st` generates zero-page opcodes. * A `byte` or `word` table can be initialized with a list of constants. * Branching and repeating on the `n` flag is now supported. +* The `--dump-fallthru-map` option outputs a map, in JSON format, of + which routines can be "fallen through" to by other routines. * Specifying multiple SixtyPical source files will produce a single compiled result from their combination. * Rudimentary support for Atari 2600 prelude in a 4K cartridge image, diff --git a/README.md b/README.md index 8b898fb..65b2036 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,6 @@ 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! -* Optimize `ld a, z` and `st a, z` to zero-page operations if address of z < 256. +* Optimize `or|and|eor a, z` to zero-page operations if address of z < 256. [VICE]: http://vice-emu.sourceforge.net/ diff --git a/eg/README.md b/eg/README.md index a805a60..58947cc 100644 --- a/eg/README.md +++ b/eg/README.md @@ -40,9 +40,9 @@ for example [hearts.60p](vic20/hearts.60p). ### atari2600 -In the [vic20](vic20/) directory are programs that run on the +In the [atari2600](atari2600/) directory are programs that run on the Atari 2600 (4K cartridge). The directory itself contains a simple -demo, [atari-2600-example.60p](atari2600/atari-2600-example.60p). -(Doesn't work yet.) +demo, [smiley.60p](atari2600/smiley.60p) which was converted from an +older Atari 2600 skeleton program written in [Ophis][]. [Ophis]: http://michaelcmartin.github.io/Ophis/ From 237a8b5b396f031b5f5e9a15c8f6371147bd8f4a Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 29 Mar 2018 17:31:14 +0100 Subject: [PATCH 20/42] Lessen dependence on the internals of ParsingContext. --- src/sixtypical/parser.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 82ba3eb..86c9948 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -33,6 +33,13 @@ class ParsingContext(object): def __str__(self): return "Symbols: {}\nStatics: {}\nTypedefs: {}\nConsts: {}".format(self.symbols, self.statics, self.typedefs, self.consts) + def lookup(self, name): + if name in self.statics: + return self.statics[name].model + if name in self.symbols: + return self.symbols[name].model + return None + class Parser(object): def __init__(self, context, text, filename): @@ -43,15 +50,8 @@ class Parser(object): def syntax_error(self, msg): self.scanner.syntax_error(msg) - def soft_lookup(self, name): - if name in self.context.statics: - return self.context.statics[name].model - if name in self.context.symbols: - return self.context.symbols[name].model - return None - def lookup(self, name): - model = self.soft_lookup(name) + model = self.context.lookup(name) if model is None: self.syntax_error('Undefined symbol "{}"'.format(name)) return model @@ -71,7 +71,7 @@ class Parser(object): while self.scanner.on(*typenames): defn = self.defn() name = defn.name - if name in self.context.symbols: + if self.context.lookup(name): self.syntax_error('Symbol "%s" already declared' % name) self.context.symbols[name] = SymEntry(defn, defn.location) defns.append(defn) @@ -83,7 +83,7 @@ class Parser(object): else: routine = self.legacy_routine() name = routine.name - if name in self.context.symbols: + if self.context.lookup(name): self.syntax_error('Symbol "%s" already declared' % name) self.context.symbols[name] = SymEntry(routine, routine.location) routines.append(routine) @@ -99,18 +99,16 @@ class Parser(object): for instr in self.backpatch_instrs: if instr.opcode in ('call', 'goto'): name = instr.location - if name not in self.context.symbols: - self.syntax_error('Undefined routine "%s"' % name) - if not isinstance(self.context.symbols[name].model.type, (RoutineType, VectorType)): + model = self.lookup(name) + if not isinstance(model.type, (RoutineType, VectorType)): self.syntax_error('Illegal call of non-executable "%s"' % name) - instr.location = self.context.symbols[name].model + instr.location = model if instr.opcode in ('copy',) and isinstance(instr.src, basestring): name = instr.src - if name not in self.context.symbols: - self.syntax_error('Undefined routine "%s"' % name) - if not isinstance(self.context.symbols[name].model.type, (RoutineType, VectorType)): + model = self.lookup(name) + if not isinstance(model.type, (RoutineType, VectorType)): self.syntax_error('Illegal copy of non-executable "%s"' % name) - instr.src = self.context.symbols[name].model + instr.src = model return Program(self.scanner.line_number, defns=defns, routines=routines) @@ -304,7 +302,7 @@ class Parser(object): c = {} for defn in statics: name = defn.name - if name in self.context.symbols or name in self.context.statics: + if self.context.lookup(name): self.syntax_error('Symbol "%s" already declared' % name) c[name] = SymEntry(defn, defn.location) return c @@ -336,7 +334,7 @@ class Parser(object): elif forward: name = self.scanner.token self.scanner.scan() - loc = self.soft_lookup(name) + loc = self.context.lookup(name) if loc is not None: return loc else: From 2be44069641afd19fb8a94dccf4d42a11d58fbe3 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 10:15:06 +0100 Subject: [PATCH 21/42] Flesh out and describe the fallthru optimization algorithm. --- README.md | 10 -------- tests/SixtyPical Fallthru.md | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 65b2036..1ab78a9 100644 --- a/README.md +++ b/README.md @@ -78,16 +78,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. -> Every routine is falled through to by zero or more routines. -> Don't consider the main routine. -> For each routine α that is finally-falled through to by a set of routines R(α), -> pick a movable routine β from R, move β in front of α, remove the `jmp` at the end of β and -> mark β as unmovable. -> Note this only works if β finally-falls through. If there are multiple tail -> positions, we can't eliminate all the `jmp`s. -> Note that if β finally-falls through to α it can't finally-fall through to anything -> else, so the sets R(α) should be disjoint for every α. (Right?) - ### And at some point... * `low` and `high` address operators - to turn `word` type into `byte`. diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index 11d701e..a27e998 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -5,6 +5,50 @@ This is a test suite, written in [Falderal][] format, for SixtyPical's ability to detect which routines make tail calls to other routines, and thus can be re-arranged to simply "fall through" to them. +The theory is as follows. + +SixtyPical supports a `goto`, but it can only appear in tail position. +If a routine r1 ends with a unique `goto` to a fixed routine r2 it is said +to *potentially fall through* to r2. + +A *unique* `goto` means that there are not multiple different `goto`s in +tail position (which can happen if, for example, an `if` is the last thing +in a routine, and each branch of that `if` ends with a different `goto`.) + +A *fixed* routine means, a routine which is known at compile time, not a +`goto` through a vector. + +Consider the set R of all routines in the program. + +Every routine r1 ∈ R either potentially falls through to a single routine +r2 ∈ R (r2 ≠ r1) or it does not potentially fall through to any routine. +We can say out(r1) = {r2} or out(r1) = ∅. + +Every routine r ∈ R in this set also has a set of zero or more +routines from which it is potentially falled through to by. Call this +in(r). It is the case that out(r1) = {r2} → r1 ∈ in(r2). + +We can trace out the connections by following the in- or our- sets of +a given routine. Because each routine potentially falls through to only +a single routine, the structures we find will be tree-like, not DAG-like. + +But they do permit cycles. + +So, we first break those cycles. We will be left with out() sets which +are disjoint trees, i.e. if r1 ∈ in(r2), then r1 ∉ in(r3) for all r3 ≠ r2. + +We then follow an algorithm something like this. Treat R as a mutable +set and start with an empty list L. Then, + +- Pick a routine r from R where out(r) = ∅. +- Find the longest chain of routines r1,r2,...rn in R where out(r1) = {r2}, + out(r2} = {r3}, ... out(rn-1) = {rn}, and rn = r. +- Remove (r1,r2,...,rn) from R and append them to L in that order. + Mark (r1,r2,...rn-1) as "will have their final `goto` removed." +- Repeat until R is empty. + +When times comes to generate code, generate it in the order given by L. + [Falderal]: http://catseye.tc/node/Falderal -> Functionality "Dump fallthru map of SixtyPical program" is implemented by From a0d3ea8167a04c8ce265d2dc76b6724a8922535b Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 11:09:39 +0100 Subject: [PATCH 22/42] Add a dedicated module for a dedicated FallthruAnalyzer. --- bin/sixtypical | 12 +++++++++++- src/sixtypical/analyzer.py | 7 +------ src/sixtypical/fallthru.py | 23 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 src/sixtypical/fallthru.py diff --git a/bin/sixtypical b/bin/sixtypical index 80fc8e6..ab714f7 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -60,7 +60,12 @@ def process_input_files(filenames, options): if options.dump_fallthru_map: import json - sys.stdout.write(json.dumps(program.fallthru_map, indent=4, sort_keys=True)) + from sixtypical.fallthru import FallthruAnalyzer + + fa = FallthruAnalyzer(debug=options.debug) + fa.analyze_program(program) + + sys.stdout.write(json.dumps(fa.fallthru_map, indent=4, sort_keys=True)) sys.stdout.write("\n") if options.analyze_only: @@ -155,6 +160,11 @@ if __name__ == '__main__': action="store_true", help="Dump the fallthru map to stdout after analyzing the program." ) + argparser.add_argument( + "--dump-fallthru-ordering", + action="store_true", + help="Dump the fallthru ordering to stdout after analyzing the program." + ) argparser.add_argument( "--parse-only", action="store_true", diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index b107ed7..df76933 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -310,14 +310,9 @@ class Analyzer(object): def analyze_program(self, program): assert isinstance(program, Program) self.routines = {r.location: r for r in program.routines} - fallthru_map = {} for routine in program.routines: context = self.analyze_routine(routine) - if context: - encountered_gotos = list(context.encountered_gotos()) - if len(encountered_gotos) == 1 and isinstance(encountered_gotos[0].type, RoutineType): - fallthru_map.setdefault(encountered_gotos[0].name, set()).add(routine.name) - program.fallthru_map = dict([(k, sorted(v)) for k, v in fallthru_map.iteritems()]) + routine.encountered_gotos = list(context.encountered_gotos()) if context else [] def analyze_routine(self, routine): assert isinstance(routine, Routine) diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py new file mode 100644 index 0000000..d7face9 --- /dev/null +++ b/src/sixtypical/fallthru.py @@ -0,0 +1,23 @@ +# encoding: UTF-8 + +from sixtypical.model import RoutineType + + +class FallthruAnalyzer(object): + + def __init__(self, debug=False): + self.debug = debug + + def analyze_program(self, program): + fallthru_map = {} + for routine in program.routines: + encountered_gotos = list(routine.encountered_gotos) + if len(encountered_gotos) == 1 and isinstance(encountered_gotos[0].type, RoutineType): + fallthru_map.setdefault(encountered_gotos[0].name, set()).add(routine.name) + self.fallthru_map = dict([(k, sorted(v)) for k, v in fallthru_map.iteritems()]) + + def break_cycles(self): + raise NotImplementedError + + def serialize(self): + raise NotImplementedError From d88381629896ba333a11c16c3c2f6eecd9b72cbc Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 11:54:50 +0100 Subject: [PATCH 23/42] --optimize-fallthru and --dump-fallthru-info options. --- HISTORY.md | 5 +++-- bin/sixtypical | 18 ++++++++++-------- tests/SixtyPical Fallthru.md | 6 +++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 993e466..3af0224 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -10,8 +10,9 @@ History of SixtyPical * Accessing zero-page with `ld` and `st` generates zero-page opcodes. * A `byte` or `word` table can be initialized with a list of constants. * Branching and repeating on the `n` flag is now supported. -* The `--dump-fallthru-map` option outputs a map, in JSON format, of - which routines can be "fallen through" to by other routines. +* The `--optimize-fallthru` option causes the program to be analyzed + for fallthru optimizations; `--dump-fallthru-info` option outputs the + information from this analysis phase, in JSON format, to stdout. * Specifying multiple SixtyPical source files will produce a single compiled result from their combination. * Rudimentary support for Atari 2600 prelude in a 4K cartridge image, diff --git a/bin/sixtypical b/bin/sixtypical index ab714f7..9d74865 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -58,15 +58,16 @@ def process_input_files(filenames, options): analyzer = Analyzer(debug=options.debug) analyzer.analyze_program(program) - if options.dump_fallthru_map: - import json + if options.optimize_fallthru: from sixtypical.fallthru import FallthruAnalyzer fa = FallthruAnalyzer(debug=options.debug) fa.analyze_program(program) - sys.stdout.write(json.dumps(fa.fallthru_map, indent=4, sort_keys=True)) - sys.stdout.write("\n") + if options.dump_fallthru_info: + import json + sys.stdout.write(json.dumps(fa.fallthru_map, indent=4, sort_keys=True)) + sys.stdout.write("\n") if options.analyze_only: return @@ -156,14 +157,15 @@ if __name__ == '__main__': help="Only parse and analyze the program; do not compile it." ) argparser.add_argument( - "--dump-fallthru-map", + "--optimize-fallthru", action="store_true", - help="Dump the fallthru map to stdout after analyzing the program." + help="Reorder the routines in the program to maximize the number of tail calls " + "that can be removed by having execution 'fall through' to the next routine." ) argparser.add_argument( - "--dump-fallthru-ordering", + "--dump-fallthru-info", action="store_true", - help="Dump the fallthru ordering to stdout after analyzing the program." + help="Dump the fallthru map and ordering to stdout after analyzing the program." ) argparser.add_argument( "--parse-only", diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index a27e998..f913fa9 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -51,10 +51,10 @@ When times comes to generate code, generate it in the order given by L. [Falderal]: http://catseye.tc/node/Falderal - -> Functionality "Dump fallthru map of SixtyPical program" is implemented by - -> shell command "bin/sixtypical --analyze-only --dump-fallthru-map --traceback %(test-body-file)" + -> Functionality "Dump fallthru info for SixtyPical program" is implemented by + -> shell command "bin/sixtypical --optimize-fallthru --dump-fallthru-info --analyze-only --traceback %(test-body-file)" - -> Tests for functionality "Dump fallthru map of SixtyPical program" + -> Tests for functionality "Dump fallthru info for SixtyPical program" A single routine, obviously, falls through to nothing and has nothing fall through to it. From 50433ac860c043cef049c626fd676dcd06590c5d Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 13:37:32 +0100 Subject: [PATCH 24/42] Find cycles in fallthru map. Try to break them; not right, yet. --- bin/sixtypical | 27 +++++++++++++++++++++++++-- src/sixtypical/fallthru.py | 30 +++++++++++++++++++++++++++++- tests/SixtyPical Fallthru.md | 11 +++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index 9d74865..96b471d 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -59,16 +59,39 @@ def process_input_files(filenames, options): analyzer.analyze_program(program) if options.optimize_fallthru: + import json from sixtypical.fallthru import FallthruAnalyzer fa = FallthruAnalyzer(debug=options.debug) fa.analyze_program(program) if options.dump_fallthru_info: - import json - sys.stdout.write(json.dumps(fa.fallthru_map, indent=4, sort_keys=True)) + fallthru_map = dict(fa.fallthru_map) + sys.stdout.write(json.dumps(fallthru_map, indent=4, sort_keys=True)) sys.stdout.write("\n") + fa.find_ancestors() + + if options.debug and options.dump_fallthru_info: + sys.stdout.write("*** ancestors:\n") + sys.stdout.write(json.dumps(fa.ancestor_map, indent=4, sort_keys=True)) + sys.stdout.write("\n") + + fa.find_cycles() + + if fa.cycles_found: + if options.dump_fallthru_info: + sys.stdout.write("*** cycles found:\n") + sys.stdout.write(json.dumps(sorted(fa.cycles_found), indent=4, sort_keys=True)) + sys.stdout.write("\n") + + fa.break_cycles() + + if options.dump_fallthru_info and fallthru_map != fa.fallthru_map: + sys.stdout.write("*** after breaking cycles:\n") + sys.stdout.write(json.dumps(fa.fallthru_map, indent=4, sort_keys=True)) + sys.stdout.write("\n") + if options.analyze_only: return diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py index d7face9..62997f8 100644 --- a/src/sixtypical/fallthru.py +++ b/src/sixtypical/fallthru.py @@ -3,6 +3,13 @@ from sixtypical.model import RoutineType +def make_transitive_closure(d, key, s): + for sub in d.get(key, []): + if sub not in s: + s.add(sub) + make_transitive_closure(d, sub, s) + + class FallthruAnalyzer(object): def __init__(self, debug=False): @@ -15,9 +22,30 @@ class FallthruAnalyzer(object): if len(encountered_gotos) == 1 and isinstance(encountered_gotos[0].type, RoutineType): fallthru_map.setdefault(encountered_gotos[0].name, set()).add(routine.name) self.fallthru_map = dict([(k, sorted(v)) for k, v in fallthru_map.iteritems()]) + return self.fallthru_map + + def find_ancestors(self): + self.ancestor_map = {} + for key in self.fallthru_map: + ancestors = set() + make_transitive_closure(self.fallthru_map, key, ancestors) + self.ancestor_map[key] = sorted(ancestors) + return self.ancestor_map + + def find_cycles(self): + self.cycles_found = set() + for key in self.ancestor_map: + if key in self.ancestor_map[key]: + self.cycles_found.add(key) + return self.cycles_found def break_cycles(self): - raise NotImplementedError + new_fallthru_map = {} + for key in self.fallthru_map: + values = set(self.fallthru_map[key]) - self.cycles_found + if values: + new_fallthru_map[key] = sorted(values) + self.fallthru_map = new_fallthru_map def serialize(self): raise NotImplementedError diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index f913fa9..72efa67 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -134,6 +134,17 @@ fall through to the other. = "bar" = ] = } + = *** cycles found: + = [ + = "bar", + = "foo" + = ] + = *** after breaking cycles: + = { + = "bar": [ + = "foo" + = ] + = } If a routine does two tail calls (which is possible because they can be in different branches of an `if`) it cannot fall through to another From 448849ac4b13ed17fb595344300d9df19c971eeb Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 14:01:17 +0100 Subject: [PATCH 25/42] Successfully break cycle. --- bin/sixtypical | 26 +++++++++++++------------- src/sixtypical/fallthru.py | 12 +++++++----- tests/SixtyPical Fallthru.md | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index 96b471d..6b1932d 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -66,32 +66,32 @@ def process_input_files(filenames, options): fa.analyze_program(program) if options.dump_fallthru_info: - fallthru_map = dict(fa.fallthru_map) - sys.stdout.write(json.dumps(fallthru_map, indent=4, sort_keys=True)) - sys.stdout.write("\n") - - fa.find_ancestors() - - if options.debug and options.dump_fallthru_info: - sys.stdout.write("*** ancestors:\n") - sys.stdout.write(json.dumps(fa.ancestor_map, indent=4, sort_keys=True)) + sys.stdout.write(json.dumps(fa.fallthru_map, indent=4, sort_keys=True)) sys.stdout.write("\n") fa.find_cycles() - if fa.cycles_found: + while fa.cycles_found: if options.dump_fallthru_info: + + if options.debug: + sys.stdout.write("*** ancestors:\n") + sys.stdout.write(json.dumps(fa.ancestor_map, indent=4, sort_keys=True)) + sys.stdout.write("\n") + sys.stdout.write("*** cycles found:\n") sys.stdout.write(json.dumps(sorted(fa.cycles_found), indent=4, sort_keys=True)) sys.stdout.write("\n") - fa.break_cycles() + fa.break_cycle() - if options.dump_fallthru_info and fallthru_map != fa.fallthru_map: - sys.stdout.write("*** after breaking cycles:\n") + if options.dump_fallthru_info: + sys.stdout.write("*** after breaking cycle:\n") sys.stdout.write(json.dumps(fa.fallthru_map, indent=4, sort_keys=True)) sys.stdout.write("\n") + fa.find_cycles() + if options.analyze_only: return diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py index 62997f8..f0ca70d 100644 --- a/src/sixtypical/fallthru.py +++ b/src/sixtypical/fallthru.py @@ -24,25 +24,27 @@ class FallthruAnalyzer(object): self.fallthru_map = dict([(k, sorted(v)) for k, v in fallthru_map.iteritems()]) return self.fallthru_map - def find_ancestors(self): + def find_cycles(self): self.ancestor_map = {} for key in self.fallthru_map: ancestors = set() make_transitive_closure(self.fallthru_map, key, ancestors) self.ancestor_map[key] = sorted(ancestors) - return self.ancestor_map - def find_cycles(self): self.cycles_found = set() for key in self.ancestor_map: if key in self.ancestor_map[key]: self.cycles_found.add(key) + return self.cycles_found - def break_cycles(self): + def break_cycle(self): + cycle_to_break = sorted(self.cycles_found)[0] + cycles_to_break = set([cycle_to_break]) + new_fallthru_map = {} for key in self.fallthru_map: - values = set(self.fallthru_map[key]) - self.cycles_found + values = set(self.fallthru_map[key]) - cycles_to_break if values: new_fallthru_map[key] = sorted(values) self.fallthru_map = new_fallthru_map diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index 72efa67..7c4662a 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -139,7 +139,7 @@ fall through to the other. = "bar", = "foo" = ] - = *** after breaking cycles: + = *** after breaking cycle: = { = "bar": [ = "foo" From 9c196efe25ddda3fe57dd9a490e1a101568abaf3 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 14:13:53 +0100 Subject: [PATCH 26/42] Abstraction for dumping JSON info. --- bin/sixtypical | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index 6b1932d..836f090 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -59,15 +59,20 @@ def process_input_files(filenames, options): analyzer.analyze_program(program) if options.optimize_fallthru: - import json + def dump(label, data): + import json + if label: + sys.stdout.write("*** {}:\n".format(label)) + sys.stdout.write(json.dumps(data, indent=4, sort_keys=True)) + sys.stdout.write("\n") + from sixtypical.fallthru import FallthruAnalyzer fa = FallthruAnalyzer(debug=options.debug) fa.analyze_program(program) if options.dump_fallthru_info: - sys.stdout.write(json.dumps(fa.fallthru_map, indent=4, sort_keys=True)) - sys.stdout.write("\n") + dump(None, fa.fallthru_map) fa.find_cycles() @@ -75,20 +80,13 @@ def process_input_files(filenames, options): if options.dump_fallthru_info: if options.debug: - sys.stdout.write("*** ancestors:\n") - sys.stdout.write(json.dumps(fa.ancestor_map, indent=4, sort_keys=True)) - sys.stdout.write("\n") - - sys.stdout.write("*** cycles found:\n") - sys.stdout.write(json.dumps(sorted(fa.cycles_found), indent=4, sort_keys=True)) - sys.stdout.write("\n") + dump('ancestors', fa.ancestor_map) + dump('cycles found', sorted(fa.cycles_found)) fa.break_cycle() if options.dump_fallthru_info: - sys.stdout.write("*** after breaking cycle:\n") - sys.stdout.write(json.dumps(fa.fallthru_map, indent=4, sort_keys=True)) - sys.stdout.write("\n") + dump('after breaking cycle', fa.fallthru_map) fa.find_cycles() From e39dbf68ede2b24d9ce6f0c4be815e00542f9bdc Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 14:16:49 +0100 Subject: [PATCH 27/42] Call it fall_in_map. --- bin/sixtypical | 4 ++-- src/sixtypical/fallthru.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index 836f090..9d93456 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -72,7 +72,7 @@ def process_input_files(filenames, options): fa.analyze_program(program) if options.dump_fallthru_info: - dump(None, fa.fallthru_map) + dump(None, fa.fall_in_map) fa.find_cycles() @@ -86,7 +86,7 @@ def process_input_files(filenames, options): fa.break_cycle() if options.dump_fallthru_info: - dump('after breaking cycle', fa.fallthru_map) + dump('after breaking cycle', fa.fall_in_map) fa.find_cycles() diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py index f0ca70d..fe440cb 100644 --- a/src/sixtypical/fallthru.py +++ b/src/sixtypical/fallthru.py @@ -16,19 +16,19 @@ class FallthruAnalyzer(object): self.debug = debug def analyze_program(self, program): - fallthru_map = {} + fall_in_map = {} for routine in program.routines: encountered_gotos = list(routine.encountered_gotos) if len(encountered_gotos) == 1 and isinstance(encountered_gotos[0].type, RoutineType): - fallthru_map.setdefault(encountered_gotos[0].name, set()).add(routine.name) - self.fallthru_map = dict([(k, sorted(v)) for k, v in fallthru_map.iteritems()]) - return self.fallthru_map + fall_in_map.setdefault(encountered_gotos[0].name, set()).add(routine.name) + self.fall_in_map = dict([(k, sorted(v)) for k, v in fall_in_map.iteritems()]) + return self.fall_in_map def find_cycles(self): self.ancestor_map = {} - for key in self.fallthru_map: + for key in self.fall_in_map: ancestors = set() - make_transitive_closure(self.fallthru_map, key, ancestors) + make_transitive_closure(self.fall_in_map, key, ancestors) self.ancestor_map[key] = sorted(ancestors) self.cycles_found = set() @@ -42,12 +42,12 @@ class FallthruAnalyzer(object): cycle_to_break = sorted(self.cycles_found)[0] cycles_to_break = set([cycle_to_break]) - new_fallthru_map = {} - for key in self.fallthru_map: - values = set(self.fallthru_map[key]) - cycles_to_break + new_fall_in_map = {} + for key in self.fall_in_map: + values = set(self.fall_in_map[key]) - cycles_to_break if values: - new_fallthru_map[key] = sorted(values) - self.fallthru_map = new_fallthru_map + new_fall_in_map[key] = sorted(values) + self.fall_in_map = new_fall_in_map def serialize(self): raise NotImplementedError From aecea9b6a3dadac16622426595066df19e41e6bd Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 14:20:56 +0100 Subject: [PATCH 28/42] Optomize dump() abstraction. (You muss optomize, always optomize.) --- bin/sixtypical | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index 9d93456..069b1ba 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -61,6 +61,8 @@ def process_input_files(filenames, options): if options.optimize_fallthru: def dump(label, data): import json + if not options.dump_fallthru_info: + return if label: sys.stdout.write("*** {}:\n".format(label)) sys.stdout.write(json.dumps(data, indent=4, sort_keys=True)) @@ -70,24 +72,15 @@ def process_input_files(filenames, options): fa = FallthruAnalyzer(debug=options.debug) fa.analyze_program(program) - - if options.dump_fallthru_info: - dump(None, fa.fall_in_map) + dump(None, fa.fall_in_map) fa.find_cycles() - while fa.cycles_found: - if options.dump_fallthru_info: - - if options.debug: - dump('ancestors', fa.ancestor_map) - dump('cycles found', sorted(fa.cycles_found)) - + if options.debug: + dump('ancestors', fa.ancestor_map) + dump('cycles found', sorted(fa.cycles_found)) fa.break_cycle() - - if options.dump_fallthru_info: - dump('after breaking cycle', fa.fall_in_map) - + dump('after breaking cycle', fa.fall_in_map) fa.find_cycles() if options.analyze_only: From 30e839033c8c4c9a7670afab58fba47795c59a82 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 15:09:48 +0100 Subject: [PATCH 29/42] First cut at serialization of fallthru-optimized routines. --- bin/sixtypical | 7 +++++-- src/sixtypical/fallthru.py | 20 +++++++++++++++++++- tests/SixtyPical Fallthru.md | 11 +++++++++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index 069b1ba..4934005 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -59,6 +59,8 @@ def process_input_files(filenames, options): analyzer.analyze_program(program) if options.optimize_fallthru: + from sixtypical.fallthru import FallthruAnalyzer + def dump(label, data): import json if not options.dump_fallthru_info: @@ -68,8 +70,6 @@ def process_input_files(filenames, options): sys.stdout.write(json.dumps(data, indent=4, sort_keys=True)) sys.stdout.write("\n") - from sixtypical.fallthru import FallthruAnalyzer - fa = FallthruAnalyzer(debug=options.debug) fa.analyze_program(program) dump(None, fa.fall_in_map) @@ -83,6 +83,9 @@ def process_input_files(filenames, options): dump('after breaking cycle', fa.fall_in_map) fa.find_cycles() + routines_list = fa.serialize() + #dump('serialization', routines_list) + if options.analyze_only: return diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py index fe440cb..00c147d 100644 --- a/src/sixtypical/fallthru.py +++ b/src/sixtypical/fallthru.py @@ -1,5 +1,7 @@ # encoding: UTF-8 +from copy import copy + from sixtypical.model import RoutineType @@ -50,4 +52,20 @@ class FallthruAnalyzer(object): self.fall_in_map = new_fall_in_map def serialize(self): - raise NotImplementedError + self.fall_out_map = {} + for key, values in self.fall_in_map.iteritems(): + for value in values: + assert value not in self.fall_out_map + self.fall_out_map[value] = key + + routine_list = [] + fall_out_map = copy(self.fall_out_map) + while fall_out_map: + key = fall_out_map.keys()[0] + # ... + # Find the longest chain of routines r1,r2,...rn in R where out(r1) = {r2}, out(r2} = {r3}, ... out(rn-1) = {rn}, and rn = r. + # Remove (r1,r2,...,rn) from R and append them to L in that order. Mark (r1,r2,...rn-1) as "will have their final goto removed." + # + del fall_out_map[key] + + return routine_list diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index 7c4662a..81d398a 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -34,8 +34,15 @@ a single routine, the structures we find will be tree-like, not DAG-like. But they do permit cycles. -So, we first break those cycles. We will be left with out() sets which -are disjoint trees, i.e. if r1 ∈ in(r2), then r1 ∉ in(r3) for all r3 ≠ r2. +So, we first break those cycles. (Is there a "best" way to do this? +Perhaps. But for now, we just break them arbitrarily; pick a r1 that +has a cycle and remove it from in(r2) for all r2. This also means +that, now, out(r1) = ∅. Then check if there are still cycles, and keep +picking one and breaking it until there are no cycles remaining.) + +We will be left with out() sets which are disjoint trees, i.e. +if r1 ∈ in(r2), then r1 ∉ in(r3) for all r3 ≠ r2. Also, +out(r1) = ∅ → for all r2, r1 ∉ in(r2). We then follow an algorithm something like this. Treat R as a mutable set and start with an empty list L. Then, From b1bcc21ffc880976775f7297af9caf26d2699ab5 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 16:22:14 +0100 Subject: [PATCH 30/42] Go slightly further with the serialization. --- bin/sixtypical | 2 +- src/sixtypical/fallthru.py | 17 ++++++-- tests/SixtyPical Fallthru.md | 78 ++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index 4934005..9827d52 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -84,7 +84,7 @@ def process_input_files(filenames, options): fa.find_cycles() routines_list = fa.serialize() - #dump('serialization', routines_list) + dump('serialization', routines_list) if options.analyze_only: return diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py index 00c147d..b6cc27c 100644 --- a/src/sixtypical/fallthru.py +++ b/src/sixtypical/fallthru.py @@ -18,6 +18,7 @@ class FallthruAnalyzer(object): self.debug = debug def analyze_program(self, program): + self.program = program fall_in_map = {} for routine in program.routines: encountered_gotos = list(routine.encountered_gotos) @@ -57,15 +58,25 @@ class FallthruAnalyzer(object): for value in values: assert value not in self.fall_out_map self.fall_out_map[value] = key + for routine in self.program.routines: + if routine.name not in self.fall_out_map: + self.fall_out_map[routine.name] = None routine_list = [] fall_out_map = copy(self.fall_out_map) while fall_out_map: key = fall_out_map.keys()[0] - # ... + in_set = self.fall_in_map.get(key, []) # Find the longest chain of routines r1,r2,...rn in R where out(r1) = {r2}, out(r2} = {r3}, ... out(rn-1) = {rn}, and rn = r. + # TODO implement this + routines = [key] + # Remove (r1,r2,...,rn) from R and append them to L in that order. Mark (r1,r2,...rn-1) as "will have their final goto removed." - # - del fall_out_map[key] + for r in routines: + del fall_out_map[r] + if r == routines[-1]: + routine_list.append(['retain', r]) + else: + routine_list.append(['fallthru', r]) return routine_list diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index 81d398a..f6b4ae9 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -70,6 +70,13 @@ through to it. | { | } = {} + = *** serialization: + = [ + = [ + = "retain", + = "main" + = ] + = ] If main does a `goto foo`, then it can fall through to `foo`. @@ -87,6 +94,17 @@ If main does a `goto foo`, then it can fall through to `foo`. = "main" = ] = } + = *** serialization: + = [ + = [ + = "fallthru", + = "main" + = ], + = [ + = "retain", + = "foo" + = ] + = ] More than one routine can fall through to a routine. @@ -113,6 +131,21 @@ If main does a `goto foo`, then it can fall through to `foo`. = "main" = ] = } + = *** serialization: + = [ + = [ + = "fallthru", + = "main" + = ], + = [ + = "retain", + = "foo" + = ], + = [ + = "retain", + = "bar" + = ] + = ] There is nothing stopping two routines from tail-calling each other, but we will only be able to make one of them, at most, @@ -152,6 +185,21 @@ fall through to the other. = "foo" = ] = } + = *** serialization: + = [ + = [ + = "retain", + = "main" + = ], + = [ + = "fallthru", + = "bar" + = ], + = [ + = "retain", + = "foo" + = ] + = ] If a routine does two tail calls (which is possible because they can be in different branches of an `if`) it cannot fall through to another @@ -176,6 +224,21 @@ routine. | } | } = {} + = *** serialization: + = [ + = [ + = "retain", + = "main" + = ], + = [ + = "retain", + = "bar" + = ], + = [ + = "retain", + = "foo" + = ] + = ] Similarly, a tail call to a vector can't be turned into a fallthru, because we don't necessarily know what actual routine the vector contains. @@ -199,3 +262,18 @@ because we don't necessarily know what actual routine the vector contains. | goto vec | } = {} + = *** serialization: + = [ + = [ + = "retain", + = "main" + = ], + = [ + = "retain", + = "bar" + = ], + = [ + = "retain", + = "foo" + = ] + = ] From efe8859209290d6756a4d8b40b4530f78cc9e010 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 16:54:12 +0100 Subject: [PATCH 31/42] Find the longest chain of routines in R and remove it. --- src/sixtypical/fallthru.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py index b6cc27c..8a72e77 100644 --- a/src/sixtypical/fallthru.py +++ b/src/sixtypical/fallthru.py @@ -12,6 +12,17 @@ def make_transitive_closure(d, key, s): make_transitive_closure(d, sub, s) +def find_chains(d, key, pred): + chains = [] + for sub in d.get(key, []): + if pred(sub): + subchains = find_chains(d, sub, pred) + for subchain in subchains: + chains.append([key] + subchain) + chains.append([key]) + return chains + + class FallthruAnalyzer(object): def __init__(self, debug=False): @@ -63,17 +74,23 @@ class FallthruAnalyzer(object): self.fall_out_map[routine.name] = None routine_list = [] - fall_out_map = copy(self.fall_out_map) - while fall_out_map: - key = fall_out_map.keys()[0] - in_set = self.fall_in_map.get(key, []) - # Find the longest chain of routines r1,r2,...rn in R where out(r1) = {r2}, out(r2} = {r3}, ... out(rn-1) = {rn}, and rn = r. - # TODO implement this - routines = [key] + pending_routines = copy(self.fall_out_map) + while pending_routines: + # Pick a routine that is still pending to be serialized. + key = pending_routines.keys()[0] - # Remove (r1,r2,...,rn) from R and append them to L in that order. Mark (r1,r2,...rn-1) as "will have their final goto removed." + in_set = self.fall_in_map.get(key, []) + + # Find the longest chain of routines r1,r2,...rn in R + # where out(r1) = {r2}, out(r2} = {r3}, ... out(rn-1) = {rn}, and rn = r. + chains = find_chains(self.fall_in_map, key, lambda k: k in pending_routines) + chains.sort(key=len, reverse=True) + routines = chains[0] + + # Remove (r1,r2,...,rn) from R and append them to L in that order. + # Mark (r1,r2,...rn-1) as "will have their final goto removed." for r in routines: - del fall_out_map[r] + del pending_routines[r] if r == routines[-1]: routine_list.append(['retain', r]) else: From cab84b8ebe466bb6f7b498492d0843cc941004ed Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 17:05:48 +0100 Subject: [PATCH 32/42] Change the serialization format to be (hopefully) clearer. --- src/sixtypical/fallthru.py | 14 ++++++-------- tests/SixtyPical Fallthru.md | 32 +++++--------------------------- 2 files changed, 11 insertions(+), 35 deletions(-) diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py index 8a72e77..5913410 100644 --- a/src/sixtypical/fallthru.py +++ b/src/sixtypical/fallthru.py @@ -73,7 +73,7 @@ class FallthruAnalyzer(object): if routine.name not in self.fall_out_map: self.fall_out_map[routine.name] = None - routine_list = [] + roster = [] pending_routines = copy(self.fall_out_map) while pending_routines: # Pick a routine that is still pending to be serialized. @@ -87,13 +87,11 @@ class FallthruAnalyzer(object): chains.sort(key=len, reverse=True) routines = chains[0] - # Remove (r1,r2,...,rn) from R and append them to L in that order. - # Mark (r1,r2,...rn-1) as "will have their final goto removed." + # Append (r1,r2,...,rn) to the roster and remove r1,r2,...rn from R. + # A sublist like this appearing in the roster has meaning + # "optimize the final goto out of all but the last routine in the sublist". for r in routines: del pending_routines[r] - if r == routines[-1]: - routine_list.append(['retain', r]) - else: - routine_list.append(['fallthru', r]) + roster.append(routines) - return routine_list + return roster diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index f6b4ae9..ef0e40b 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -73,7 +73,6 @@ through to it. = *** serialization: = [ = [ - = "retain", = "main" = ] = ] @@ -97,18 +96,13 @@ If main does a `goto foo`, then it can fall through to `foo`. = *** serialization: = [ = [ - = "fallthru", - = "main" - = ], - = [ - = "retain", + = "main", = "foo" = ] = ] -More than one routine can fall through to a routine. - -If main does a `goto foo`, then it can fall through to `foo`. +More than one routine can fall through to a routine. We pick one +of them to fall through, when selecting the order of routines. | define foo routine trashes a, z, n | { @@ -134,15 +128,10 @@ If main does a `goto foo`, then it can fall through to `foo`. = *** serialization: = [ = [ - = "fallthru", - = "main" - = ], - = [ - = "retain", + = "main", = "foo" = ], = [ - = "retain", = "bar" = ] = ] @@ -188,15 +177,10 @@ fall through to the other. = *** serialization: = [ = [ - = "retain", = "main" = ], = [ - = "fallthru", - = "bar" - = ], - = [ - = "retain", + = "bar", = "foo" = ] = ] @@ -227,15 +211,12 @@ routine. = *** serialization: = [ = [ - = "retain", = "main" = ], = [ - = "retain", = "bar" = ], = [ - = "retain", = "foo" = ] = ] @@ -265,15 +246,12 @@ because we don't necessarily know what actual routine the vector contains. = *** serialization: = [ = [ - = "retain", = "main" = ], = [ - = "retain", = "bar" = ], = [ - = "retain", = "foo" = ] = ] From 9c68b6a7e0040a0e1f5a317d5327003872b44c31 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 17:49:50 +0100 Subject: [PATCH 33/42] Serialize routines with in() sets first, and in the right order. --- src/sixtypical/fallthru.py | 19 ++++++++----------- tests/SixtyPical Fallthru.md | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py index 5913410..fc2f609 100644 --- a/src/sixtypical/fallthru.py +++ b/src/sixtypical/fallthru.py @@ -64,20 +64,16 @@ class FallthruAnalyzer(object): self.fall_in_map = new_fall_in_map def serialize(self): - self.fall_out_map = {} - for key, values in self.fall_in_map.iteritems(): - for value in values: - assert value not in self.fall_out_map - self.fall_out_map[value] = key - for routine in self.program.routines: - if routine.name not in self.fall_out_map: - self.fall_out_map[routine.name] = None + pending_routines = sorted(self.fall_in_map.keys()) + routine_names = sorted([routine.name for routine in self.program.routines]) + for routine_name in routine_names: + if routine_name not in pending_routines: + pending_routines.append(routine_name) roster = [] - pending_routines = copy(self.fall_out_map) while pending_routines: # Pick a routine that is still pending to be serialized. - key = pending_routines.keys()[0] + key = pending_routines[0] in_set = self.fall_in_map.get(key, []) @@ -86,12 +82,13 @@ class FallthruAnalyzer(object): chains = find_chains(self.fall_in_map, key, lambda k: k in pending_routines) chains.sort(key=len, reverse=True) routines = chains[0] + routines.reverse() # Append (r1,r2,...,rn) to the roster and remove r1,r2,...rn from R. # A sublist like this appearing in the roster has meaning # "optimize the final goto out of all but the last routine in the sublist". for r in routines: - del pending_routines[r] + pending_routines.remove(r) roster.append(routines) return roster diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index ef0e40b..7d89734 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -128,11 +128,11 @@ of them to fall through, when selecting the order of routines. = *** serialization: = [ = [ - = "main", + = "bar", = "foo" = ], = [ - = "bar" + = "main" = ] = ] @@ -177,11 +177,11 @@ fall through to the other. = *** serialization: = [ = [ - = "main" + = "foo", + = "bar" = ], = [ - = "bar", - = "foo" + = "main" = ] = ] @@ -211,13 +211,13 @@ routine. = *** serialization: = [ = [ - = "main" - = ], - = [ = "bar" = ], = [ = "foo" + = ], + = [ + = "main" = ] = ] @@ -246,12 +246,12 @@ because we don't necessarily know what actual routine the vector contains. = *** serialization: = [ = [ - = "main" - = ], - = [ = "bar" = ], = [ = "foo" + = ], + = [ + = "main" = ] = ] From 7fa54f53cbe4c5a4298c10208bc23b78a3a8c99a Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 4 Apr 2018 17:55:59 +0100 Subject: [PATCH 34/42] Serialize main first. However, this could use a fresh approach. --- src/sixtypical/fallthru.py | 10 ++++++++++ tests/SixtyPical Fallthru.md | 27 +++++++++++++++------------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py index fc2f609..eebfd89 100644 --- a/src/sixtypical/fallthru.py +++ b/src/sixtypical/fallthru.py @@ -64,12 +64,22 @@ class FallthruAnalyzer(object): self.fall_in_map = new_fall_in_map def serialize(self): + # NOTE, we can probably do this completely differently; + # construct the fall_out map + # construct fall_out chains + # sort these by length + # do the longest ones first + pending_routines = sorted(self.fall_in_map.keys()) routine_names = sorted([routine.name for routine in self.program.routines]) for routine_name in routine_names: if routine_name not in pending_routines: pending_routines.append(routine_name) + # make sure `main` appears first, whatever else may be the case. + pending_routines.remove('main') + pending_routines = ['main'] + pending_routines + roster = [] while pending_routines: # Pick a routine that is still pending to be serialized. diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index 7d89734..6bf24ea 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -104,6 +104,9 @@ If main does a `goto foo`, then it can fall through to `foo`. More than one routine can fall through to a routine. We pick one of them to fall through, when selecting the order of routines. +Also note, `main` is always serialized first, so that the entry +point of the entire program appears at the beginning of the code. + | define foo routine trashes a, z, n | { | ld a, 0 @@ -128,11 +131,11 @@ of them to fall through, when selecting the order of routines. = *** serialization: = [ = [ - = "bar", - = "foo" + = "main" = ], = [ - = "main" + = "bar", + = "foo" = ] = ] @@ -177,11 +180,11 @@ fall through to the other. = *** serialization: = [ = [ - = "foo", - = "bar" + = "main" = ], = [ - = "main" + = "foo", + = "bar" = ] = ] @@ -211,13 +214,13 @@ routine. = *** serialization: = [ = [ + = "main" + = ], + = [ = "bar" = ], = [ = "foo" - = ], - = [ - = "main" = ] = ] @@ -246,12 +249,12 @@ because we don't necessarily know what actual routine the vector contains. = *** serialization: = [ = [ + = "main" + = ], + = [ = "bar" = ], = [ = "foo" - = ], - = [ - = "main" = ] = ] From ac24f98dd3393ee5e6679d653f13032ca5006e13 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 5 Apr 2018 09:57:14 +0100 Subject: [PATCH 35/42] Vastly simplify the fallthru analysis algorithm. --- bin/sixtypical | 15 +---- src/sixtypical/fallthru.py | 105 +++++++++------------------------- tests/SixtyPical Fallthru.md | 106 ++++++++++++----------------------- 3 files changed, 63 insertions(+), 163 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index 9827d52..2ade86c 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -61,7 +61,7 @@ def process_input_files(filenames, options): if options.optimize_fallthru: from sixtypical.fallthru import FallthruAnalyzer - def dump(label, data): + def dump(data, label=None): import json if not options.dump_fallthru_info: return @@ -72,19 +72,8 @@ def process_input_files(filenames, options): fa = FallthruAnalyzer(debug=options.debug) fa.analyze_program(program) - dump(None, fa.fall_in_map) - - fa.find_cycles() - while fa.cycles_found: - if options.debug: - dump('ancestors', fa.ancestor_map) - dump('cycles found', sorted(fa.cycles_found)) - fa.break_cycle() - dump('after breaking cycle', fa.fall_in_map) - fa.find_cycles() - routines_list = fa.serialize() - dump('serialization', routines_list) + dump(routines_list) if options.analyze_only: return diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py index eebfd89..f33248c 100644 --- a/src/sixtypical/fallthru.py +++ b/src/sixtypical/fallthru.py @@ -5,24 +5,6 @@ from copy import copy from sixtypical.model import RoutineType -def make_transitive_closure(d, key, s): - for sub in d.get(key, []): - if sub not in s: - s.add(sub) - make_transitive_closure(d, sub, s) - - -def find_chains(d, key, pred): - chains = [] - for sub in d.get(key, []): - if pred(sub): - subchains = find_chains(d, sub, pred) - for subchain in subchains: - chains.append([key] + subchain) - chains.append([key]) - return chains - - class FallthruAnalyzer(object): def __init__(self, debug=False): @@ -30,75 +12,40 @@ class FallthruAnalyzer(object): def analyze_program(self, program): self.program = program - fall_in_map = {} + + self.fallthru_map = {} for routine in program.routines: encountered_gotos = list(routine.encountered_gotos) if len(encountered_gotos) == 1 and isinstance(encountered_gotos[0].type, RoutineType): - fall_in_map.setdefault(encountered_gotos[0].name, set()).add(routine.name) - self.fall_in_map = dict([(k, sorted(v)) for k, v in fall_in_map.iteritems()]) - return self.fall_in_map + self.fallthru_map[routine.name] = encountered_gotos[0].name + else: + self.fallthru_map[routine.name] = None - def find_cycles(self): - self.ancestor_map = {} - for key in self.fall_in_map: - ancestors = set() - make_transitive_closure(self.fall_in_map, key, ancestors) - self.ancestor_map[key] = sorted(ancestors) - - self.cycles_found = set() - for key in self.ancestor_map: - if key in self.ancestor_map[key]: - self.cycles_found.add(key) - - return self.cycles_found - - def break_cycle(self): - cycle_to_break = sorted(self.cycles_found)[0] - cycles_to_break = set([cycle_to_break]) - - new_fall_in_map = {} - for key in self.fall_in_map: - values = set(self.fall_in_map[key]) - cycles_to_break - if values: - new_fall_in_map[key] = sorted(values) - self.fall_in_map = new_fall_in_map + def find_chain(self, routine_name, available): + chain = [routine_name] + seen = set(chain) + while True: + next = self.fallthru_map.get(routine_name) + if next is None or next in seen or next not in available: + return chain + seen.add(next) + chain.append(next) def serialize(self): - # NOTE, we can probably do this completely differently; - # construct the fall_out map - # construct fall_out chains - # sort these by length - # do the longest ones first - - pending_routines = sorted(self.fall_in_map.keys()) - routine_names = sorted([routine.name for routine in self.program.routines]) - for routine_name in routine_names: - if routine_name not in pending_routines: - pending_routines.append(routine_name) - - # make sure `main` appears first, whatever else may be the case. - pending_routines.remove('main') - pending_routines = ['main'] + pending_routines - + pending_routines = copy(self.fallthru_map) roster = [] + + main_chain = self.find_chain('main', pending_routines) + roster.append(main_chain) + for k in main_chain: + del pending_routines[k] + while pending_routines: - # Pick a routine that is still pending to be serialized. - key = pending_routines[0] - - in_set = self.fall_in_map.get(key, []) - - # Find the longest chain of routines r1,r2,...rn in R - # where out(r1) = {r2}, out(r2} = {r3}, ... out(rn-1) = {rn}, and rn = r. - chains = find_chains(self.fall_in_map, key, lambda k: k in pending_routines) + chains = [self.find_chain(k, pending_routines) for k in pending_routines.keys()] chains.sort(key=len, reverse=True) - routines = chains[0] - routines.reverse() - - # Append (r1,r2,...,rn) to the roster and remove r1,r2,...rn from R. - # A sublist like this appearing in the roster has meaning - # "optimize the final goto out of all but the last routine in the sublist". - for r in routines: - pending_routines.remove(r) - roster.append(routines) + c = chains[0] + roster.append(c) + for k in c: + del pending_routines[k] return roster diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index 6bf24ea..580df47 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -18,43 +18,46 @@ in a routine, and each branch of that `if` ends with a different `goto`.) A *fixed* routine means, a routine which is known at compile time, not a `goto` through a vector. -Consider the set R of all routines in the program. +Consider the set R of all available routines in the program. -Every routine r1 ∈ R either potentially falls through to a single routine -r2 ∈ R (r2 ≠ r1) or it does not potentially fall through to any routine. -We can say out(r1) = {r2} or out(r1) = ∅. +Every routine either potentially falls through to a single other routine +or it does not potentially fall through to any routine. -Every routine r ∈ R in this set also has a set of zero or more -routines from which it is potentially falled through to by. Call this -in(r). It is the case that out(r1) = {r2} → r1 ∈ in(r2). +More formally, we can say -We can trace out the connections by following the in- or our- sets of -a given routine. Because each routine potentially falls through to only -a single routine, the structures we find will be tree-like, not DAG-like. +fall : R → R ∪ {nil}, fall(r) ≠ r -But they do permit cycles. +where `nil` is an atom that represents no routine. -So, we first break those cycles. (Is there a "best" way to do this? -Perhaps. But for now, we just break them arbitrarily; pick a r1 that -has a cycle and remove it from in(r2) for all r2. This also means -that, now, out(r1) = ∅. Then check if there are still cycles, and keep -picking one and breaking it until there are no cycles remaining.) +Now consider an operation chain() vaguely similar to a transitive closure +on fall(). Starting with r, we construct a list of r, fall(r), +fall(fall(r)), ... with the following restrictions: -We will be left with out() sets which are disjoint trees, i.e. -if r1 ∈ in(r2), then r1 ∉ in(r3) for all r3 ≠ r2. Also, -out(r1) = ∅ → for all r2, r1 ∉ in(r2). +- we stop when we reach `nil` (because fall(`nil`) is not defined) +- we stop when we see an element that is not in R. +- we stop when we see an element that we have already added to the + list (this is to prevent infinite lists due to cycles.) -We then follow an algorithm something like this. Treat R as a mutable -set and start with an empty list L. Then, +With these definitions, our algorithm is something like this. -- Pick a routine r from R where out(r) = ∅. -- Find the longest chain of routines r1,r2,...rn in R where out(r1) = {r2}, - out(r2} = {r3}, ... out(rn-1) = {rn}, and rn = r. -- Remove (r1,r2,...,rn) from R and append them to L in that order. - Mark (r1,r2,...rn-1) as "will have their final `goto` removed." -- Repeat until R is empty. +Treat R as a mutable set and start with an empty list of lists L. Then, + +- For all r ∈ R, find all chain(r). +- Pick a longest such chain. Call it C. +- Append C to L. +- Remove all elements occurring in C, from R. +- Repeat until R is empty. When times comes to generate code, generate it in the order given by L. +In addition, each sublist in L represents a number of routines to +generate; all except the final routine in such a sublist need not have +any jump instruction generated for its final `goto`. + +The tests in this document test against the list L. + +Note that this optimization is a feature of the SixtyPical's reference +compiler, not the language. So an implementation is not required +to pass these tests to be considered an implementation of SixtyPical. [Falderal]: http://catseye.tc/node/Falderal @@ -69,8 +72,6 @@ through to it. | define main routine | { | } - = {} - = *** serialization: = [ = [ = "main" @@ -88,12 +89,6 @@ If main does a `goto foo`, then it can fall through to `foo`. | { | goto foo | } - = { - = "foo": [ - = "main" - = ] - = } - = *** serialization: = [ = [ = "main", @@ -122,20 +117,13 @@ point of the entire program appears at the beginning of the code. | { | goto foo | } - = { - = "foo": [ - = "bar", - = "main" - = ] - = } - = *** serialization: = [ = [ - = "main" + = "main", + = "foo" = ], = [ - = "bar", - = "foo" + = "bar" = ] = ] @@ -158,33 +146,13 @@ fall through to the other. | define main routine trashes a, z, n | { | } - = { - = "bar": [ - = "foo" - = ], - = "foo": [ - = "bar" - = ] - = } - = *** cycles found: - = [ - = "bar", - = "foo" - = ] - = *** after breaking cycle: - = { - = "bar": [ - = "foo" - = ] - = } - = *** serialization: = [ = [ = "main" = ], = [ - = "foo", - = "bar" + = "bar", + = "foo" = ] = ] @@ -210,8 +178,6 @@ routine. | goto bar | } | } - = {} - = *** serialization: = [ = [ = "main" @@ -245,8 +211,6 @@ because we don't necessarily know what actual routine the vector contains. | copy bar, vec | goto vec | } - = {} - = *** serialization: = [ = [ = "main" From 2ca843ece75d9ffca6f6ab18583a21ec6333c501 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 5 Apr 2018 10:52:14 +0100 Subject: [PATCH 36/42] Begin hooking the fallthru analysis up to the compilation phase. --- bin/sixtypical | 7 ++++--- src/sixtypical/compiler.py | 14 +++++++++----- tests/SixtyPical Fallthru.md | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index 2ade86c..2197c2b 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -58,6 +58,7 @@ def process_input_files(filenames, options): analyzer = Analyzer(debug=options.debug) analyzer.analyze_program(program) + compilation_roster = None if options.optimize_fallthru: from sixtypical.fallthru import FallthruAnalyzer @@ -72,8 +73,8 @@ def process_input_files(filenames, options): fa = FallthruAnalyzer(debug=options.debug) fa.analyze_program(program) - routines_list = fa.serialize() - dump(routines_list) + compilation_roster = fa.serialize() + dump(compilation_roster) if options.analyze_only: return @@ -116,7 +117,7 @@ def process_input_files(filenames, options): for byte in prelude: emitter.emit(Byte(byte)) compiler = Compiler(emitter) - compiler.compile_program(program) + compiler.compile_program(program, compilation_roster=compilation_roster) # If we are outputting a cartridge with boot and BRK address # at the end, pad to ROM size minus 4 bytes, and emit addresses. diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index 113d8a2..b050af4 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -75,7 +75,7 @@ class Compiler(object): # visitor methods - def compile_program(self, program): + def compile_program(self, program, compilation_roster=None): assert isinstance(program, Program) defn_labels = [] @@ -102,10 +102,14 @@ class Compiler(object): defn_labels.append((defn, label)) self.routine_statics[routine.name] = static_labels - self.compile_routine(self.routines['main']) - for routine in program.routines: - if routine.name != 'main': - self.compile_routine(routine) + if compilation_roster is None: + compilation_roster = [['main']] + [[routine.name] for routine in program.routines if routine.name != 'main'] + + for roster_row in compilation_roster: + for routine_name in roster_row[0:-1]: + self.compile_routine(self.routines[routine_name]) + routine_name = roster_row[-1] + self.compile_routine(self.routines[routine_name]) for location, label in self.trampolines.iteritems(): self.emitter.resolve_label(label) diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index 580df47..9ae5a7f 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -64,6 +64,9 @@ to pass these tests to be considered an implementation of SixtyPical. -> Functionality "Dump fallthru info for SixtyPical program" is implemented by -> shell command "bin/sixtypical --optimize-fallthru --dump-fallthru-info --analyze-only --traceback %(test-body-file)" + -> Functionality "Compile SixtyPical program with fallthru optimization" is implemented by + -> shell command "bin/sixtypical --prelude=c64 --optimize-fallthru --traceback %(test-body-file) >/tmp/foo && tests/appliances/bin/dcc6502-adapter Tests for functionality "Dump fallthru info for SixtyPical program" A single routine, obviously, falls through to nothing and has nothing fall @@ -222,3 +225,32 @@ because we don't necessarily know what actual routine the vector contains. = "foo" = ] = ] + + -> Tests for functionality "Compile SixtyPical program with fallthru optimization" + +Basic test for actually applying this optimization when compiling SixtyPical programs. + +Note this currently reflects the re-ordering, but does not remove the jmp/rts. + + | define foo routine trashes a, z, n + | { + | ld a, 0 + | } + | + | define bar routine trashes a, z, n + | { + | ld a, 255 + | goto foo + | } + | + | define main routine trashes a, z, n + | { + | goto foo + | } + = $080D JMP $0811 + = $0810 RTS + = $0811 LDA #$00 + = $0813 RTS + = $0814 LDA #$FF + = $0816 JMP $0811 + = $0819 RTS From 27df0d27dbe67f2265edd8c65cec8bf1cb0a5edb Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 5 Apr 2018 14:10:04 +0100 Subject: [PATCH 37/42] Optimize away `RTS` and `JMP` when possible. Fallthru stuff done. --- HISTORY.md | 10 +++++++--- README.md | 6 +----- src/sixtypical/compiler.py | 28 ++++++++++++++++++---------- tests/SixtyPical Compilation.md | 9 ++++----- tests/SixtyPical Fallthru.md | 13 +++++-------- 5 files changed, 35 insertions(+), 31 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 3af0224..3f6ebdb 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -10,9 +10,13 @@ History of SixtyPical * Accessing zero-page with `ld` and `st` generates zero-page opcodes. * A `byte` or `word` table can be initialized with a list of constants. * Branching and repeating on the `n` flag is now supported. -* The `--optimize-fallthru` option causes the program to be analyzed - for fallthru optimizations; `--dump-fallthru-info` option outputs the - information from this analysis phase, in JSON format, to stdout. +* The `--optimize-fallthru` option causes the routines of the program + to be re-ordered to maximize the number of cases where a `goto`'ed + routine can be simply "falled through" to instead of `JMP`ed to. +* `--dump-fallthru-info` option outputs the information from the + fallthru analysis phase, in JSON format, to stdout. +* Even without fallthru optimization, `RTS` is no longer emitted after + the `JMP` from compiling a final `goto`. * Specifying multiple SixtyPical source files will produce a single compiled result from their combination. * Rudimentary support for Atari 2600 prelude in a 4K cartridge image, diff --git a/README.md b/README.md index 1ab78a9..1c25b7a 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Documentation * [Literate test suite for SixtyPical syntax](tests/SixtyPical%20Syntax.md) * [Literate test suite for SixtyPical analysis](tests/SixtyPical%20Analysis.md) * [Literate test suite for SixtyPical compilation](tests/SixtyPical%20Compilation.md) +* [Literate test suite for SixtyPical fallthru optimization](tests/SixtyPical%20Fallthru.md) * [6502 Opcodes used/not used in SixtyPical](doc/6502%20Opcodes.md) TODO @@ -73,11 +74,6 @@ TODO This preserves them, so that, semantically, they can be used later even though they 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 adequately is probably not that hard. - ### And at some point... * `low` and `high` address operators - to turn `word` type into `byte`. diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index b050af4..de01fba 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -107,7 +107,7 @@ class Compiler(object): for roster_row in compilation_roster: for routine_name in roster_row[0:-1]: - self.compile_routine(self.routines[routine_name]) + self.compile_routine(self.routines[routine_name], skip_final_goto=True) routine_name = roster_row[-1] self.compile_routine(self.routines[routine_name]) @@ -140,14 +140,18 @@ class Compiler(object): if defn.initial is None and defn.addr is None: self.emitter.resolve_bss_label(label) - def compile_routine(self, routine): + def compile_routine(self, routine, skip_final_goto=False): self.current_routine = routine + self.skip_final_goto = skip_final_goto + self.final_goto_seen = False assert isinstance(routine, Routine) if routine.block: self.emitter.resolve_label(self.get_label(routine.name)) self.compile_block(routine.block) - self.emitter.emit(RTS()) + if not self.final_goto_seen: + self.emitter.emit(RTS()) self.current_routine = None + self.skip_final_goto = False def compile_block(self, block): assert isinstance(block, Block) @@ -353,14 +357,18 @@ class Compiler(object): else: raise NotImplementedError elif opcode == 'goto': - location = instr.location - label = self.get_label(instr.location.name) - if isinstance(location.type, RoutineType): - self.emitter.emit(JMP(Absolute(label))) - elif isinstance(location.type, VectorType): - self.emitter.emit(JMP(Indirect(label))) + self.final_goto_seen = True + if self.skip_final_goto: + pass else: - raise NotImplementedError + location = instr.location + label = self.get_label(instr.location.name) + if isinstance(location.type, RoutineType): + self.emitter.emit(JMP(Absolute(label))) + elif isinstance(location.type, VectorType): + self.emitter.emit(JMP(Indirect(label))) + else: + raise NotImplementedError elif opcode == 'copy': self.compile_copy(instr, instr.src, instr.dest) elif opcode == 'trash': diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index 19c7250..d6562d0 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -776,7 +776,7 @@ Indirect call. = $081E JMP ($0822) = $0821 RTS -goto. +Compiling `goto`. Note that no `RTS` is emitted after the `JMP`. | routine bar | inputs y @@ -794,10 +794,9 @@ goto. | goto bar | } = $080D LDY #$C8 - = $080F JMP $0813 - = $0812 RTS - = $0813 LDX #$C8 - = $0815 RTS + = $080F JMP $0812 + = $0812 LDX #$C8 + = $0814 RTS ### Vector tables diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index 9ae5a7f..7f50462 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -25,7 +25,7 @@ or it does not potentially fall through to any routine. More formally, we can say -fall : R → R ∪ {nil}, fall(r) ≠ r +> fall : R → R ∪ {nil}, fall(r) ≠ r where `nil` is an atom that represents no routine. @@ -247,10 +247,7 @@ Note this currently reflects the re-ordering, but does not remove the jmp/rts. | { | goto foo | } - = $080D JMP $0811 - = $0810 RTS - = $0811 LDA #$00 - = $0813 RTS - = $0814 LDA #$FF - = $0816 JMP $0811 - = $0819 RTS + = $080D LDA #$00 + = $080F RTS + = $0810 LDA #$FF + = $0812 JMP $080D From cce278da28f883c1ca7fdd0c3e5fcd95c0cee91b Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 5 Apr 2018 14:36:38 +0100 Subject: [PATCH 38/42] Rebuild "The PETulant Cursor". --- eg/README.md | 3 ++- eg/c64/petulant/petulant-60p.prg | Bin 48 -> 46 bytes 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/eg/README.md b/eg/README.md index 58947cc..51c3d70 100644 --- a/eg/README.md +++ b/eg/README.md @@ -30,7 +30,8 @@ elaborate demos: the P65 assembler (now Ophis) and re-released on April 1st, 2008 (a hint as to its nature). - Translated to SixtyPical (in 2018), it's 48 bytes. + Translated to SixtyPical (in 2018), after adding some optimizations + to the SixtyPical compiler, the resulting executable is still 44 bytes! ### vic20 diff --git a/eg/c64/petulant/petulant-60p.prg b/eg/c64/petulant/petulant-60p.prg index 2ce000006345020d4891f08c96505bd8e1fee85c..3ecb862b5e8bcd397fd51c8f38e28afc43bee8c0 100644 GIT binary patch literal 46 zcmV+}0MY-a0(h+y1C7%HtrY`})B>r%jT8f^0*w^|SYV~k@CL1h0!+;Uts&5jAkb{n E0-_ocn*aa+ literal 48 zcmV-00MGxY0(h+y1C7-JtrY`})&i-(jT8f^0*w^|SYWNs0PqH_h5}5^0<9s?jUdo$ G)dFC_U=s=e From 350bab42d72b9f4bcb4272fee46069220b7834d4 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Thu, 5 Apr 2018 14:45:17 +0100 Subject: [PATCH 39/42] Rebuild Ribos 2. --- eg/c64/ribos/ribos2-60p.prg | Bin 91 -> 90 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/eg/c64/ribos/ribos2-60p.prg b/eg/c64/ribos/ribos2-60p.prg index c11bfc54826dab52b5597eb1755b930293dd4fe5..35896e02560bcd1f80737c47283cda8a1839da02 100644 GIT binary patch delta 62 zcma!!l4dwiu~vk+H{!rrQRd#r11t4=MVMC}=oMvN8$MB5StjCuPxyhg3Ku;8_bOai S8-4&thacz_y5Q6JDii<@2^(Gj delta 63 zcma!wmS#9mu~vk+H}b$*QRd#L11t4=MVMC}=oMvN8!=H@SvK;3Pxyhg3Ku;8_bOai T8*u Date: Fri, 6 Apr 2018 13:24:04 +0100 Subject: [PATCH 40/42] Add more tests. Apparently our algorithm does not do a good job. --- tests/SixtyPical Fallthru.md | 83 ++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index 7f50462..ec88370 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -81,7 +81,7 @@ through to it. = ] = ] -If main does a `goto foo`, then it can fall through to `foo`. +If `main` does a `goto foo`, then it can fall through to `foo`. | define foo routine trashes a, z, n | { @@ -102,9 +102,6 @@ If main does a `goto foo`, then it can fall through to `foo`. More than one routine can fall through to a routine. We pick one of them to fall through, when selecting the order of routines. -Also note, `main` is always serialized first, so that the entry -point of the entire program appears at the beginning of the code. - | define foo routine trashes a, z, n | { | ld a, 0 @@ -130,6 +127,29 @@ point of the entire program appears at the beginning of the code. = ] = ] +Because `main` is always serialized first (so that the entry +point of the entire program appears at the beginning of the code), +nothing ever falls through to `main`. + + | define foo routine trashes a, z, n + | { + | ld a, 0 + | goto main + | } + | + | define main routine trashes a, z, n + | { + | ld a, 1 + | } + = [ + = [ + = "main" + = ], + = [ + = "foo" + = ] + = ] + There is nothing stopping two routines from tail-calling each other, but we will only be able to make one of them, at most, fall through to the other. @@ -226,6 +246,61 @@ because we don't necessarily know what actual routine the vector contains. = ] = ] +Our algorithm might not be strictly optimal, but it does a good job. + + | define r1 routine trashes a, z, n + | { + | ld a, 0 + | goto r2 + | } + | + | define r2 routine trashes a, z, n + | { + | ld a, 0 + | goto r3 + | } + | + | define r3 routine trashes a, z, n + | { + | ld a, 0 + | goto r4 + | } + | + | define r4 routine trashes a, z, n + | { + | ld a, 0 + | } + | + | define r5 routine trashes a, z, n + | { + | ld a, 0 + | goto r6 + | } + | + | define r6 routine trashes a, z, n + | { + | ld a, 0 + | goto r3 + | } + | + | define main routine trashes a, z, n + | { + | goto r1 + | } + = [ + = [ + = "main", + = "r1", + = "r2", + = "r3", + = "r4" + = ], + = [ + = "r5", + = "r6" + = ] + = ] + -> Tests for functionality "Compile SixtyPical program with fallthru optimization" Basic test for actually applying this optimization when compiling SixtyPical programs. From 5f641134848b72694dd961d0b9cc8444b56c21bf Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Fri, 6 Apr 2018 13:27:40 +0100 Subject: [PATCH 41/42] 1-line bugfix. Now the optimizer does do the expected good job. --- src/sixtypical/fallthru.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py index f33248c..85024bd 100644 --- a/src/sixtypical/fallthru.py +++ b/src/sixtypical/fallthru.py @@ -30,6 +30,7 @@ class FallthruAnalyzer(object): return chain seen.add(next) chain.append(next) + routine_name = next def serialize(self): pending_routines = copy(self.fallthru_map) From 475584769d51d5990f15d0d76762d02628fd32f4 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Fri, 6 Apr 2018 14:08:09 +0100 Subject: [PATCH 42/42] Add more test cases. --- src/sixtypical/parser.py | 2 +- tests/SixtyPical Fallthru.md | 103 ++++++++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 86c9948..fa05526 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -444,7 +444,7 @@ class Parser(object): self.scanner.scan() dest = self.locexpr() return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=None) - elif self.scanner.token in ("nop"): + elif self.scanner.token in ("nop",): opcode = self.scanner.token self.scanner.scan() return SingleOp(self.scanner.line_number, opcode=opcode, dest=None, src=None) diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index ec88370..28023ca 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -213,6 +213,38 @@ routine. = ] = ] +If, however, they are the same goto, one can be optimized away. + + | define foo routine trashes a, z, n + | { + | ld a, 0 + | if z { + | ld a, 1 + | goto bar + | } else { + | ld a, 2 + | goto bar + | } + | } + | + | define bar routine trashes a, z, n + | { + | ld a, 255 + | } + | + | define main routine trashes a, z, n + | { + | } + = [ + = [ + = "main" + = ], + = [ + = "foo", + = "bar" + = ] + = ] + Similarly, a tail call to a vector can't be turned into a fallthru, because we don't necessarily know what actual routine the vector contains. @@ -305,8 +337,6 @@ Our algorithm might not be strictly optimal, but it does a good job. Basic test for actually applying this optimization when compiling SixtyPical programs. -Note this currently reflects the re-ordering, but does not remove the jmp/rts. - | define foo routine trashes a, z, n | { | ld a, 0 @@ -326,3 +356,72 @@ Note this currently reflects the re-ordering, but does not remove the jmp/rts. = $080F RTS = $0810 LDA #$FF = $0812 JMP $080D + +It can optimize out one of the `goto`s if they are the same. + + | define foo routine trashes a, z, n + | { + | ld a, 0 + | if z { + | ld a, 1 + | goto bar + | } else { + | ld a, 2 + | goto bar + | } + | } + | + | define bar routine trashes a, z, n + | { + | ld a, 255 + | } + | + | define main routine trashes a, z, n + | { + | } + = $080D RTS + = $080E LDA #$00 + = $0810 BNE $0817 + = $0812 LDA #$01 + = $0814 JMP $0819 + = $0817 LDA #$02 + = $0819 LDA #$FF + = $081B RTS + +It cannot optimize out the `goto`s if they are different. + +Note, this currently produces unfortunately unoptimized code, +because generating code for the "true" branch of an `if` always +generates a jump out of the `if`, even if the last instruction +in the "true" branch is a `goto`. + + | define foo routine trashes a, z, n + | { + | ld a, 0 + | if z { + | ld a, 1 + | goto bar + | } else { + | ld a, 2 + | goto main + | } + | } + | + | define bar routine trashes a, z, n + | { + | ld a, 255 + | } + | + | define main routine trashes a, z, n + | { + | } + = $080D RTS + = $080E LDA #$FF + = $0810 RTS + = $0811 LDA #$00 + = $0813 BNE $081D + = $0815 LDA #$01 + = $0817 JMP $080E + = $081A JMP $0822 + = $081D LDA #$02 + = $081F JMP $080D