From 952e3528a393b1685d69864c2b57ef4a68a0eae7 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 16 Jul 2019 16:22:29 +0100 Subject: [PATCH 01/36] Try this. --- .circleci/config.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..14b928d --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,26 @@ +# Python CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-python/ for more details +# +version: 2 +jobs: + build: + docker: + - image: circleci/python:3.6.1 + + working_directory: ~/SixtyPical + + steps: + - checkout + + - run: + name: install dependencies + command: | + git clone https://github.com/catseye/Falderal + git clone https://github.com/catseye/dcc6502 + (cd dcc6502 && make) + + - run: + name: run tests + command: | + PATH=dcc6502:Falderal/bin:$PATH ./test.sh From 775af38960a9b57316b190515bae5bff82f2e93c Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 16 Jul 2019 16:23:30 +0100 Subject: [PATCH 02/36] Change config. --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 14b928d..a0213db 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,6 +16,7 @@ jobs: - run: name: install dependencies command: | + echo "hi" git clone https://github.com/catseye/Falderal git clone https://github.com/catseye/dcc6502 (cd dcc6502 && make) From 0ef0dc162823cc9a02608976a1a0037bab049cd7 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 16 Jul 2019 16:26:16 +0100 Subject: [PATCH 03/36] dcc6502-adapter fixes. --- tests/appliances/bin/dcc6502-adapter | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/appliances/bin/dcc6502-adapter b/tests/appliances/bin/dcc6502-adapter index 28c20b6..85b6f17 100755 --- a/tests/appliances/bin/dcc6502-adapter +++ b/tests/appliances/bin/dcc6502-adapter @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 # script that allows the binary output of sixtypical --output-format=c64-basic-prg --compile to be # disassembled by https://github.com/tcarmelveilleux/dcc6502 @@ -19,5 +19,4 @@ f.close() lines = [line for line in check_output("dcc6502 -o 2061 {}".format(filename), shell=True).split('\n') if line and not line.startswith(';')] lines = [re.sub(r'\s*\;.*$', '', line) for line in lines] -lines.pop() sys.stdout.write('\n'.join(lines)) From 87a2f70092364e2fd9f087277cc4daf568ce836e Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 21 Oct 2019 14:03:35 +0100 Subject: [PATCH 04/36] First cut at constructing a call graph. --- bin/sixtypical | 12 ++++++++++++ src/sixtypical/analyzer.py | 2 ++ src/sixtypical/context.py | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/bin/sixtypical b/bin/sixtypical index c847389..bb9db27 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -50,6 +50,13 @@ def process_input_files(filenames, options): sys.stdout.write(json.dumps(analyzer.exit_contexts_map, indent=4, sort_keys=True, separators=(',', ':'))) sys.stdout.write("\n") + if options.dump_callgraph: + for routine in program.routines: + sys.stdout.write('-----------\n') + sys.stdout.write('{}\n'.format(routine.name)) + for called_routine in routine.called_routines: + sys.stdout.write(' {}\n'.format(called_routine.name)) + compilation_roster = None if options.optimize_fallthru: from sixtypical.fallthru import FallthruAnalyzer @@ -161,6 +168,11 @@ if __name__ == '__main__': action="store_true", help="Dump the ordered fallthru map, in JSON, to stdout after analyzing the program." ) + argparser.add_argument( + "--dump-callgraph", + action="store_true", + help="Dump the call graph, in JSON, 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 1e3991b..9e1b840 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -143,6 +143,7 @@ class Analyzer(object): for routine in program.routines: context = self.analyze_routine(routine) routine.encountered_gotos = list(context.encountered_gotos()) if context else [] + routine.called_routines = list(context.called_routines) if context else [] def analyze_routine(self, routine): assert isinstance(routine, Routine) @@ -515,6 +516,7 @@ class Analyzer(object): type = self.get_type(instr.location) if not isinstance(type, (RoutineType, VectorType)): raise TypeMismatchError(instr, instr.location.name) + context.mark_as_called(instr.location) if isinstance(type, VectorType): type = type.of_type for ref in type.inputs: diff --git a/src/sixtypical/context.py b/src/sixtypical/context.py index a92c513..c66f204 100644 --- a/src/sixtypical/context.py +++ b/src/sixtypical/context.py @@ -35,6 +35,7 @@ class AnalysisContext(object): self._terminated = False self._gotos_encountered = set() self._pointer_assoc = dict() + self.called_routines = set() for ref in inputs: if self.is_constant(ref): @@ -79,6 +80,7 @@ class AnalysisContext(object): c._writeable = set(self._writeable) c._pointer_assoc = dict(self._pointer_assoc) c._gotos_encountered = set(self._gotos_encountered) + c.called_routines = set(self.called_routines) return c def update_from(self, other): @@ -94,6 +96,7 @@ class AnalysisContext(object): self._writeable = set(other._writeable) self._terminated = other._terminated self._pointer_assoc = dict(other._pointer_assoc) + self.called_routines = set(other.called_routines) def each_meaningful(self): for ref in self._range.keys(): @@ -326,3 +329,6 @@ class AnalysisContext(object): return self.symtab.fetch_local_type(self.routine.name, ref.name).max_range else: return self.symtab.fetch_global_type(ref.name).max_range + + def mark_as_called(self, routine): + self.called_routines.add(routine) From a966a496d0f80c246aae46402760e6eb0618b623 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 21 Oct 2019 14:07:41 +0100 Subject: [PATCH 05/36] Bump version number. --- README.md | 2 +- bin/sixtypical | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ab177fc..a396a6d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ SixtyPical ========== -_Version 0.20. Work-in-progress, everything is subject to change._ +_Version 0.21. Work-in-progress, everything is subject to change._ **SixtyPical** is a [low-level](#low-level) programming language supporting a sophisticated [static analysis](#static-analysis). diff --git a/bin/sixtypical b/bin/sixtypical index c847389..3eade0d 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -185,7 +185,7 @@ if __name__ == '__main__': argparser.add_argument( "--version", action="version", - version="%(prog)s 0.20" + version="%(prog)s 0.21" ) options, unknown = argparser.parse_known_args(sys.argv[1:]) From 894fb1a0f2191f9edf9cb1af48b1c71b9a79e801 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 21 Oct 2019 14:43:21 +0100 Subject: [PATCH 06/36] Refine the callgraph algorithm. Still incomplete though. --- bin/sixtypical | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index 41410a6..e0492e4 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -51,11 +51,28 @@ def process_input_files(filenames, options): sys.stdout.write("\n") if options.dump_callgraph: + graph = {} + for routine in program.routines: - sys.stdout.write('-----------\n') - sys.stdout.write('{}\n'.format(routine.name)) + potentially_calls = [] for called_routine in routine.called_routines: - sys.stdout.write(' {}\n'.format(called_routine.name)) + # TODO if called_routine is a vector, this should be all routines which can be assigned to this vector + potentially_calls.append(called_routine.name) + graph[routine.name] = { + 'potentially-calls': potentially_calls, + } + + # Reflexive closure + + for routine in program.routines: + potentially_called_by = [] + for (name, node) in graph.items(): + potential_calls = node['potentially-calls'] + if routine.name in potential_calls: + potentially_called_by.append(name) + graph[routine.name]['potentially-called-by'] = potentially_called_by + + sys.stdout.write(json.dumps(graph, indent=4, sort_keys=True, separators=(',', ':'))) compilation_roster = None if options.optimize_fallthru: From bcc256aa5d95e18a39b7acdec33a9b45a658f293 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 21 Oct 2019 15:07:54 +0100 Subject: [PATCH 07/36] Checkpoint. --- bin/sixtypical | 16 +++++++++++++--- src/sixtypical/analyzer.py | 2 +- src/sixtypical/context.py | 4 ++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index e0492e4..7f98e03 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -53,11 +53,21 @@ def process_input_files(filenames, options): if options.dump_callgraph: graph = {} + from sixtypical.model import RoutineType, VectorType + + def find_routines_matching_vector_type(program, type_): + return [] # dummy + for routine in program.routines: potentially_calls = [] - for called_routine in routine.called_routines: - # TODO if called_routine is a vector, this should be all routines which can be assigned to this vector - potentially_calls.append(called_routine.name) + for (called_routine, called_routine_type) in routine.called_routines: + if isinstance(called_routine_type, RoutineType): + potentially_calls.append(called_routine.name) + elif isinstance(called_routine_type, VectorType): + for potentially_called in find_routines_matching_vector_type(program, called_routine_type): + potentially_calls.append(potentially_called.name) + else: + raise NotImplementedError graph[routine.name] = { 'potentially-calls': potentially_calls, } diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 9e1b840..e6aa6df 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -516,7 +516,7 @@ class Analyzer(object): type = self.get_type(instr.location) if not isinstance(type, (RoutineType, VectorType)): raise TypeMismatchError(instr, instr.location.name) - context.mark_as_called(instr.location) + context.mark_as_called(instr.location, type) if isinstance(type, VectorType): type = type.of_type for ref in type.inputs: diff --git a/src/sixtypical/context.py b/src/sixtypical/context.py index c66f204..117c02f 100644 --- a/src/sixtypical/context.py +++ b/src/sixtypical/context.py @@ -330,5 +330,5 @@ class AnalysisContext(object): else: return self.symtab.fetch_global_type(ref.name).max_range - def mark_as_called(self, routine): - self.called_routines.add(routine) + def mark_as_called(self, location, type_): + self.called_routines.add((location, type_)) From 7187fa6285149e3b286443737ef5b2efda5b4893 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 21 Oct 2019 15:41:17 +0100 Subject: [PATCH 08/36] Refactor: move to dedicated module. --- bin/sixtypical | 33 ++------------------------------- src/sixtypical/callgraph.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 src/sixtypical/callgraph.py diff --git a/bin/sixtypical b/bin/sixtypical index 7f98e03..f16ffac 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -51,37 +51,8 @@ def process_input_files(filenames, options): sys.stdout.write("\n") if options.dump_callgraph: - graph = {} - - from sixtypical.model import RoutineType, VectorType - - def find_routines_matching_vector_type(program, type_): - return [] # dummy - - for routine in program.routines: - potentially_calls = [] - for (called_routine, called_routine_type) in routine.called_routines: - if isinstance(called_routine_type, RoutineType): - potentially_calls.append(called_routine.name) - elif isinstance(called_routine_type, VectorType): - for potentially_called in find_routines_matching_vector_type(program, called_routine_type): - potentially_calls.append(potentially_called.name) - else: - raise NotImplementedError - graph[routine.name] = { - 'potentially-calls': potentially_calls, - } - - # Reflexive closure - - for routine in program.routines: - potentially_called_by = [] - for (name, node) in graph.items(): - potential_calls = node['potentially-calls'] - if routine.name in potential_calls: - potentially_called_by.append(name) - graph[routine.name]['potentially-called-by'] = potentially_called_by - + from sixtypical.callgraph import construct_callgraph + graph = construct_callgraph(program) sys.stdout.write(json.dumps(graph, indent=4, sort_keys=True, separators=(',', ':'))) compilation_roster = None diff --git a/src/sixtypical/callgraph.py b/src/sixtypical/callgraph.py new file mode 100644 index 0000000..d7a9773 --- /dev/null +++ b/src/sixtypical/callgraph.py @@ -0,0 +1,35 @@ +from sixtypical.model import RoutineType, VectorType + + +def find_routines_matching_vector_type(program, type_): + return [] # dummy + + +def construct_callgraph(program): + graph = {} + + for routine in program.routines: + potentially_calls = [] + for (called_routine, called_routine_type) in routine.called_routines: + if isinstance(called_routine_type, RoutineType): + potentially_calls.append(called_routine.name) + elif isinstance(called_routine_type, VectorType): + for potentially_called in find_routines_matching_vector_type(program, called_routine_type): + potentially_calls.append(potentially_called.name) + else: + raise NotImplementedError + graph[routine.name] = { + 'potentially-calls': potentially_calls, + } + + # Reflexive closure + + for routine in program.routines: + potentially_called_by = [] + for (name, node) in graph.items(): + potential_calls = node['potentially-calls'] + if routine.name in potential_calls: + potentially_called_by.append(name) + graph[routine.name]['potentially-called-by'] = potentially_called_by + + return graph From 182935a088440e1528450d6038f2f2702ec916df Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 21 Oct 2019 16:12:06 +0100 Subject: [PATCH 09/36] First cut at finding all routines that could be assigned to vector. --- src/sixtypical/analyzer.py | 12 ++++++++---- src/sixtypical/callgraph.py | 9 ++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index e6aa6df..39fc5f7 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -141,18 +141,22 @@ class Analyzer(object): def analyze_program(self, program): assert isinstance(program, Program) for routine in program.routines: - context = self.analyze_routine(routine) + context, type_ = self.analyze_routine(routine) + if type_: + routine.routine_type = type_ routine.encountered_gotos = list(context.encountered_gotos()) if context else [] routine.called_routines = list(context.called_routines) if context else [] def analyze_routine(self, routine): assert isinstance(routine, Routine) + type_ = self.get_type_for_name(routine.name) + if routine.block is None: # it's an extern, that's fine - return None + return None, type_ self.current_routine = routine - type_ = self.get_type_for_name(routine.name) + context = AnalysisContext(self.symtab, routine, type_.inputs, type_.outputs, type_.trashes) # register any local statics as already-initialized @@ -210,7 +214,7 @@ class Analyzer(object): self.exit_contexts = None self.current_routine = None - return context + return context, type_ def analyze_block(self, block, context): assert isinstance(block, Block) diff --git a/src/sixtypical/callgraph.py b/src/sixtypical/callgraph.py index d7a9773..44990d2 100644 --- a/src/sixtypical/callgraph.py +++ b/src/sixtypical/callgraph.py @@ -1,8 +1,11 @@ from sixtypical.model import RoutineType, VectorType -def find_routines_matching_vector_type(program, type_): - return [] # dummy +def find_routines_matching_type(program, type_): + """Return the subset of routines of the program whose value + may be assigned to a location of the given type_ (typically + a vector).""" + return [r for r in program.routines if RoutineType.executable_types_compatible(r.routine_type, type_)] def construct_callgraph(program): @@ -14,7 +17,7 @@ def construct_callgraph(program): if isinstance(called_routine_type, RoutineType): potentially_calls.append(called_routine.name) elif isinstance(called_routine_type, VectorType): - for potentially_called in find_routines_matching_vector_type(program, called_routine_type): + for potentially_called in find_routines_matching_type(program, called_routine_type): potentially_calls.append(potentially_called.name) else: raise NotImplementedError From b09d0c0b7684abd4efd675a4ee0cb6baa8f780a5 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 21 Oct 2019 17:15:28 +0100 Subject: [PATCH 10/36] Routines that are goto'd are also in the call graph. --- src/sixtypical/analyzer.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 39fc5f7..aee46f5 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -517,17 +517,17 @@ class Analyzer(object): raise NotImplementedError(opcode) def analyze_call(self, instr, context): - type = self.get_type(instr.location) - if not isinstance(type, (RoutineType, VectorType)): + type_ = self.get_type(instr.location) + if not isinstance(type_, (RoutineType, VectorType)): raise TypeMismatchError(instr, instr.location.name) - context.mark_as_called(instr.location, type) - if isinstance(type, VectorType): - type = type.of_type - for ref in type.inputs: + context.mark_as_called(instr.location, type_) + if isinstance(type_, VectorType): + type_ = type_.of_type + for ref in type_.inputs: context.assert_meaningful(ref) - for ref in type.outputs: + for ref in type_.outputs: context.set_written(ref) - for ref in type.trashes: + for ref in type_.trashes: context.assert_writeable(ref) context.set_touched(ref) context.set_unmeaningful(ref) @@ -538,6 +538,7 @@ class Analyzer(object): if not isinstance(type_, (RoutineType, VectorType)): raise TypeMismatchError(instr, location.name) + context.mark_as_called(instr.location, type_) # assert that the dest routine's inputs are all initialized if isinstance(type_, VectorType): From 684256f7e99185193f031f6e7c9c7c93e163b301 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 21 Oct 2019 17:29:38 +0100 Subject: [PATCH 11/36] Start of tests for this. --- test.sh | 1 + tests/SixtyPical Callgraph.md | 22 ++++++++++++++++++++++ tests/appliances/sixtypical.md | 3 +++ 3 files changed, 26 insertions(+) create mode 100644 tests/SixtyPical Callgraph.md diff --git a/test.sh b/test.sh index a19ec97..9967a0b 100755 --- a/test.sh +++ b/test.sh @@ -10,4 +10,5 @@ falderal --substring-error \ "tests/SixtyPical Storage.md" \ "tests/SixtyPical Control Flow.md" \ "tests/SixtyPical Fallthru.md" \ + "tests/SixtyPical Callgraph.md" \ "tests/SixtyPical Compilation.md" diff --git a/tests/SixtyPical Callgraph.md b/tests/SixtyPical Callgraph.md new file mode 100644 index 0000000..547e590 --- /dev/null +++ b/tests/SixtyPical Callgraph.md @@ -0,0 +1,22 @@ +SixtyPical Callgraph +==================== + +This is a test suite, written in [Falderal][] format, for the ability of +a SixtyPical analyzer to construct a callgraph of which routines call which +other routines, and its ability to discover which routines will never be +called. + +[Falderal]: http://catseye.tc/node/Falderal + + -> Tests for functionality "Dump callgraph info for SixtyPical program" + +The `main` routine is always called. + + | define main routine + | { + | } + = [ + = [ + = "main" + = ] + = ] diff --git a/tests/appliances/sixtypical.md b/tests/appliances/sixtypical.md index 1ee2bc4..425b970 100644 --- a/tests/appliances/sixtypical.md +++ b/tests/appliances/sixtypical.md @@ -16,5 +16,8 @@ implementation, `sixtypical`, that is going to implement these functionalities. -> 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 "Dump callgraph info for SixtyPical program" is implemented by + -> shell command "bin/sixtypical --dump-callgraph --analyze-only --traceback %(test-body-file)" + -> Functionality "Compile SixtyPical program with fallthru optimization" is implemented by -> shell command "bin/sixtypical --output-format=c64-basic-prg --optimize-fallthru --traceback %(test-body-file) --output /tmp/foo && tests/appliances/bin/dcc6502-adapter Date: Mon, 21 Oct 2019 21:23:14 +0100 Subject: [PATCH 12/36] Fix test, dump format. --- bin/sixtypical | 4 ++-- tests/SixtyPical Callgraph.md | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index f16ffac..fbe7f71 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -53,7 +53,7 @@ def process_input_files(filenames, options): if options.dump_callgraph: from sixtypical.callgraph import construct_callgraph graph = construct_callgraph(program) - sys.stdout.write(json.dumps(graph, indent=4, sort_keys=True, separators=(',', ':'))) + sys.stdout.write(json.dumps(graph, indent=4, sort_keys=True, separators=(',', ': '))) compilation_roster = None if options.optimize_fallthru: @@ -64,7 +64,7 @@ def process_input_files(filenames, options): return if label: sys.stdout.write("*** {}:\n".format(label)) - sys.stdout.write(json.dumps(data, indent=4, sort_keys=True, separators=(',', ':'))) + sys.stdout.write(json.dumps(data, indent=4, sort_keys=True, separators=(',', ': '))) sys.stdout.write("\n") fa = FallthruAnalyzer(symtab, debug=options.debug) diff --git a/tests/SixtyPical Callgraph.md b/tests/SixtyPical Callgraph.md index 547e590..5caf5f8 100644 --- a/tests/SixtyPical Callgraph.md +++ b/tests/SixtyPical Callgraph.md @@ -15,8 +15,9 @@ The `main` routine is always called. | define main routine | { | } - = [ - = [ - = "main" - = ] - = ] + = { + = "main": { + = "potentially-called-by": [], + = "potentially-calls": [] + = } + = } From a84cd4de8c31165122acd9f06f25eb12fb459cb7 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 21 Oct 2019 21:35:28 +0100 Subject: [PATCH 13/36] Expand on the callgraph tests. --- src/sixtypical/callgraph.py | 2 + tests/SixtyPical Callgraph.md | 100 +++++++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/src/sixtypical/callgraph.py b/src/sixtypical/callgraph.py index 44990d2..b3a87db 100644 --- a/src/sixtypical/callgraph.py +++ b/src/sixtypical/callgraph.py @@ -33,6 +33,8 @@ def construct_callgraph(program): potential_calls = node['potentially-calls'] if routine.name in potential_calls: potentially_called_by.append(name) + if getattr(routine, 'explicitly_marked_as_called', None) or routine.name == 'main': + potentially_called_by.append('*marked*') graph[routine.name]['potentially-called-by'] = potentially_called_by return graph diff --git a/tests/SixtyPical Callgraph.md b/tests/SixtyPical Callgraph.md index 5caf5f8..aeee12b 100644 --- a/tests/SixtyPical Callgraph.md +++ b/tests/SixtyPical Callgraph.md @@ -10,14 +10,112 @@ called. -> Tests for functionality "Dump callgraph info for SixtyPical program" -The `main` routine is always called. +The `main` routine is always called. The thing that it will +be called by is the system, but the callgraph analyzer will +simply consider it to be "marked as called". | define main routine | { | } = { = "main": { + = "potentially-called-by": [ + = "*marked*" + = ], + = "potentially-calls": [] + = } + = } + +If a routine is called by another routine, this fact will be noted. + + | define main routine + | { + | call other + | } + | + | define other routine + | { + | } + = { + = "main": { + = "potentially-called-by": [ + = "*marked*" + = ], + = "potentially-calls": [ + = "other" + = ] + = }, + = "other": { + = "potentially-called-by": [ + = "main" + = ], + = "potentially-calls": [] + = } + = } + +If a routine is not called by another routine, and it is not `main` +and it is not explicitly marked as preserved, this absence will be +noted, and a compiler or linker will be permitted to omit it from +the final executable. + + | define main routine + | { + | } + | + | define other routine + | { + | } + = { + = "main": { + = "potentially-called-by": [ + = "*marked*" + = ], + = "potentially-calls": [] + = }, + = "other": { = "potentially-called-by": [], = "potentially-calls": [] = } = } + +If two routines potentially call each other, this will be noted, +even if nothing else potentially calls either of those routines. +This may change in the future. + + | define main routine + | { + | } + | + | define other1 routine + | { + | call other2 + | } + | + | define other2 routine + | { + | call other1 + | } + = { + = "main": { + = "potentially-called-by": [ + = "*marked*" + = ], + = "potentially-calls": [] + = }, + = "other1": { + = "potentially-called-by": [ + = "other2" + = ], + = "potentially-calls": [ + = "other2" + = ] + = }, + = "other2": { + = "potentially-called-by": [ + = "other1" + = ], + = "potentially-calls": [ + = "other1" + = ] + = } + = } From 2192a48e0ed81c1d00445c1d154ac6fa86a44646 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 21 Oct 2019 21:37:30 +0100 Subject: [PATCH 14/36] Update HISTORY. --- HISTORY.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 36bf490..a7d1486 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,16 @@ History of SixtyPical ===================== +0.21 +---- + +* The reference implementation constructs a callgraph and + approximates the set of routines which are not called + by any other routine, with an eye to omitting them from + the final executable. +* The `dcc6502-adapter` test adapter was updated to conform + to the output of the latest version of `dcc6502`. + 0.20 ---- From 1098347fa5efc757aafc22513f1af67fba759d18 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Mon, 21 Oct 2019 21:45:59 +0100 Subject: [PATCH 15/36] Routines can be declared `preserved`. --- HISTORY.md | 3 +++ src/sixtypical/callgraph.py | 4 ++-- src/sixtypical/parser.py | 4 ++++ tests/SixtyPical Callgraph.md | 39 ++++++++++++++++++++++++++++++----- tests/SixtyPical Syntax.md | 12 +++++++++++ 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index a7d1486..d5ca93c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -8,6 +8,9 @@ History of SixtyPical approximates the set of routines which are not called by any other routine, with an eye to omitting them from the final executable. +* A routine can be declared `preserved`, which prevents a + compiler from omitting it from the final executable, even + if it determines it is not called by any other routine. * The `dcc6502-adapter` test adapter was updated to conform to the output of the latest version of `dcc6502`. diff --git a/src/sixtypical/callgraph.py b/src/sixtypical/callgraph.py index b3a87db..bf8b39d 100644 --- a/src/sixtypical/callgraph.py +++ b/src/sixtypical/callgraph.py @@ -33,8 +33,8 @@ def construct_callgraph(program): potential_calls = node['potentially-calls'] if routine.name in potential_calls: potentially_called_by.append(name) - if getattr(routine, 'explicitly_marked_as_called', None) or routine.name == 'main': - potentially_called_by.append('*marked*') + if getattr(routine, 'preserved', None) or routine.name == 'main': + potentially_called_by.append('*preserved*') graph[routine.name]['potentially-called-by'] = potentially_called_by return graph diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 0cf7107..64b6631 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -111,8 +111,12 @@ class Parser(object): name = self.scanner.token self.scanner.scan() self.current_routine_name = name + preserved = False + if self.scanner.consume('preserved'): + preserved = True type_, routine = self.routine(name) self.declare(name, routine, type_) + routine.preserved = preserved routines.append(routine) self.current_routine_name = None self.scanner.check_type('EOF') diff --git a/tests/SixtyPical Callgraph.md b/tests/SixtyPical Callgraph.md index aeee12b..62d5ff6 100644 --- a/tests/SixtyPical Callgraph.md +++ b/tests/SixtyPical Callgraph.md @@ -12,7 +12,7 @@ called. The `main` routine is always called. The thing that it will be called by is the system, but the callgraph analyzer will -simply consider it to be "marked as called". +simply consider it to be "marked as preserved". | define main routine | { @@ -20,7 +20,7 @@ simply consider it to be "marked as called". = { = "main": { = "potentially-called-by": [ - = "*marked*" + = "*preserved*" = ], = "potentially-calls": [] = } @@ -39,7 +39,7 @@ If a routine is called by another routine, this fact will be noted. = { = "main": { = "potentially-called-by": [ - = "*marked*" + = "*preserved*" = ], = "potentially-calls": [ = "other" @@ -68,7 +68,7 @@ the final executable. = { = "main": { = "potentially-called-by": [ - = "*marked*" + = "*preserved*" = ], = "potentially-calls": [] = }, @@ -78,6 +78,35 @@ the final executable. = } = } +If a routine is not called by another routine, but it is declared +explicitly as `preserved`, then it will not be considered unused, +and a compiler or linker will not be permitted to omit it from +the final executable. This is useful for interrupt routines and +such that really are used by some part of the system, even if not +directly by another SixtyPical routine. + + | define main routine + | { + | } + | + | define other preserved routine + | { + | } + = { + = "main": { + = "potentially-called-by": [ + = "*preserved*" + = ], + = "potentially-calls": [] + = }, + = "other": { + = "potentially-called-by": [ + = "*preserved*" + = ], + = "potentially-calls": [] + = } + = } + If two routines potentially call each other, this will be noted, even if nothing else potentially calls either of those routines. This may change in the future. @@ -98,7 +127,7 @@ This may change in the future. = { = "main": { = "potentially-called-by": [ - = "*marked*" + = "*preserved*" = ], = "potentially-calls": [] = }, diff --git a/tests/SixtyPical Syntax.md b/tests/SixtyPical Syntax.md index f830394..f164a73 100644 --- a/tests/SixtyPical Syntax.md +++ b/tests/SixtyPical Syntax.md @@ -73,6 +73,18 @@ Extern routines | @ 65487 = ok +Preserved routine. + + | define main routine { + | ld a, $ff + | add a, $01 + | } + | define foo preserved routine { + | ld a, 0 + | add a, 1 + | } + = ok + Trash. | define main routine { From 1df6941b01413354b7286c302537d7b4badd11d5 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 09:07:16 +0100 Subject: [PATCH 16/36] Callgraph uses reachability. --- HISTORY.md | 6 +- TODO.md | 3 - src/sixtypical/callgraph.py | 24 +++++++- tests/SixtyPical Callgraph.md | 113 ++++++++++++++++++++++------------ 4 files changed, 99 insertions(+), 47 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index d5ca93c..318419c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,9 +5,9 @@ History of SixtyPical ---- * The reference implementation constructs a callgraph and - approximates the set of routines which are not called - by any other routine, with an eye to omitting them from - the final executable. + determines the set of routines which are not reachable + (directly or indirectly) from `main`, with an eye to + omitting them from the final executable. * A routine can be declared `preserved`, which prevents a compiler from omitting it from the final executable, even if it determines it is not called by any other routine. diff --git a/TODO.md b/TODO.md index 1761bde..036555c 100644 --- a/TODO.md +++ b/TODO.md @@ -102,9 +102,6 @@ Once we have a call graph we can omit routines that we're sure aren't called. This would let us use include-files and standard-libraries nicely: any routines they define, but that you don't use, don't get included. -Analyzing the set of possible routines that a vector can take on would help -this immensely. - Implementation -------------- diff --git a/src/sixtypical/callgraph.py b/src/sixtypical/callgraph.py index bf8b39d..b91e312 100644 --- a/src/sixtypical/callgraph.py +++ b/src/sixtypical/callgraph.py @@ -8,6 +8,15 @@ def find_routines_matching_type(program, type_): return [r for r in program.routines if RoutineType.executable_types_compatible(r.routine_type, type_)] +def mark_as_reachable(graph, routine_name): + node = graph[routine_name] + if node.get('reachable', False): + return + node['reachable'] = True + for next_routine_name in node['potentially-calls']: + mark_as_reachable(graph, next_routine_name) + + def construct_callgraph(program): graph = {} @@ -33,8 +42,19 @@ def construct_callgraph(program): potential_calls = node['potentially-calls'] if routine.name in potential_calls: potentially_called_by.append(name) - if getattr(routine, 'preserved', None) or routine.name == 'main': - potentially_called_by.append('*preserved*') graph[routine.name]['potentially-called-by'] = potentially_called_by + # Root set + + root_set = set() + + for routine in program.routines: + if getattr(routine, 'preserved', False) or routine.name == 'main': + root_set.add(routine) + + # Reachability + + for routine in root_set: + mark_as_reachable(graph, routine.name) + return graph diff --git a/tests/SixtyPical Callgraph.md b/tests/SixtyPical Callgraph.md index 62d5ff6..b703511 100644 --- a/tests/SixtyPical Callgraph.md +++ b/tests/SixtyPical Callgraph.md @@ -11,22 +11,23 @@ called. -> Tests for functionality "Dump callgraph info for SixtyPical program" The `main` routine is always called. The thing that it will -be called by is the system, but the callgraph analyzer will -simply consider it to be "marked as preserved". +be called by is the system, but the callgraph analyzer simply +considers it to be "reachable". | define main routine | { | } = { = "main": { - = "potentially-called-by": [ - = "*preserved*" - = ], - = "potentially-calls": [] + = "potentially-called-by": [], + = "potentially-calls": [], + = "reachable": true = } = } If a routine is called by another routine, this fact will be noted. +If it is reachable (directly or indirectly) from `main`, this will +be noted as well. | define main routine | { @@ -38,25 +39,25 @@ If a routine is called by another routine, this fact will be noted. | } = { = "main": { - = "potentially-called-by": [ - = "*preserved*" - = ], + = "potentially-called-by": [], = "potentially-calls": [ = "other" - = ] + = ], + = "reachable": true = }, = "other": { = "potentially-called-by": [ = "main" = ], - = "potentially-calls": [] + = "potentially-calls": [], + = "reachable": true = } = } -If a routine is not called by another routine, and it is not `main` -and it is not explicitly marked as preserved, this absence will be -noted, and a compiler or linker will be permitted to omit it from -the final executable. +If a routine is not potentially called by any other routine that is +ultimately potentially called by `main`, this absence will be noted +— the routine will not be considered reachable — and a compiler or +linker will be permitted to omit it from the final executable. | define main routine | { @@ -67,10 +68,9 @@ the final executable. | } = { = "main": { - = "potentially-called-by": [ - = "*preserved*" - = ], - = "potentially-calls": [] + = "potentially-called-by": [], + = "potentially-calls": [], + = "reachable": true = }, = "other": { = "potentially-called-by": [], @@ -79,11 +79,11 @@ the final executable. = } If a routine is not called by another routine, but it is declared -explicitly as `preserved`, then it will not be considered unused, -and a compiler or linker will not be permitted to omit it from -the final executable. This is useful for interrupt routines and -such that really are used by some part of the system, even if not -directly by another SixtyPical routine. +explicitly as `preserved`, then it will still be considered +reachable, and a compiler or linker will not be permitted to omit it +from the final executable. This is useful for interrupt routines +and such that really are used by some part of the system, even if +not directly by another SixtyPical routine. | define main routine | { @@ -94,22 +94,58 @@ directly by another SixtyPical routine. | } = { = "main": { - = "potentially-called-by": [ - = "*preserved*" - = ], - = "potentially-calls": [] + = "potentially-called-by": [], + = "potentially-calls": [], + = "reachable": true = }, = "other": { - = "potentially-called-by": [ - = "*preserved*" - = ], - = "potentially-calls": [] + = "potentially-called-by": [], + = "potentially-calls": [], + = "reachable": true = } = } -If two routines potentially call each other, this will be noted, -even if nothing else potentially calls either of those routines. -This may change in the future. +If a routine is called from a preserved routine, that routine is +reachable. + + | define main routine + | { + | } + | + | define other1 preserved routine + | { + | call other2 + | } + | + | define other2 preserved routine + | { + | } + = { + = "main": { + = "potentially-called-by": [], + = "potentially-calls": [], + = "reachable": true + = }, + = "other1": { + = "potentially-called-by": [], + = "potentially-calls": [ + = "other2" + = ], + = "reachable": true + = }, + = "other2": { + = "potentially-called-by": [ + = "other1" + = ], + = "potentially-calls": [], + = "reachable": true + = } + = } + +If a group of routines potentially call each other, but neither is +found to be reachable (directly or indirectly) from `main` or a +`preserved` routine, the routines in the group will not be considered +reachable. | define main routine | { @@ -126,10 +162,9 @@ This may change in the future. | } = { = "main": { - = "potentially-called-by": [ - = "*preserved*" - = ], - = "potentially-calls": [] + = "potentially-called-by": [], + = "potentially-calls": [], + = "reachable": true = }, = "other1": { = "potentially-called-by": [ From 3d88226058c1b6cec6a76940b4ce917658161f44 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 09:41:30 +0100 Subject: [PATCH 17/36] Implement --prune-unreachable-routines. --- HISTORY.md | 3 +++ TODO.md | 12 +++++------ bin/sixtypical | 16 ++++++++++---- src/sixtypical/callgraph.py | 7 +++++++ tests/SixtyPical Callgraph.md | 38 ++++++++++++++++++++++++++++++++++ tests/appliances/sixtypical.md | 9 +++++--- 6 files changed, 71 insertions(+), 14 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 318419c..75e8d68 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,6 +11,9 @@ History of SixtyPical * A routine can be declared `preserved`, which prevents a compiler from omitting it from the final executable, even if it determines it is not called by any other routine. +* Added `--prune-unreachable-routines` option, which causes + the compiler to in fact omit routines determined to be + unreachable as described above. * The `dcc6502-adapter` test adapter was updated to conform to the output of the latest version of `dcc6502`. diff --git a/TODO.md b/TODO.md index 036555c..95967d4 100644 --- a/TODO.md +++ b/TODO.md @@ -95,13 +95,6 @@ As long as the routine has consistent type context every place it exits, that sh Currently the `if` generator is not smart enough to avoid generating silly jump instructions. (See the Fallthru tests.) Improve it. -### Dead code removal - -Once we have a call graph we can omit routines that we're sure aren't called. - -This would let us use include-files and standard-libraries nicely: any -routines they define, but that you don't use, don't get included. - Implementation -------------- @@ -110,6 +103,11 @@ Implementation For analysis errors, there is a line number, but it's the line of the routine after the routine in which the analysis error occurred. Fix this. +### Libraries + +Now that we have dead-code removal, establish some libraries of reusable +routines. + Blue-skying ----------- diff --git a/bin/sixtypical b/bin/sixtypical index fbe7f71..afa4f02 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -19,6 +19,7 @@ import traceback from sixtypical.symtab import SymbolTable from sixtypical.parser import Parser, merge_programs from sixtypical.analyzer import Analyzer +from sixtypical.callgraph import construct_callgraph, prune_unreachable_routines from sixtypical.outputter import outputter_class_for from sixtypical.compiler import Compiler @@ -47,13 +48,14 @@ def process_input_files(filenames, options): analyzer.analyze_program(program) finally: if options.dump_exit_contexts: - sys.stdout.write(json.dumps(analyzer.exit_contexts_map, indent=4, sort_keys=True, separators=(',', ':'))) + sys.stdout.write(json.dumps(analyzer.exit_contexts_map, indent=4, sort_keys=True, separators=(',', ': '))) sys.stdout.write("\n") + callgraph = construct_callgraph(program) if options.dump_callgraph: - from sixtypical.callgraph import construct_callgraph - graph = construct_callgraph(program) - sys.stdout.write(json.dumps(graph, indent=4, sort_keys=True, separators=(',', ': '))) + sys.stdout.write(json.dumps(callgraph, indent=4, sort_keys=True, separators=(',', ': '))) + if options.prune_unreachable_routines: + program = prune_unreachable_routines(program, callgraph) compilation_roster = None if options.optimize_fallthru: @@ -166,6 +168,12 @@ if __name__ == '__main__': action="store_true", help="Dump the ordered fallthru map, in JSON, to stdout after analyzing the program." ) + argparser.add_argument( + "--prune-unreachable-routines", + action="store_true", + help="Omit code for unreachable routines (as determined by the callgraph) " + "from the final output." + ) argparser.add_argument( "--dump-callgraph", action="store_true", diff --git a/src/sixtypical/callgraph.py b/src/sixtypical/callgraph.py index b91e312..9260209 100644 --- a/src/sixtypical/callgraph.py +++ b/src/sixtypical/callgraph.py @@ -1,3 +1,4 @@ +from sixtypical.ast import Program from sixtypical.model import RoutineType, VectorType @@ -58,3 +59,9 @@ def construct_callgraph(program): mark_as_reachable(graph, routine.name) return graph + + +def prune_unreachable_routines(program, callgraph): + return Program(1, defns=program.defns, routines=[ + r for r in program.routines if callgraph[r.name].get('reachable', False) + ]) diff --git a/tests/SixtyPical Callgraph.md b/tests/SixtyPical Callgraph.md index b703511..6aa91b9 100644 --- a/tests/SixtyPical Callgraph.md +++ b/tests/SixtyPical Callgraph.md @@ -183,3 +183,41 @@ reachable. = ] = } = } + + -> Tests for functionality "Compile SixtyPical program with unreachable routine removal" + +Basic test for actually removing unreachable routines from the resulting +executable when compiling SixtyPical programs. + + | define main routine outputs a trashes z, n + | { + | ld a, 100 + | } + | + | define other1 routine + | { + | call other2 + | } + | + | define other2 routine + | { + | call other1 + | } + = $080D LDA #$64 + = $080F RTS + +Test that marking routine as `preserved` preserves it in the output. + + | define main routine outputs a trashes z, n + | { + | ld a, 100 + | } + | + | define other preserved routine outputs a trashes z, n + | { + | ld a, 5 + | } + = $080D LDA #$64 + = $080F RTS + = $0810 LDA #$05 + = $0812 RTS diff --git a/tests/appliances/sixtypical.md b/tests/appliances/sixtypical.md index 425b970..34bbf8d 100644 --- a/tests/appliances/sixtypical.md +++ b/tests/appliances/sixtypical.md @@ -13,11 +13,14 @@ implementation, `sixtypical`, that is going to implement these functionalities. -> Functionality "Compile SixtyPical program" is implemented by -> shell command "bin/sixtypical --output-format=c64-basic-prg --traceback %(test-body-file) --output /tmp/foo && tests/appliances/bin/dcc6502-adapter Functionality "Dump fallthru info for SixtyPical program" is implemented by - -> shell command "bin/sixtypical --optimize-fallthru --dump-fallthru-info --analyze-only --traceback %(test-body-file)" - -> Functionality "Dump callgraph info for SixtyPical program" is implemented by -> shell command "bin/sixtypical --dump-callgraph --analyze-only --traceback %(test-body-file)" + -> Functionality "Compile SixtyPical program with unreachable routine removal" is implemented by + -> shell command "bin/sixtypical --output-format=c64-basic-prg --prune-unreachable-routines --traceback %(test-body-file) --output /tmp/foo && tests/appliances/bin/dcc6502-adapter Functionality "Dump fallthru info for SixtyPical program" is implemented by + -> shell command "bin/sixtypical --optimize-fallthru --dump-fallthru-info --analyze-only --traceback %(test-body-file)" + -> Functionality "Compile SixtyPical program with fallthru optimization" is implemented by -> shell command "bin/sixtypical --output-format=c64-basic-prg --optimize-fallthru --traceback %(test-body-file) --output /tmp/foo && tests/appliances/bin/dcc6502-adapter Date: Tue, 22 Oct 2019 10:18:56 +0100 Subject: [PATCH 18/36] Make test as intended. It fails atm. Simplify driver code. --- bin/sixtypical | 11 ++--------- tests/SixtyPical Fallthru.md | 18 ++++++------------ 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index afa4f02..c22f0b0 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -61,18 +61,11 @@ def process_input_files(filenames, options): if options.optimize_fallthru: from sixtypical.fallthru import FallthruAnalyzer - def dump(data, label=None): - 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, separators=(',', ': '))) - sys.stdout.write("\n") - fa = FallthruAnalyzer(symtab, debug=options.debug) fa.analyze_program(program) compilation_roster = fa.serialize() - dump(compilation_roster) + if options.dump_fallthru_info: + sys.stdout.write(json.dumps(compilation_roster, indent=4, sort_keys=True, separators=(',', ': '))) if options.analyze_only or (options.output is None and not options.run_on): return diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md index 7f95573..22c5376 100644 --- a/tests/SixtyPical Fallthru.md +++ b/tests/SixtyPical Fallthru.md @@ -384,11 +384,6 @@ It can optimize out one of the `goto`s if they are the same. 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 @@ -411,11 +406,10 @@ in the "true" branch is a `goto`. | } = $080D RTS = $080E LDA #$00 - = $0810 BNE $081A + = $0810 BNE $0817 = $0812 LDA #$01 - = $0814 JMP $081F - = $0817 JMP $081F - = $081A LDA #$02 - = $081C JMP $080D - = $081F LDA #$FF - = $0821 RTS + = $0814 JMP $081C + = $0817 LDA #$02 + = $0819 JMP $080D + = $081C LDA #$FF + = $081E RTS From b9df1482c62bf67ae7388b589077a343700058e2 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 10:32:14 +0100 Subject: [PATCH 19/36] Pass next routine to each routine being compiled. --- src/sixtypical/compiler.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index f9fac26..4cc4dd3 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -122,10 +122,11 @@ class Compiler(object): 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], skip_final_goto=True) - routine_name = roster_row[-1] - self.compile_routine(self.routines[routine_name]) + for i, routine_name in enumerate(roster_row): + if i < len(roster_row) - 1: + self.compile_routine(self.routines[routine_name], next_routine=self.routines[roster_row[i + 1]]) + else: + self.compile_routine(self.routines[routine_name]) for location, label in self.trampolines.items(): self.emitter.resolve_label(label) @@ -155,9 +156,9 @@ class Compiler(object): if defn.initial is None and defn.addr is None: self.emitter.resolve_bss_label(label) - def compile_routine(self, routine, skip_final_goto=False): + def compile_routine(self, routine, next_routine=None): self.current_routine = routine - self.skip_final_goto = skip_final_goto + self.skip_final_goto = (next_routine is not None) self.final_goto_seen = False assert isinstance(routine, Routine) if routine.block: @@ -170,8 +171,11 @@ class Compiler(object): def compile_block(self, block): assert isinstance(block, Block) + block.shallow_contains_goto = False for instr in block.instrs: self.compile_instr(instr) + if isinstance(instr, GoTo): + block.shallow_contains_goto = True def compile_instr(self, instr): if isinstance(instr, SingleOp): From 44d97c33a2145d4b02ee5f17077fcb1e650a90bd Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 11:37:00 +0100 Subject: [PATCH 20/36] Better generation of code at tail of routines. --- HISTORY.md | 3 ++ TODO.md | 5 --- src/sixtypical/compiler.py | 61 ++++++++++++++++++++------------- src/sixtypical/emitter.py | 4 +++ tests/SixtyPical Compilation.md | 2 -- 5 files changed, 45 insertions(+), 30 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 75e8d68..64fa049 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -14,6 +14,9 @@ History of SixtyPical * Added `--prune-unreachable-routines` option, which causes the compiler to in fact omit routines determined to be unreachable as described above. +* Code generation now performs modest peephole optimization, + generating better code for `goto`s and `if` blocks at the + end of a routine. * The `dcc6502-adapter` test adapter was updated to conform to the output of the latest version of `dcc6502`. diff --git a/TODO.md b/TODO.md index 95967d4..861f8fe 100644 --- a/TODO.md +++ b/TODO.md @@ -90,11 +90,6 @@ if the block is in tail position. The constraints should iron out the same both As long as the routine has consistent type context every place it exits, that should be fine. -### Branch optimization in `if` - -Currently the `if` generator is not smart enough to avoid generating silly -jump instructions. (See the Fallthru tests.) Improve it. - Implementation -------------- diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index 4cc4dd3..23b1f0c 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -9,7 +9,7 @@ from sixtypical.model import ( TableType, PointerType, RoutineType, VectorType, REG_A, REG_X, REG_Y, FLAG_C ) -from sixtypical.emitter import Byte, Word, Table, Label, Offset, LowAddressByte, HighAddressByte +from sixtypical.emitter import Byte, Word, Table, Label, Offset, LowAddressByte, HighAddressByte, Emitter from sixtypical.gen6502 import ( Immediate, Absolute, AbsoluteX, AbsoluteY, ZeroPage, Indirect, IndirectY, Relative, LDA, LDX, LDY, STA, STX, STY, @@ -157,17 +157,31 @@ class Compiler(object): self.emitter.resolve_bss_label(label) def compile_routine(self, routine, next_routine=None): - self.current_routine = routine - self.skip_final_goto = (next_routine is not None) - self.final_goto_seen = False assert isinstance(routine, Routine) + + self.current_routine = routine + saved_emitter = self.emitter + self.emitter = Emitter(saved_emitter.addr) if routine.block: self.emitter.resolve_label(self.get_label(routine.name)) self.compile_block(routine.block) - if not self.final_goto_seen: + + needs_rts = True + if self.emitter.accum: + last_op = self.emitter.accum[-1] + if isinstance(last_op, JMP): + needs_rts = False + if isinstance(last_op.operand, Absolute): + if isinstance(last_op.operand.value, Label): + if next_routine and last_op.operand.value.name == next_routine.name: + self.emitter.retract() + + if needs_rts: self.emitter.emit(RTS()) + + saved_emitter.emit(*self.emitter.accum) + self.emitter = saved_emitter self.current_routine = None - self.skip_final_goto = False def compile_block(self, block): assert isinstance(block, Block) @@ -441,19 +455,15 @@ class Compiler(object): raise NotImplementedError(location_type) def compile_goto(self, instr): - self.final_goto_seen = True - if self.skip_final_goto: - pass + location = instr.location + label = self.get_label(instr.location.name) + location_type = self.get_type(location) + if isinstance(location_type, RoutineType): + self.emitter.emit(JMP(Absolute(label))) + elif isinstance(location_type, VectorType): + self.emitter.emit(JMP(Indirect(label))) else: - location = instr.location - label = self.get_label(instr.location.name) - location_type = self.get_type(location) - 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(location_type) + raise NotImplementedError(location_type) def compile_cmp(self, instr, src, dest): """`instr` is only for reporting purposes""" @@ -666,12 +676,17 @@ class Compiler(object): else_label = Label('else_label') self.emitter.emit(cls(Relative(else_label))) self.compile_block(instr.block1) + if instr.block2 is not None: - end_label = Label('end_label') - self.emitter.emit(JMP(Absolute(end_label))) - self.emitter.resolve_label(else_label) - self.compile_block(instr.block2) - self.emitter.resolve_label(end_label) + if instr.block1.shallow_contains_goto: + self.emitter.resolve_label(else_label) + self.compile_block(instr.block2) + else: + end_label = Label('end_label') + self.emitter.emit(JMP(Absolute(end_label))) + self.emitter.resolve_label(else_label) + self.compile_block(instr.block2) + self.emitter.resolve_label(end_label) else: self.emitter.resolve_label(else_label) diff --git a/src/sixtypical/emitter.py b/src/sixtypical/emitter.py index e574640..31ce731 100644 --- a/src/sixtypical/emitter.py +++ b/src/sixtypical/emitter.py @@ -171,6 +171,10 @@ class Emitter(object): self.accum.append(thing) self.addr += thing.size() + def retract(self): + thing = self.accum.pop() + self.addr -= thing.size() + def serialize_to(self, stream): """`stream` should be a file opened in binary mode.""" addr = self.start_addr diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index 687d57e..1f71955 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -610,7 +610,6 @@ Compiling `repeat forever`. = $080D LDY #$41 = $080F INY = $0810 JMP $080F - = $0813 RTS The body of `repeat forever` can be empty. @@ -620,7 +619,6 @@ The body of `repeat forever` can be empty. | } forever | } = $080D JMP $080D - = $0810 RTS Compiling `for ... up to`. From 78a1f2910ce34e79346681e9801abc68e9e1871b Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 11:55:21 +0100 Subject: [PATCH 21/36] Simplify and improve Emitter abstraction. --- src/sixtypical/compiler.py | 22 +++++++++------------- src/sixtypical/emitter.py | 6 ++++++ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index 23b1f0c..6a6d201 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -9,7 +9,7 @@ from sixtypical.model import ( TableType, PointerType, RoutineType, VectorType, REG_A, REG_X, REG_Y, FLAG_C ) -from sixtypical.emitter import Byte, Word, Table, Label, Offset, LowAddressByte, HighAddressByte, Emitter +from sixtypical.emitter import Byte, Word, Table, Label, Offset, LowAddressByte, HighAddressByte from sixtypical.gen6502 import ( Immediate, Absolute, AbsoluteX, AbsoluteY, ZeroPage, Indirect, IndirectY, Relative, LDA, LDX, LDY, STA, STX, STY, @@ -160,27 +160,23 @@ class Compiler(object): assert isinstance(routine, Routine) self.current_routine = routine - saved_emitter = self.emitter - self.emitter = Emitter(saved_emitter.addr) + if routine.block: self.emitter.resolve_label(self.get_label(routine.name)) self.compile_block(routine.block) needs_rts = True - if self.emitter.accum: - last_op = self.emitter.accum[-1] - if isinstance(last_op, JMP): - needs_rts = False - if isinstance(last_op.operand, Absolute): - if isinstance(last_op.operand.value, Label): - if next_routine and last_op.operand.value.name == next_routine.name: - self.emitter.retract() + last_op = self.emitter.get_tail() + if isinstance(last_op, JMP): + needs_rts = False + if isinstance(last_op.operand, Absolute): + if isinstance(last_op.operand.value, Label): + if next_routine and last_op.operand.value.name == next_routine.name: + self.emitter.retract() if needs_rts: self.emitter.emit(RTS()) - saved_emitter.emit(*self.emitter.accum) - self.emitter = saved_emitter self.current_routine = None def compile_block(self, block): diff --git a/src/sixtypical/emitter.py b/src/sixtypical/emitter.py index 31ce731..1e7672f 100644 --- a/src/sixtypical/emitter.py +++ b/src/sixtypical/emitter.py @@ -171,6 +171,12 @@ class Emitter(object): self.accum.append(thing) self.addr += thing.size() + def get_tail(self): + if self.accum: + return self.accum[-1] + else: + return None + def retract(self): thing = self.accum.pop() self.addr -= thing.size() From 92b1cfeefb83e4e0e5164121b867184f2bdb1b79 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 12:23:15 +0100 Subject: [PATCH 22/36] Implement tail-call optimization. --- HISTORY.md | 8 +- TODO.md | 7 -- src/sixtypical/compiler.py | 9 ++ tests/SixtyPical Compilation.md | 180 ++++++++++++++++++-------------- 4 files changed, 115 insertions(+), 89 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 64fa049..47f5298 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -14,9 +14,11 @@ History of SixtyPical * Added `--prune-unreachable-routines` option, which causes the compiler to in fact omit routines determined to be unreachable as described above. -* Code generation now performs modest peephole optimization, - generating better code for `goto`s and `if` blocks at the - end of a routine. +* Code generation now performs modest peephole optimization + at the end of each routine. This results in better code + generation for constructs in tail position, notably + tail optimization of `calls`, but also for `goto`s and + `if` blocks at the end of a routine. * The `dcc6502-adapter` test adapter was updated to conform to the output of the latest version of `dcc6502`. diff --git a/TODO.md b/TODO.md index 861f8fe..127c031 100644 --- a/TODO.md +++ b/TODO.md @@ -83,13 +83,6 @@ This is not just an impressive trick -- in the presence of local pointers, which use up a word in zero-page, which we consider a precious resource, it allow those zero-page locations to be re-used. -### Tail-call optimization - -If a block ends in a `call` can that be converted to end in a `goto`? Why not? I think it can, -if the block is in tail position. The constraints should iron out the same both ways. - -As long as the routine has consistent type context every place it exits, that should be fine. - Implementation -------------- diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index 6a6d201..f72da61 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -167,6 +167,15 @@ class Compiler(object): needs_rts = True last_op = self.emitter.get_tail() + + if isinstance(last_op, JSR): + if isinstance(last_op.operand, Absolute): + if isinstance(last_op.operand.value, Label): + label = last_op.operand.value + self.emitter.retract() + self.emitter.emit(JMP(Absolute(label))) + last_op = self.emitter.get_tail() + if isinstance(last_op, JMP): needs_rts = False if isinstance(last_op.operand, Absolute): diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index 1f71955..cac6d9c 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -51,10 +51,12 @@ Call extern. | { | ld a, 65 | call chrout + | ld a, 0 | } = $080D LDA #$41 = $080F JSR $FFD2 - = $0812 RTS + = $0812 LDA #$00 + = $0814 RTS Call defined routine. @@ -71,13 +73,39 @@ Call defined routine. | trashes a, x, y, z, n | { | call foo + | ld a, 1 | } - = $080D JSR $0811 - = $0810 RTS - = $0811 LDA #$00 - = $0813 LDX #$00 - = $0815 LDY #$00 - = $0817 RTS + = $080D JSR $0813 + = $0810 LDA #$01 + = $0812 RTS + = $0813 LDA #$00 + = $0815 LDX #$00 + = $0817 LDY #$00 + = $0819 RTS + +Tail call is optimized into a jump. + + | define foo routine + | outputs a, x, y + | trashes z, n + | { + | ld a, 0 + | ld x, 0 + | ld y, 0 + | } + | + | define main routine + | trashes a, x, y, z, n + | { + | ld a, 1 + | call foo + | } + = $080D LDA #$01 + = $080F JMP $0812 + = $0812 LDA #$00 + = $0814 LDX #$00 + = $0816 LDY #$00 + = $0818 RTS Access a defined memory location. @@ -1053,7 +1081,7 @@ Copy word to word table and back, with constant offsets. = $0848 STA $084D = $084B RTS -Indirect call. +Indirect call. TODO: we don't need the final RTS here, omit it. | vector routine | outputs x @@ -1074,16 +1102,15 @@ Indirect call. | copy bar, foo | call foo | } - = $080D LDA #$1B - = $080F STA $0822 + = $080D LDA #$1A + = $080F STA $0821 = $0812 LDA #$08 - = $0814 STA $0823 - = $0817 JSR $081E - = $081A RTS - = $081B LDX #$C8 - = $081D RTS - = $081E JMP ($0822) - = $0821 RTS + = $0814 STA $0822 + = $0817 JMP $081D + = $081A LDX #$C8 + = $081C RTS + = $081D JMP ($0821) + = $0820 RTS Compiling `goto`. Note that no `RTS` is emitted after the `JMP`. @@ -1137,28 +1164,27 @@ Copying to and from a vector table. | call one | } = $080D LDX #$00 - = $080F LDA #$3F - = $0811 STA $0846 + = $080F LDA #$3E + = $0811 STA $0845 = $0814 LDA #$08 - = $0816 STA $0847 - = $0819 LDA #$3F - = $081B STA $0848,X + = $0816 STA $0846 + = $0819 LDA #$3E + = $081B STA $0847,X = $081E LDA #$08 - = $0820 STA $0948,X - = $0823 LDA $0846 - = $0826 STA $0848,X - = $0829 LDA $0847 - = $082C STA $0948,X - = $082F LDA $0848,X - = $0832 STA $0846 - = $0835 LDA $0948,X - = $0838 STA $0847 - = $083B JSR $0842 - = $083E RTS - = $083F LDX #$C8 - = $0841 RTS - = $0842 JMP ($0846) - = $0845 RTS + = $0820 STA $0947,X + = $0823 LDA $0845 + = $0826 STA $0847,X + = $0829 LDA $0846 + = $082C STA $0947,X + = $082F LDA $0847,X + = $0832 STA $0845 + = $0835 LDA $0947,X + = $0838 STA $0846 + = $083B JMP $0841 + = $083E LDX #$C8 + = $0840 RTS + = $0841 JMP ($0845) + = $0844 RTS Copying to and from a vector table, with constant offsets. @@ -1188,28 +1214,27 @@ Copying to and from a vector table, with constant offsets. | call one | } = $080D LDX #$00 - = $080F LDA #$3F - = $0811 STA $0846 + = $080F LDA #$3E + = $0811 STA $0845 = $0814 LDA #$08 - = $0816 STA $0847 - = $0819 LDA #$3F - = $081B STA $0849,X + = $0816 STA $0846 + = $0819 LDA #$3E + = $081B STA $0848,X = $081E LDA #$08 - = $0820 STA $0949,X - = $0823 LDA $0846 - = $0826 STA $084A,X - = $0829 LDA $0847 - = $082C STA $094A,X - = $082F LDA $084B,X - = $0832 STA $0846 - = $0835 LDA $094B,X - = $0838 STA $0847 - = $083B JSR $0842 - = $083E RTS - = $083F LDX #$C8 - = $0841 RTS - = $0842 JMP ($0846) - = $0845 RTS + = $0820 STA $0948,X + = $0823 LDA $0845 + = $0826 STA $0849,X + = $0829 LDA $0846 + = $082C STA $0949,X + = $082F LDA $084A,X + = $0832 STA $0845 + = $0835 LDA $094A,X + = $0838 STA $0846 + = $083B JMP $0841 + = $083E LDX #$C8 + = $0840 RTS + = $0841 JMP ($0845) + = $0844 RTS ### add, sub @@ -1695,15 +1720,14 @@ just the same as initialized global storage locations are. | ld x, t | call foo | } - = $080D LDX $081F - = $0810 JSR $0814 - = $0813 RTS - = $0814 STX $081E - = $0817 INC $081E - = $081A LDX $081E - = $081D RTS - = $081E .byte $FF - = $081F .byte $07 + = $080D LDX $081E + = $0810 JMP $0813 + = $0813 STX $081D + = $0816 INC $081D + = $0819 LDX $081D + = $081C RTS + = $081D .byte $FF + = $081E .byte $07 Memory locations defined local dynamic to a routine are allocated just the same as uninitialized global storage locations are. @@ -1728,13 +1752,12 @@ just the same as uninitialized global storage locations are. | call foo | } = $080D LDX #$00 - = $080F STX $0821 - = $0812 JSR $0816 - = $0815 RTS - = $0816 STX $0820 - = $0819 INC $0820 - = $081C LDX $0820 - = $081F RTS + = $080F STX $0820 + = $0812 JMP $0815 + = $0815 STX $081F + = $0818 INC $081F + = $081B LDX $081F + = $081E RTS Memory locations defined local dynamic to a routine are allocated just the same as uninitialized global storage locations are, even @@ -1761,9 +1784,8 @@ when declared with a fixed address. | } = $080D LDX #$00 = $080F STX $0401 - = $0812 JSR $0816 - = $0815 RTS - = $0816 STX $0400 - = $0819 INC $0400 - = $081C LDX $0400 - = $081F RTS + = $0812 JMP $0815 + = $0815 STX $0400 + = $0818 INC $0400 + = $081B LDX $0400 + = $081E RTS From 565f959ee22b56647eaed66c41d0eec7a9b2f88f Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 12:35:29 +0100 Subject: [PATCH 23/36] Our CINV interrupt service routine should be preserved. --- eg/c64/demo-game/demo-game.60p | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eg/c64/demo-game/demo-game.60p b/eg/c64/demo-game/demo-game.60p index 31d9f97..b97a479 100644 --- a/eg/c64/demo-game/demo-game.60p +++ b/eg/c64/demo-game/demo-game.60p @@ -425,7 +425,7 @@ define game_state_game_over game_state_routine // * Main Game Loop Driver * // ************************* -define our_cinv game_state_routine +define our_cinv preserved game_state_routine { goto dispatch_game_state } From 987974cc2132b0e574c65ce1fc53d3975a771b4c Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 12:42:17 +0100 Subject: [PATCH 24/36] Simplify, fixing an apparent bug in the process. --- src/sixtypical/analyzer.py | 10 +++++++--- src/sixtypical/callgraph.py | 1 + src/sixtypical/context.py | 3 --- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index aee46f5..9dcca6f 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -141,11 +141,12 @@ class Analyzer(object): def analyze_program(self, program): assert isinstance(program, Program) for routine in program.routines: + routine.called_routines = set() context, type_ = self.analyze_routine(routine) if type_: routine.routine_type = type_ routine.encountered_gotos = list(context.encountered_gotos()) if context else [] - routine.called_routines = list(context.called_routines) if context else [] + routine.called_routines = list(routine.called_routines) def analyze_routine(self, routine): assert isinstance(routine, Routine) @@ -520,7 +521,9 @@ class Analyzer(object): type_ = self.get_type(instr.location) if not isinstance(type_, (RoutineType, VectorType)): raise TypeMismatchError(instr, instr.location.name) - context.mark_as_called(instr.location, type_) + + self.current_routine.called_routines.add((instr.location, type_)) + if isinstance(type_, VectorType): type_ = type_.of_type for ref in type_.inputs: @@ -538,7 +541,8 @@ class Analyzer(object): if not isinstance(type_, (RoutineType, VectorType)): raise TypeMismatchError(instr, location.name) - context.mark_as_called(instr.location, type_) + + self.current_routine.called_routines.add((instr.location, type_)) # assert that the dest routine's inputs are all initialized if isinstance(type_, VectorType): diff --git a/src/sixtypical/callgraph.py b/src/sixtypical/callgraph.py index 9260209..8f7afce 100644 --- a/src/sixtypical/callgraph.py +++ b/src/sixtypical/callgraph.py @@ -36,6 +36,7 @@ def construct_callgraph(program): } # Reflexive closure + # (Note, this information isn't used anywhere yet) for routine in program.routines: potentially_called_by = [] diff --git a/src/sixtypical/context.py b/src/sixtypical/context.py index 117c02f..cba38f8 100644 --- a/src/sixtypical/context.py +++ b/src/sixtypical/context.py @@ -329,6 +329,3 @@ class AnalysisContext(object): return self.symtab.fetch_local_type(self.routine.name, ref.name).max_range else: return self.symtab.fetch_global_type(ref.name).max_range - - def mark_as_called(self, location, type_): - self.called_routines.add((location, type_)) From 175c07781a970035eb6b74ebd175e892faac8a7a Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 12:54:04 +0100 Subject: [PATCH 25/36] First cut at include files. --- bin/sixtypical | 6 ++---- src/sixtypical/parser.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index c22f0b0..2a3f20d 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -17,7 +17,7 @@ from tempfile import NamedTemporaryFile import traceback from sixtypical.symtab import SymbolTable -from sixtypical.parser import Parser, merge_programs +from sixtypical.parser import Parser, load_program, merge_programs from sixtypical.analyzer import Analyzer from sixtypical.callgraph import construct_callgraph, prune_unreachable_routines from sixtypical.outputter import outputter_class_for @@ -30,11 +30,9 @@ def process_input_files(filenames, options): programs = [] for filename in options.filenames: - text = open(filename).read() - parser = Parser(symtab, text, filename) + program = load_program(filename, symtab) if options.debug: print(symtab) - program = parser.program() programs.append(program) if options.parse_only: diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 64b6631..8be9c46 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -96,6 +96,12 @@ class Parser(object): def program(self): defns = [] routines = [] + includes = [] + while self.scanner.consume('include'): + filename = self.scanner.token + self.scanner.scan() + program = load_program(filename, self.symtab) + includes.append(program) while self.scanner.on('typedef', 'const'): if self.scanner.on('typedef'): self.typedef() @@ -470,6 +476,13 @@ class Parser(object): # - - - - +def load_program(filename, symtab): + text = open(filename).read() + parser = Parser(symtab, text, filename) + program = parser.program() + return program + + def merge_programs(programs): """Assumes that the programs do not have any conflicts.""" From c70e4a55e492f53c2d6514a9f451a319ddc134bb Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 13:26:21 +0100 Subject: [PATCH 26/36] Merge included programs. --- src/sixtypical/parser.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 8be9c46..ce03244 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -128,6 +128,9 @@ class Parser(object): self.scanner.check_type('EOF') program = Program(self.scanner.line_number, defns=defns, routines=routines) + programs = includes + [program] + program = merge_programs(programs) + self.resolve_symbols(program) return program From d408e740ed26928335d2686d3a0a92a733e63042 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 15:10:55 +0100 Subject: [PATCH 27/36] Establish an `include` directory and `--include-path` option. --- HISTORY.md | 13 ++++++++----- TODO.md | 8 -------- bin/sixtypical | 9 ++++++++- eg/README.md | 17 ++++++++++++----- eg/rudiments/add.60p | 3 ++- eg/rudiments/buffer.60p | 3 ++- eg/rudiments/call.60p | 3 ++- eg/rudiments/cmp-byte.60p | 3 ++- eg/rudiments/cmp-litword.60p | 3 ++- eg/rudiments/cmp-word.60p | 3 ++- eg/rudiments/conditional.60p | 4 ++-- eg/rudiments/conditional2.60p | 3 ++- eg/rudiments/example.60p | 4 +++- eg/rudiments/goto.60p | 3 ++- eg/rudiments/loop.60p | 3 ++- eg/rudiments/memloc.60p | 3 ++- eg/rudiments/nested-for.60p | 3 ++- eg/rudiments/print.60p | 3 ++- eg/rudiments/vector-table.60p | 6 ++++-- eg/rudiments/vector.60p | 3 ++- eg/rudiments/word-table.60p | 3 ++- .../support/c64.60p => include/c64/chrout.60p | 0 .../stdlib.60p => include/stdlib/prbyte.60p | 0 .../vic20.60p => include/vic20/chrout.60p | 0 src/sixtypical/parser.py | 14 ++++++++++---- 25 files changed, 75 insertions(+), 42 deletions(-) rename eg/rudiments/support/c64.60p => include/c64/chrout.60p (100%) rename eg/rudiments/support/stdlib.60p => include/stdlib/prbyte.60p (100%) rename eg/rudiments/support/vic20.60p => include/vic20/chrout.60p (100%) diff --git a/HISTORY.md b/HISTORY.md index 47f5298..2ed6a2d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,23 +4,26 @@ History of SixtyPical 0.21 ---- +* A source file can be included in another source file + by means of the `include` directive. +* A routine can be declared `preserved`, which prevents a + compiler from omitting it from the final executable, even + if it determines it is not called by any other routine. * The reference implementation constructs a callgraph and determines the set of routines which are not reachable (directly or indirectly) from `main`, with an eye to omitting them from the final executable. -* A routine can be declared `preserved`, which prevents a - compiler from omitting it from the final executable, even - if it determines it is not called by any other routine. * Added `--prune-unreachable-routines` option, which causes the compiler to in fact omit routines determined to be unreachable as described above. +* Added `--include-path` option, which configures the list + of directories that are searched when a source file is + included with the `include` directive. * Code generation now performs modest peephole optimization at the end of each routine. This results in better code generation for constructs in tail position, notably tail optimization of `calls`, but also for `goto`s and `if` blocks at the end of a routine. -* The `dcc6502-adapter` test adapter was updated to conform - to the output of the latest version of `dcc6502`. 0.20 ---- diff --git a/TODO.md b/TODO.md index 127c031..c1c39c0 100644 --- a/TODO.md +++ b/TODO.md @@ -31,14 +31,6 @@ For goodness sake, let the programmer say `'A'` instead of `65`. Not all computers think `'A'` should be `65`. Allow the character set to be mapped. Probably copy what Ophis does. -### "Include" directives - -Search a searchlist of include paths. And use them to make libraries of routines. - -One such library routine might be an `interrupt routine` type for various architectures. -Since "the supervisor" has stored values on the stack, we should be able to trash them -with impunity, in such a routine. - ### Pointers into non-byte tables Right now you cannot get a pointer into a non-byte (for instance, word or vector) table. diff --git a/bin/sixtypical b/bin/sixtypical index 2a3f20d..b1420b4 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -26,11 +26,12 @@ from sixtypical.compiler import Compiler def process_input_files(filenames, options): symtab = SymbolTable() + include_path = options.include_path.split(':') programs = [] for filename in options.filenames: - program = load_program(filename, symtab) + program = load_program(filename, symtab, include_path) if options.debug: print(symtab) programs.append(program) @@ -137,6 +138,12 @@ if __name__ == '__main__': "Default: raw." ) + argparser.add_argument( + "--include-path", "-I", type=str, metavar='PATH', default='.', + help="A colon-separated list of directories in which to look for " + "files which are included during `include` directives." + ) + argparser.add_argument( "--analyze-only", action="store_true", diff --git a/eg/README.md b/eg/README.md index eb4be82..615a8aa 100644 --- a/eg/README.md +++ b/eg/README.md @@ -3,11 +3,18 @@ in subdirectories by machine architecture. ### rudiments -In the [rudiments](rudiments/) directory are programs which are not for -any particular machine, but meant to demonstrate the features of SixtyPical. -Some are meant to fail and produce an error message. Others can run on -any architecture where there is a routine at 65490 which outputs the value -of the accumulator as an ASCII character. +In the [rudiments](rudiments/) directory are programs which are +meant to demonstrate the elementary features of SixtyPical, and +to serve as manual integration test cases. + +These sources are portable across architectures. They include +architecture-dependent libraries to produce output. Libraries +for such are provided in the `include` directory in the root +directory of the repository; make it is on the compiler's include +search path. + +Some others of these sources are meant to fail and produce an error +message when compiled. ### c64 diff --git a/eg/rudiments/add.60p b/eg/rudiments/add.60p index 3c5a631..5174627 100644 --- a/eg/rudiments/add.60p +++ b/eg/rudiments/add.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print YY +include "chrout.60p" + word score define main routine diff --git a/eg/rudiments/buffer.60p b/eg/rudiments/buffer.60p index 18eb6d8..d5c3023 100644 --- a/eg/rudiments/buffer.60p +++ b/eg/rudiments/buffer.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print Y +include "chrout.60p" + byte table[2048] buf pointer ptr @ 254 byte foo diff --git a/eg/rudiments/call.60p b/eg/rudiments/call.60p index eed32f3..a407204 100644 --- a/eg/rudiments/call.60p +++ b/eg/rudiments/call.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print AA +include "chrout.60p" + define print routine trashes a, z, n { diff --git a/eg/rudiments/cmp-byte.60p b/eg/rudiments/cmp-byte.60p index 21e1f7b..1d488f1 100644 --- a/eg/rudiments/cmp-byte.60p +++ b/eg/rudiments/cmp-byte.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print ENGGL +include "chrout.60p" + byte b define main routine diff --git a/eg/rudiments/cmp-litword.60p b/eg/rudiments/cmp-litword.60p index 5cf9793..66366b6 100644 --- a/eg/rudiments/cmp-litword.60p +++ b/eg/rudiments/cmp-litword.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print ENGGL +include "chrout.60p" + word w1 define main routine diff --git a/eg/rudiments/cmp-word.60p b/eg/rudiments/cmp-word.60p index 853c08e..e5ff73c 100644 --- a/eg/rudiments/cmp-word.60p +++ b/eg/rudiments/cmp-word.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print ENGGL +include "chrout.60p" + word w1 word w2 diff --git a/eg/rudiments/conditional.60p b/eg/rudiments/conditional.60p index 66c0ea8..cbb4c53 100644 --- a/eg/rudiments/conditional.60p +++ b/eg/rudiments/conditional.60p @@ -1,7 +1,7 @@ -// Demonstrates vector tables. -// Include `support/${PLATFORM}.60p` before this source // Should print YN +include "chrout.60p" + define main routine trashes a, x, y, z, n, c, v { diff --git a/eg/rudiments/conditional2.60p b/eg/rudiments/conditional2.60p index 0def82c..6709b61 100644 --- a/eg/rudiments/conditional2.60p +++ b/eg/rudiments/conditional2.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print YA +include "chrout.60p" + define main routine trashes a, x, y, z, n, c, v { diff --git a/eg/rudiments/example.60p b/eg/rudiments/example.60p index a374c6e..e6f0031 100644 --- a/eg/rudiments/example.60p +++ b/eg/rudiments/example.60p @@ -1,6 +1,8 @@ -// Include `support/${PLATFORM}.60p` and `support/stdlib.60p` before this source // Should print 01 +include "chrout.60p" +include "prbyte.60p" + byte lives define main routine diff --git a/eg/rudiments/goto.60p b/eg/rudiments/goto.60p index 1c8d9a9..1e57945 100644 --- a/eg/rudiments/goto.60p +++ b/eg/rudiments/goto.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print AB +include "chrout.60p" + define bar routine trashes a, z, n { ld a, 66 call chrout diff --git a/eg/rudiments/loop.60p b/eg/rudiments/loop.60p index 6ccb445..b4c57fc 100644 --- a/eg/rudiments/loop.60p +++ b/eg/rudiments/loop.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print ABCDEFGHIJKLMNOPQRSTUVWXYZ +include "chrout.60p" + define main routine trashes a, y, z, n, c { diff --git a/eg/rudiments/memloc.60p b/eg/rudiments/memloc.60p index cb8bd5b..2baf8a8 100644 --- a/eg/rudiments/memloc.60p +++ b/eg/rudiments/memloc.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print AB +include "chrout.60p" + byte foo define print routine diff --git a/eg/rudiments/nested-for.60p b/eg/rudiments/nested-for.60p index 833f09d..c735d90 100644 --- a/eg/rudiments/nested-for.60p +++ b/eg/rudiments/nested-for.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print H (being ASCII 72 = 8 * 9) +include "chrout.60p" + // Increase y by 7, circuitously // define foo routine diff --git a/eg/rudiments/print.60p b/eg/rudiments/print.60p index 7589930..78ac6bb 100644 --- a/eg/rudiments/print.60p +++ b/eg/rudiments/print.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print A +include "chrout.60p" + define main routine inputs a trashes a, z, n diff --git a/eg/rudiments/vector-table.60p b/eg/rudiments/vector-table.60p index 0bead91..7b8e9b0 100644 --- a/eg/rudiments/vector-table.60p +++ b/eg/rudiments/vector-table.60p @@ -1,7 +1,9 @@ -// Demonstrates vector tables. -// Include `support/${PLATFORM}.60p` before this source // Should print AABAB +// Demonstrates vector tables. + +include "chrout.60p" + vector routine trashes a, z, n print diff --git a/eg/rudiments/vector.60p b/eg/rudiments/vector.60p index b4b018c..7f0b38a 100644 --- a/eg/rudiments/vector.60p +++ b/eg/rudiments/vector.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print AB +include "chrout.60p" + vector routine trashes a, z, n print diff --git a/eg/rudiments/word-table.60p b/eg/rudiments/word-table.60p index 54babbc..9372700 100644 --- a/eg/rudiments/word-table.60p +++ b/eg/rudiments/word-table.60p @@ -1,6 +1,7 @@ -// Include `support/${PLATFORM}.60p` before this source // Should print YY +include "chrout.60p" + word one word table[256] many diff --git a/eg/rudiments/support/c64.60p b/include/c64/chrout.60p similarity index 100% rename from eg/rudiments/support/c64.60p rename to include/c64/chrout.60p diff --git a/eg/rudiments/support/stdlib.60p b/include/stdlib/prbyte.60p similarity index 100% rename from eg/rudiments/support/stdlib.60p rename to include/stdlib/prbyte.60p diff --git a/eg/rudiments/support/vic20.60p b/include/vic20/chrout.60p similarity index 100% rename from eg/rudiments/support/vic20.60p rename to include/vic20/chrout.60p diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index ce03244..cc35c2d 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -21,8 +21,9 @@ class ForwardReference(object): class Parser(object): - def __init__(self, symtab, text, filename): + def __init__(self, symtab, text, filename, include_path): self.symtab = symtab + self.include_path = include_path self.scanner = Scanner(text, filename) self.current_routine_name = None @@ -100,7 +101,7 @@ class Parser(object): while self.scanner.consume('include'): filename = self.scanner.token self.scanner.scan() - program = load_program(filename, self.symtab) + program = load_program(filename, self.symtab, self.include_path) includes.append(program) while self.scanner.on('typedef', 'const'): if self.scanner.on('typedef'): @@ -479,9 +480,14 @@ class Parser(object): # - - - - -def load_program(filename, symtab): +def load_program(filename, symtab, include_path): + import os + for include_dir in include_path: + if os.path.exists(os.path.join(include_dir, filename)): + filename = os.path.join(include_dir, filename) + break text = open(filename).read() - parser = Parser(symtab, text, filename) + parser = Parser(symtab, text, filename, include_path) program = parser.program() return program From dc4677ade8cbd577657e869e9624868e16da7a83 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 15:30:24 +0100 Subject: [PATCH 28/36] Update READMEs. --- eg/README.md | 12 ++---------- eg/rudiments/README.md | 16 ++++++++-------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/eg/README.md b/eg/README.md index 615a8aa..9268429 100644 --- a/eg/README.md +++ b/eg/README.md @@ -5,16 +5,8 @@ in subdirectories by machine architecture. In the [rudiments](rudiments/) directory are programs which are meant to demonstrate the elementary features of SixtyPical, and -to serve as manual integration test cases. - -These sources are portable across architectures. They include -architecture-dependent libraries to produce output. Libraries -for such are provided in the `include` directory in the root -directory of the repository; make it is on the compiler's include -search path. - -Some others of these sources are meant to fail and produce an error -message when compiled. +to serve as manual integration test cases. See +[the README in that directory](rudiments/README.md) for details. ### c64 diff --git a/eg/rudiments/README.md b/eg/rudiments/README.md index a3b675d..547eb63 100644 --- a/eg/rudiments/README.md +++ b/eg/rudiments/README.md @@ -2,16 +2,16 @@ This directory contains example sources which demonstrate the rudiments of SixtyPical. Examples that are meant to fail and produce an error message -are in the `errorful/` subdirectory. +when being compiled are in the `errorful/` subdirectory. -These files are intended to be architecture-agnostic. -For the ones that do produce output, an appropriate source -under `support/` should be included first, so that system entry -points such as `chrout` are defined. In addition, some of these -programs use "standard" support modules, so those should be included -first too. For example: +The other sources are portable across architectures. They use +`include` directives to bring in architecture-dependent libraries +to produce output. Libraries for such are provided in the +architecture-specific subdirectories of the `include` directory +in the root directory of this repository; make sure it is on the +compiler's include search path. For example: - sixtypical --run-on=x64 support/c64.60p support/stdlib.60p vector-table.60p + sixtypical --run-on=x64 -I../../include/c64/:../../include/stdlib/ vector-table.60p `chrout` is a routine with outputs the value of the accumulator as an ASCII character, disturbing none of the other registers, From 1b5f4c0c4b0b83d4835caa5120f7a5ad7881d48b Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 16:09:58 +0100 Subject: [PATCH 29/36] Add architecture-dependent "joy2delta.60p" to "standard library". --- HISTORY.md | 2 ++ TODO.md | 7 +------ eg/c64/demo-game/demo-game.60p | 37 ++------------------------------- eg/c64/joystick.60p | 38 ++-------------------------------- include/c64/joy2delta.60p | 36 ++++++++++++++++++++++++++++++++ 5 files changed, 43 insertions(+), 77 deletions(-) create mode 100644 include/c64/joy2delta.60p diff --git a/HISTORY.md b/HISTORY.md index 2ed6a2d..48ea5db 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -24,6 +24,8 @@ History of SixtyPical generation for constructs in tail position, notably tail optimization of `calls`, but also for `goto`s and `if` blocks at the end of a routine. +* Began collecting architecture-specific and portable + include-files for a nascent "standard library". 0.20 ---- diff --git a/TODO.md b/TODO.md index c1c39c0..7b08e56 100644 --- a/TODO.md +++ b/TODO.md @@ -78,16 +78,11 @@ zero-page locations to be re-used. Implementation -------------- -### Line numbers in analysis error messages +### Filename and line number in analysis error messages For analysis errors, there is a line number, but it's the line of the routine after the routine in which the analysis error occurred. Fix this. -### Libraries - -Now that we have dead-code removal, establish some libraries of reusable -routines. - Blue-skying ----------- diff --git a/eg/c64/demo-game/demo-game.60p b/eg/c64/demo-game/demo-game.60p index b97a479..027f2cc 100644 --- a/eg/c64/demo-game/demo-game.60p +++ b/eg/c64/demo-game/demo-game.60p @@ -2,6 +2,8 @@ // * Demo Game for SixtyPical * // **************************** +include "joy2delta.60p" + // ---------------------------------------------------------------- // Type Definitions // ---------------------------------------------------------------- @@ -56,7 +58,6 @@ byte vic_border @ 53280 byte vic_bg @ 53281 byte table[2048] screen @ 1024 byte table[2048] colormap @ 55296 -byte joy2 @ $dc00 // ---------------------------------------------------------------- // Global Variables @@ -69,7 +70,6 @@ word pos word new_pos word table[256] actor_delta -word delta byte player_died @@ -103,39 +103,6 @@ vector game_state_routine // Utility Routines // ---------------------------------------------------------------- -define read_stick routine - inputs joy2 - outputs delta - trashes a, x, z, n -{ - ld x, joy2 - ld a, x - and a, 1 // up - if z { - copy $ffd8, delta // -40 - } else { - ld a, x - and a, 2 // down - if z { - copy word 40, delta - } else { - ld a, x - and a, 4 // left - if z { - copy $ffff, delta // -1 - } else { - ld a, x - and a, 8 // right - if z { - copy word 1, delta - } else { - copy word 0, delta - } - } - } - } -} - // You can repeatedly (i.e. as part of actor logic or an IRQ handler) // call this routine. // Upon return, if carry is set, the button was pressed then released. diff --git a/eg/c64/joystick.60p b/eg/c64/joystick.60p index fdbaa83..0558426 100644 --- a/eg/c64/joystick.60p +++ b/eg/c64/joystick.60p @@ -1,40 +1,6 @@ +include "joy2delta.60p" + word screen @ 1024 -byte joy2 @ $dc00 - -word delta - -define read_stick routine - inputs joy2 - outputs delta - trashes a, x, z, n -{ - ld x, joy2 - ld a, x - and a, 1 // up - if z { - copy $ffd8, delta // -40 - } else { - ld a, x - and a, 2 // down - if z { - copy word 40, delta - } else { - ld a, x - and a, 4 // left - if z { - copy $ffff, delta // -1 - } else { - ld a, x - and a, 8 // right - if z { - copy word 1, delta - } else { - copy word 0, delta - } - } - } - } -} define main routine inputs joy2 diff --git a/include/c64/joy2delta.60p b/include/c64/joy2delta.60p new file mode 100644 index 0000000..ff1b583 --- /dev/null +++ b/include/c64/joy2delta.60p @@ -0,0 +1,36 @@ +byte joy2 @ $dc00 + +word delta + +define read_stick routine + inputs joy2 + outputs delta + trashes a, x, z, n +{ + ld x, joy2 + ld a, x + and a, 1 // up + if z { + copy $ffd8, delta // -40 + } else { + ld a, x + and a, 2 // down + if z { + copy word 40, delta + } else { + ld a, x + and a, 4 // left + if z { + copy $ffff, delta // -1 + } else { + ld a, x + and a, 8 // right + if z { + copy word 1, delta + } else { + copy word 0, delta + } + } + } + } +} From 4b518508d57d7ede676991b0cb5e94559ac1cae5 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 16:26:50 +0100 Subject: [PATCH 30/36] called_routines are not stored in context at all. --- src/sixtypical/context.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/sixtypical/context.py b/src/sixtypical/context.py index cba38f8..a92c513 100644 --- a/src/sixtypical/context.py +++ b/src/sixtypical/context.py @@ -35,7 +35,6 @@ class AnalysisContext(object): self._terminated = False self._gotos_encountered = set() self._pointer_assoc = dict() - self.called_routines = set() for ref in inputs: if self.is_constant(ref): @@ -80,7 +79,6 @@ class AnalysisContext(object): c._writeable = set(self._writeable) c._pointer_assoc = dict(self._pointer_assoc) c._gotos_encountered = set(self._gotos_encountered) - c.called_routines = set(self.called_routines) return c def update_from(self, other): @@ -96,7 +94,6 @@ class AnalysisContext(object): self._writeable = set(other._writeable) self._terminated = other._terminated self._pointer_assoc = dict(other._pointer_assoc) - self.called_routines = set(other.called_routines) def each_meaningful(self): for ref in self._range.keys(): From fbfab44887265b5111bea369eb10bf8098cb5ff7 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Tue, 22 Oct 2019 16:56:19 +0100 Subject: [PATCH 31/36] Update the errorful rudiments. --- eg/rudiments/errorful/add.60p | 5 +++++ eg/rudiments/errorful/range.60p | 4 ++++ eg/rudiments/errorful/vector.60p | 11 ++++++++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/eg/rudiments/errorful/add.60p b/eg/rudiments/errorful/add.60p index ba653a4..fdd49e7 100644 --- a/eg/rudiments/errorful/add.60p +++ b/eg/rudiments/errorful/add.60p @@ -1,3 +1,8 @@ +// should fail analysis with an UnmeaningfulReadError +// because adding 4 to the accumulator reads the carry +// but the carry has neither been provided as input +// nor set to anything in particular by this routine. + define add_four routine inputs a outputs a diff --git a/eg/rudiments/errorful/range.60p b/eg/rudiments/errorful/range.60p index 87e1095..941c82b 100644 --- a/eg/rudiments/errorful/range.60p +++ b/eg/rudiments/errorful/range.60p @@ -1,3 +1,7 @@ +// should fail analysis with a RangeExceededError +// because the index is detected to fall outside the +// allowable range of the table it is indexing. + byte table[8] message : "WHAT?" define main routine diff --git a/eg/rudiments/errorful/vector.60p b/eg/rudiments/errorful/vector.60p index 3671924..55b7a21 100644 --- a/eg/rudiments/errorful/vector.60p +++ b/eg/rudiments/errorful/vector.60p @@ -1,9 +1,14 @@ -vector vec +// should fail analysis with a ConstantConstraintError +// because it cannot copy the address of `foo` into `vec` +// because it has incompatible constraints. + +vector routine inputs y outputs y trashes z, n + vec -routine foo +define foo routine inputs x outputs x trashes z, n @@ -11,7 +16,7 @@ routine foo inc x } -routine main +define main routine inputs foo outputs vec trashes a, z, n From f4577804c7d7869a67536fec88a95860a44be0b4 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 23 Oct 2019 10:21:02 +0100 Subject: [PATCH 32/36] A little reshuffling and improving of READMEs and notes. --- README.md | 6 +- TODO.md | 25 ++++++++ eg/README.md | 20 +----- eg/c64/README.md | 22 ++++++- eg/c64/demo-game/demo-game.60p | 34 +---------- eg/c64/demo-game/run.sh | 23 +++++++ eg/c64/{joystick.60p => joystick-demo.60p} | 2 +- include/c64/joy2delta.60p | 36 ----------- include/c64/joystick.60p | 71 ++++++++++++++++++++++ 9 files changed, 148 insertions(+), 91 deletions(-) create mode 100755 eg/c64/demo-game/run.sh rename eg/c64/{joystick.60p => joystick-demo.60p} (89%) delete mode 100644 include/c64/joy2delta.60p create mode 100644 include/c64/joystick.60p diff --git a/README.md b/README.md index a396a6d..d16a403 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,10 @@ SixtyPical _Version 0.21. Work-in-progress, everything is subject to change._ +**SixtyPical** brings [extended static checking][] to the [6502][]. + **SixtyPical** is a [low-level](#low-level) programming language -supporting a sophisticated [static analysis](#static-analysis). +supporting some advanced [static analysis](#static-analysis) methods. Its reference compiler can generate [efficient code](#efficient-code) for several 6502-based [target platforms](#target-platforms) while catching many common mistakes at compile-time, reducing the time spent in debugging. @@ -124,7 +126,9 @@ Documentation * [Output formats supported by `sixtypical`](doc/Output%20Formats.md) * [TODO](TODO.md) +[6502]: https://en.wikipedia.org/wiki/MOS_Technology_6502 [MOS Technology 6502]: https://en.wikipedia.org/wiki/MOS_Technology_6502 +[extended static checking]: https://en.wikipedia.org/wiki/Extended_static_checking [effect system]: https://en.wikipedia.org/wiki/Effect_system [abstractly interprets]: https://en.wikipedia.org/wiki/Abstract_interpretation [calling conventions]: https://en.wikipedia.org/wiki/Calling_convention diff --git a/TODO.md b/TODO.md index 7b08e56..8be7282 100644 --- a/TODO.md +++ b/TODO.md @@ -55,6 +55,14 @@ What happens if a routine calls itself, directly or indirectly? Many constraints might be violated in this case. We should probably disallow recursion by default. (Which means assembling the callgraph in all cases.) +However note, it's okay for a routine to goto itself. It's a common +pattern for implementing a state machine, for a routine to tail-goto a +vector, which might contain the address of the same routine. + +The problems only come up, I think, when a routine calls itself re-entrantly. + +So the callgraph would need to distinguish between these two cases. + ### Analyze memory usage If you define two variables that occupy the same address, an analysis error ought @@ -83,6 +91,23 @@ Implementation For analysis errors, there is a line number, but it's the line of the routine after the routine in which the analysis error occurred. Fix this. +### Better selection of options + +`-O` should turn on the standard optimizations. + +There should maybe be a flag to turn off tail-call optimization. + +Some options should automatically add the appropriate architecture include +directory to the path. + +Distribution +------------ + +### Demo game + +Seems you're not be able to get killed unless you go off the top or bottom of +the screen? In particular, you cannot collide with a bar? + Blue-skying ----------- diff --git a/eg/README.md b/eg/README.md index 9268429..289839c 100644 --- a/eg/README.md +++ b/eg/README.md @@ -13,24 +13,8 @@ to serve as manual integration test cases. See In the [c64](c64/) directory are programs that run on the Commodore 64. The directory itself contains some simple demos, for example [hearts.60p](c64/hearts.60p), while there are subdirectories for more -elaborate demos: - -* [demo-game](c64/demo-game/): a little game-like program written as a - "can we write something you'd see in practice?" test case for SixtyPical. - -* [ribos](c64/ribos/): a well-commented example of a C64 raster interrupt - routine. Originally written with the P65 assembler (which has since - been reborn as [Ophis][]). - - The second version of Ribos has been translated to SixtyPical. - -* [petulant](c64/petulant/): "The PETulant Cursor", a tiny (44 bytes) - "display hack". Originally written in the late 80's. Rewritten with - the P65 assembler (now Ophis) and re-released on April 1st, 2008 (a - hint as to its nature). - - Translated to SixtyPical (in 2018), after adding some optimizations - to the SixtyPical compiler, the resulting executable is still 44 bytes! +elaborate demos, like the flagship demo game. See +[the README in that directory](c64/README.md) for details. ### vic20 diff --git a/eg/c64/README.md b/eg/c64/README.md index a953e06..b63c514 100644 --- a/eg/c64/README.md +++ b/eg/c64/README.md @@ -1,5 +1,23 @@ This directory contains SixtyPical example programs specifically for the Commodore 64. -See the [README in the parent directory](../README.md) for -more information on these example programs. +There are subdirectories for more elaborate demos: + +* [demo-game](demo-game/): a little game-like program written as a + "can we write something you'd see in practice?" test case for SixtyPical. + +* [ribos](ribos/): a well-commented example of a C64 raster interrupt + routine. Originally written with the P65 assembler (which has since + been reborn as [Ophis][]). + + The second version of Ribos has been translated to SixtyPical. + +* [petulant](petulant/): "The PETulant Cursor", a tiny (44 bytes) + "display hack". Originally written in the late 80's. Rewritten with + the P65 assembler (now Ophis) and re-released on April 1st, 2008 (a + hint as to its nature). + + Translated to SixtyPical (in 2018), after adding some optimizations + to the SixtyPical compiler, the resulting executable is still 44 bytes! + +[Ophis]: http://michaelcmartin.github.io/Ophis/ diff --git a/eg/c64/demo-game/demo-game.60p b/eg/c64/demo-game/demo-game.60p index 027f2cc..11fcc8d 100644 --- a/eg/c64/demo-game/demo-game.60p +++ b/eg/c64/demo-game/demo-game.60p @@ -2,7 +2,7 @@ // * Demo Game for SixtyPical * // **************************** -include "joy2delta.60p" +include "joystick.60p" // ---------------------------------------------------------------- // Type Definitions @@ -103,38 +103,6 @@ vector game_state_routine // Utility Routines // ---------------------------------------------------------------- -// You can repeatedly (i.e. as part of actor logic or an IRQ handler) -// call this routine. -// Upon return, if carry is set, the button was pressed then released. - -define check_button routine - inputs joy2 - outputs c - trashes a, z, n - static byte button_down : 0 -{ - ld a, button_down - if z { - ld a, joy2 - and a, $10 - if z { - ld a, 1 - st a, button_down - } - st off, c - } else { - ld a, joy2 - and a, $10 - if not z { - ld a, 0 - st a, button_down - st on, c - } else { - st off, c - } - } -} - define clear_screen routine outputs screen, colormap trashes a, y, c, n, z diff --git a/eg/c64/demo-game/run.sh b/eg/c64/demo-game/run.sh new file mode 100755 index 0000000..1086985 --- /dev/null +++ b/eg/c64/demo-game/run.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# This script builds and runs the demo game. You need +# the VICE emulatore installed, in particular VICE's x64. + +# You might want a `vicerc` file like the following: +# [C64] +# VICIIDoubleScan=0 +# VICIIDoubleSize=0 +# KeySet1NorthWest=0 +# KeySet1North=273 +# KeySet1NorthEast=0 +# KeySet1East=275 +# KeySet1SouthEast=0 +# KeySet1South=274 +# KeySet1SouthWest=0 +# KeySet1West=276 +# KeySet1Fire=306 +# KeySetEnable=1 +# JoyDevice1=0 +# JoyDevice2=2 + +../../../bin/sixtypical --run-on x64 -I ../../../include/c64/ demo-game.60p diff --git a/eg/c64/joystick.60p b/eg/c64/joystick-demo.60p similarity index 89% rename from eg/c64/joystick.60p rename to eg/c64/joystick-demo.60p index 0558426..faeb949 100644 --- a/eg/c64/joystick.60p +++ b/eg/c64/joystick-demo.60p @@ -1,4 +1,4 @@ -include "joy2delta.60p" +include "joystick.60p" word screen @ 1024 diff --git a/include/c64/joy2delta.60p b/include/c64/joy2delta.60p deleted file mode 100644 index ff1b583..0000000 --- a/include/c64/joy2delta.60p +++ /dev/null @@ -1,36 +0,0 @@ -byte joy2 @ $dc00 - -word delta - -define read_stick routine - inputs joy2 - outputs delta - trashes a, x, z, n -{ - ld x, joy2 - ld a, x - and a, 1 // up - if z { - copy $ffd8, delta // -40 - } else { - ld a, x - and a, 2 // down - if z { - copy word 40, delta - } else { - ld a, x - and a, 4 // left - if z { - copy $ffff, delta // -1 - } else { - ld a, x - and a, 8 // right - if z { - copy word 1, delta - } else { - copy word 0, delta - } - } - } - } -} diff --git a/include/c64/joystick.60p b/include/c64/joystick.60p new file mode 100644 index 0000000..faf41c1 --- /dev/null +++ b/include/c64/joystick.60p @@ -0,0 +1,71 @@ +byte joy2 @ $dc00 + +word delta + +// Read the joystick and compute the delta it represents +// in a row-based 40-column grid like the C64's screen. + +define read_stick routine + inputs joy2 + outputs delta + trashes a, x, z, n +{ + ld x, joy2 + ld a, x + and a, 1 // up + if z { + copy $ffd8, delta // -40 + } else { + ld a, x + and a, 2 // down + if z { + copy word 40, delta + } else { + ld a, x + and a, 4 // left + if z { + copy $ffff, delta // -1 + } else { + ld a, x + and a, 8 // right + if z { + copy word 1, delta + } else { + copy word 0, delta + } + } + } + } +} + +// You can repeatedly (i.e. as part of actor logic or an IRQ handler) +// call this routine. +// Upon return, if carry is set, the button was pressed then released. + +define check_button routine + inputs joy2 + outputs c + trashes a, z, n + static byte button_down : 0 +{ + ld a, button_down + if z { + ld a, joy2 + and a, $10 + if z { + ld a, 1 + st a, button_down + } + st off, c + } else { + ld a, joy2 + and a, $10 + if not z { + ld a, 0 + st a, button_down + st on, c + } else { + st off, c + } + } +} From a9917d3ea87081f4228ee63ed603e32889be145d Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 23 Oct 2019 10:23:43 +0100 Subject: [PATCH 33/36] Tweak. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d16a403..ce15afa 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ _Version 0.21. Work-in-progress, everything is subject to change._ **SixtyPical** brings [extended static checking][] to the [6502][]. -**SixtyPical** is a [low-level](#low-level) programming language +SixtyPical is a [low-level](#low-level) programming language supporting some advanced [static analysis](#static-analysis) methods. Its reference compiler can generate [efficient code](#efficient-code) for several 6502-based [target platforms](#target-platforms) while catching many From 023a415a145de2498a15e5189093d321e05db688 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 23 Oct 2019 10:32:26 +0100 Subject: [PATCH 34/36] Don't search for filenames given on cmdline in the include path. --- bin/sixtypical | 2 +- src/sixtypical/parser.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bin/sixtypical b/bin/sixtypical index b1420b4..2296ac3 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -31,7 +31,7 @@ def process_input_files(filenames, options): programs = [] for filename in options.filenames: - program = load_program(filename, symtab, include_path) + program = load_program(filename, symtab, include_path, include_file=False) if options.debug: print(symtab) programs.append(program) diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index cc35c2d..15c39b4 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -101,7 +101,7 @@ class Parser(object): while self.scanner.consume('include'): filename = self.scanner.token self.scanner.scan() - program = load_program(filename, self.symtab, self.include_path) + program = load_program(filename, self.symtab, self.include_path, include_file=True) includes.append(program) while self.scanner.on('typedef', 'const'): if self.scanner.on('typedef'): @@ -480,12 +480,13 @@ class Parser(object): # - - - - -def load_program(filename, symtab, include_path): +def load_program(filename, symtab, include_path, include_file=False): import os - for include_dir in include_path: - if os.path.exists(os.path.join(include_dir, filename)): - filename = os.path.join(include_dir, filename) - break + if include_file: + for include_dir in include_path: + if os.path.exists(os.path.join(include_dir, filename)): + filename = os.path.join(include_dir, filename) + break text = open(filename).read() parser = Parser(symtab, text, filename, include_path) program = parser.program() From ff67e15208c70691e404c02819854b394a81cdd4 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 23 Oct 2019 10:34:37 +0100 Subject: [PATCH 35/36] Link to new test suite from README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ce15afa..a6c0eed 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ In order to run the tests for compilation, [dcc6502][] needs to be installed. * [Literate test suite for SixtyPical analysis (control flow)](tests/SixtyPical%20Control%20Flow.md) * [Literate test suite for SixtyPical compilation](tests/SixtyPical%20Compilation.md) * [Literate test suite for SixtyPical fallthru optimization](tests/SixtyPical%20Fallthru.md) +* [Literate test suite for SixtyPical callgraph construction](tests/SixtyPical%20Callgraph.md) Documentation ------------- From 632c2f4517f106fb3382f4e47e2d9ade2c0c4f04 Mon Sep 17 00:00:00 2001 From: Chris Pressey Date: Wed, 23 Oct 2019 15:00:03 +0100 Subject: [PATCH 36/36] Correct comment. --- src/sixtypical/callgraph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sixtypical/callgraph.py b/src/sixtypical/callgraph.py index 8f7afce..745a17d 100644 --- a/src/sixtypical/callgraph.py +++ b/src/sixtypical/callgraph.py @@ -35,7 +35,7 @@ def construct_callgraph(program): 'potentially-calls': potentially_calls, } - # Reflexive closure + # Symmetric closure # (Note, this information isn't used anywhere yet) for routine in program.routines: