Merge pull request #22 from catseye/develop-0.21

Develop 0.21
This commit is contained in:
Chris Pressey 2019-10-25 12:51:24 +01:00 committed by GitHub
commit 4b539930bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 806 additions and 313 deletions

27
.circleci/config.yml Normal file
View File

@ -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

View File

@ -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
----

View File

@ -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

57
TODO.md
View File

@ -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
-----------

View File

@ -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:])

View File

@ -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

View File

@ -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/

View File

@ -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
}

23
eg/c64/demo-game/run.sh Executable file
View File

@ -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

15
eg/c64/joystick-demo.60p Normal file
View File

@ -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
}

View File

@ -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,

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print YY
include "chrout.60p"
word score
define main routine

View File

@ -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

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print AA
include "chrout.60p"
define print routine
trashes a, z, n
{

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print ENGGL
include "chrout.60p"
byte b
define main routine

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print ENGGL
include "chrout.60p"
word w1
define main routine

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print ENGGL
include "chrout.60p"
word w1
word w2

View File

@ -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
{

View File

@ -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
{

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
{

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print AB
include "chrout.60p"
byte foo
define print routine

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print AB
include "chrout.60p"
vector routine
trashes a, z, n
print

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print YY
include "chrout.60p"
word one
word table[256] many

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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)
])

View File

@ -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)

View File

@ -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

View File

@ -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."""

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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 </tmp/foo"
-> 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 </tmp/foo"
-> 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)"