diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..a0213db --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,27 @@ +# 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: | + echo "hi" + 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 diff --git a/HISTORY.md b/HISTORY.md index 36bf490..48ea5db 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,32 @@ 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. +* 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. +* Began collecting architecture-specific and portable + include-files for a nascent "standard library". + 0.20 ---- diff --git a/README.md b/README.md index ab177fc..a6c0eed 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ 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). +**SixtyPical** brings [extended static checking][] to the [6502][]. + +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 common mistakes at compile-time, reducing the time spent in debugging. @@ -114,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 ------------- @@ -124,7 +127,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 1761bde..8be7282 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. @@ -63,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,36 +83,31 @@ This is not just an impressive trick -- in the presence of local pointers, which use up a word in zero-page, which we consider a precious resource, it allow those zero-page locations to be re-used. -### Tail-call optimization - -If a block ends in a `call` can that be converted to end in a `goto`? Why not? I think it can, -if the block is in tail position. The constraints should iron out the same both ways. - -As long as the routine has consistent type context every place it exits, that should be fine. - -### Branch optimization in `if` - -Currently the `if` generator is not smart enough to avoid generating silly -jump instructions. (See the Fallthru tests.) Improve it. - -### Dead code removal - -Once we have a call graph we can omit routines that we're sure aren't called. - -This would let us use include-files and standard-libraries nicely: any -routines they define, but that you don't use, don't get included. - -Analyzing the set of possible routines that a vector can take on would help -this immensely. - Implementation -------------- -### Line numbers in analysis error messages +### 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. +### 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/bin/sixtypical b/bin/sixtypical index c847389..2296ac3 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -17,23 +17,23 @@ 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 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: - text = open(filename).read() - parser = Parser(symtab, text, filename) + program = load_program(filename, symtab, include_path, include_file=False) if options.debug: print(symtab) - program = parser.program() programs.append(program) if options.parse_only: @@ -47,25 +47,24 @@ 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: + 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: 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 @@ -139,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", @@ -161,6 +166,17 @@ 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", + help="Dump the call graph, in JSON, to stdout after analyzing the program." + ) argparser.add_argument( "--parse-only", action="store_true", @@ -185,7 +201,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:]) diff --git a/eg/README.md b/eg/README.md index eb4be82..289839c 100644 --- a/eg/README.md +++ b/eg/README.md @@ -3,35 +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. See +[the README in that directory](rudiments/README.md) for details. ### c64 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 31d9f97..11fcc8d 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 "joystick.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,71 +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. - -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 @@ -425,7 +360,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 } 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-demo.60p b/eg/c64/joystick-demo.60p new file mode 100644 index 0000000..faeb949 --- /dev/null +++ b/eg/c64/joystick-demo.60p @@ -0,0 +1,15 @@ +include "joystick.60p" + +word screen @ 1024 + +define main routine + inputs joy2 + outputs delta + trashes a, x, z, n, screen +{ + repeat { + call read_stick + copy delta, screen + ld a, 1 + } until z +} 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, 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/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 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/c64/joystick.60p b/include/c64/joystick.60p similarity index 51% rename from eg/c64/joystick.60p rename to include/c64/joystick.60p index fdbaa83..faf41c1 100644 --- a/eg/c64/joystick.60p +++ b/include/c64/joystick.60p @@ -1,8 +1,10 @@ -word screen @ 1024 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 @@ -36,14 +38,34 @@ define read_stick routine } } -define main routine +// 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 delta - trashes a, x, z, n, screen + outputs c + trashes a, z, n + static byte button_down : 0 { - repeat { - call read_stick - copy delta, screen - ld a, 1 - } until z + 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 + } + } } 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/analyzer.py b/src/sixtypical/analyzer.py index 1e3991b..9dcca6f 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -141,17 +141,23 @@ class Analyzer(object): def analyze_program(self, program): assert isinstance(program, Program) for routine in program.routines: - context = self.analyze_routine(routine) + 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(routine.called_routines) 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 @@ -209,7 +215,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) @@ -512,16 +518,19 @@ 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) - if isinstance(type, VectorType): - type = type.of_type - for ref in type.inputs: + + self.current_routine.called_routines.add((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) @@ -533,6 +542,8 @@ class Analyzer(object): if not isinstance(type_, (RoutineType, VectorType)): raise TypeMismatchError(instr, location.name) + self.current_routine.called_routines.add((instr.location, type_)) + # assert that the dest routine's inputs are all initialized if isinstance(type_, VectorType): type_ = type_.of_type diff --git a/src/sixtypical/callgraph.py b/src/sixtypical/callgraph.py new file mode 100644 index 0000000..745a17d --- /dev/null +++ b/src/sixtypical/callgraph.py @@ -0,0 +1,68 @@ +from sixtypical.ast import Program +from sixtypical.model import RoutineType, VectorType + + +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 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 = {} + + 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_type(program, called_routine_type): + potentially_calls.append(potentially_called.name) + else: + raise NotImplementedError + graph[routine.name] = { + 'potentially-calls': potentially_calls, + } + + # Symmetric closure + # (Note, this information isn't used anywhere yet) + + 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 + + # 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 + + +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/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index f9fac26..f72da61 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,23 +156,45 @@ 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): - self.current_routine = routine - self.skip_final_goto = skip_final_goto - self.final_goto_seen = False + def compile_routine(self, routine, next_routine=None): assert isinstance(routine, Routine) + + self.current_routine = routine + 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 + 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): + 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()) + self.current_routine = None - self.skip_final_goto = False 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): @@ -437,19 +460,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""" @@ -662,12 +681,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..1e7672f 100644 --- a/src/sixtypical/emitter.py +++ b/src/sixtypical/emitter.py @@ -171,6 +171,16 @@ 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() + def serialize_to(self, stream): """`stream` should be a file opened in binary mode.""" addr = self.start_addr diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 0cf7107..15c39b4 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 @@ -96,6 +97,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, self.include_path, include_file=True) + includes.append(program) while self.scanner.on('typedef', 'const'): if self.scanner.on('typedef'): self.typedef() @@ -111,13 +118,20 @@ 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') program = Program(self.scanner.line_number, defns=defns, routines=routines) + programs = includes + [program] + program = merge_programs(programs) + self.resolve_symbols(program) return program @@ -466,6 +480,19 @@ class Parser(object): # - - - - +def load_program(filename, symtab, include_path, include_file=False): + import os + 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() + return program + + def merge_programs(programs): """Assumes that the programs do not have any conflicts.""" 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..6aa91b9 --- /dev/null +++ b/tests/SixtyPical Callgraph.md @@ -0,0 +1,223 @@ +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. The thing that it will +be called by is the system, but the callgraph analyzer simply +considers it to be "reachable". + + | define main routine + | { + | } + = { + = "main": { + = "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 + | { + | call other + | } + | + | define other routine + | { + | } + = { + = "main": { + = "potentially-called-by": [], + = "potentially-calls": [ + = "other" + = ], + = "reachable": true + = }, + = "other": { + = "potentially-called-by": [ + = "main" + = ], + = "potentially-calls": [], + = "reachable": true + = } + = } + +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 + | { + | } + | + | define other routine + | { + | } + = { + = "main": { + = "potentially-called-by": [], + = "potentially-calls": [], + = "reachable": true + = }, + = "other": { + = "potentially-called-by": [], + = "potentially-calls": [] + = } + = } + +If a routine is not called by another routine, but it is declared +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 + | { + | } + | + | define other preserved routine + | { + | } + = { + = "main": { + = "potentially-called-by": [], + = "potentially-calls": [], + = "reachable": true + = }, + = "other": { + = "potentially-called-by": [], + = "potentially-calls": [], + = "reachable": true + = } + = } + +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 + | { + | } + | + | define other1 routine + | { + | call other2 + | } + | + | define other2 routine + | { + | call other1 + | } + = { + = "main": { + = "potentially-called-by": [], + = "potentially-calls": [], + = "reachable": true + = }, + = "other1": { + = "potentially-called-by": [ + = "other2" + = ], + = "potentially-calls": [ + = "other2" + = ] + = }, + = "other2": { + = "potentially-called-by": [ + = "other1" + = ], + = "potentially-calls": [ + = "other1" + = ] + = } + = } + + -> 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/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index 687d57e..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. @@ -610,7 +638,6 @@ Compiling `repeat forever`. = $080D LDY #$41 = $080F INY = $0810 JMP $080F - = $0813 RTS The body of `repeat forever` can be empty. @@ -620,7 +647,6 @@ The body of `repeat forever` can be empty. | } forever | } = $080D JMP $080D - = $0810 RTS Compiling `for ... up to`. @@ -1055,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 @@ -1076,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`. @@ -1139,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. @@ -1190,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 @@ -1697,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. @@ -1730,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 @@ -1763,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 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 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 { diff --git a/tests/appliances/sixtypical.md b/tests/appliances/sixtypical.md index 1ee2bc4..34bbf8d 100644 --- a/tests/appliances/sixtypical.md +++ b/tests/appliances/sixtypical.md @@ -13,6 +13,12 @@ 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 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)"