1
0
mirror of https://github.com/catseye/SixtyPical.git synced 2025-04-06 17:39:38 +00:00

Merge pull request #11 from catseye/develop-0.15

Develop 0.15
This commit is contained in:
Chris Pressey 2018-04-10 10:27:49 +01:00 committed by GitHub
commit 9b53ed03c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1917 additions and 228 deletions

View File

@ -1,6 +1,27 @@
History of SixtyPical
=====================
0.15
----
* Symbolic constants can be defined with the `const` keyword, and can
be used in most places where literal values can be used.
* Added `nop` opcode, which compiles to `NOP` (mainly for timing.)
* Accessing zero-page with `ld` and `st` generates zero-page opcodes.
* A `byte` or `word` table can be initialized with a list of constants.
* Branching and repeating on the `n` flag is now supported.
* The `--optimize-fallthru` option causes the routines of the program
to be re-ordered to maximize the number of cases where a `goto`'ed
routine can be simply "falled through" to instead of `JMP`ed to.
* `--dump-fallthru-info` option outputs the information from the
fallthru analysis phase, in JSON format, to stdout.
* Even without fallthru optimization, `RTS` is no longer emitted after
the `JMP` from compiling a final `goto`.
* Specifying multiple SixtyPical source files will produce a single
compiled result from their combination.
* Rudimentary support for Atari 2600 prelude in a 4K cartridge image,
and an example program in `eg/atari2600` directory.
0.14
----

View File

@ -1,7 +1,7 @@
SixtyPical
==========
_Version 0.14. Work-in-progress, everything is subject to change._
_Version 0.15. Work-in-progress, everything is subject to change._
**SixtyPical** is a 6502-like programming language with advanced
static analysis.
@ -63,6 +63,7 @@ Documentation
* [Literate test suite for SixtyPical syntax](tests/SixtyPical%20Syntax.md)
* [Literate test suite for SixtyPical analysis](tests/SixtyPical%20Analysis.md)
* [Literate test suite for SixtyPical compilation](tests/SixtyPical%20Compilation.md)
* [Literate test suite for SixtyPical fallthru optimization](tests/SixtyPical%20Fallthru.md)
* [6502 Opcodes used/not used in SixtyPical](doc/6502%20Opcodes.md)
TODO
@ -73,19 +74,13 @@ TODO
This preserves them, so that, semantically, they can be used later even though they
are trashed inside the block.
### Re-order routines and optimize tail-calls to fallthroughs
Not because it saves 3 bytes, but because it's a neat trick. Doing it optimally
is probably NP-complete. But doing it adequately is probably not that hard.
### And at some point...
* `low` and `high` address operators - to turn `word` type into `byte`.
* `const`s that can be used in defining the size of tables, etc.
* Tests, and implementation, ensuring a routine can be assigned to a vector of "wider" type
* Related: can we simply view a (small) part of a buffer as a byte table? If not, why not?
* Related: add constant to buffer to get new buffer. (Or to table, but... well, maybe.)
* Check that the buffer being read or written to through pointer, appears in approporiate inputs or outputs set.
* Check that the buffer being read or written to through pointer, appears in appropriate inputs or outputs set.
(Associate each pointer with the buffer it points into.)
* `static` pointers -- currently not possible because pointers must be zero-page, thus `@`, thus uninitialized.
* Question the value of the "consistent initialization" principle for `if` statement analysis.
@ -94,7 +89,6 @@ is probably NP-complete. But doing it adequately is probably not that hard.
* Automatic tail-call optimization (could be tricky, w/constraints?)
* Possibly `ld x, [ptr] + y`, possibly `st x, [ptr] + y`.
* Maybe even `copy [ptra] + y, [ptrb] + y`, which can be compiled to indirect LDA then indirect STA!
* Optimize `ld a, z` and `st a, z` to zero-page operations if address of z < 256.
* Include files?
* Optimize `or|and|eor a, z` to zero-page operations if address of z < 256.
[VICE]: http://vice-emu.sourceforge.net/

View File

@ -18,12 +18,120 @@ from pprint import pprint
import sys
import traceback
from sixtypical.parser import Parser
from sixtypical.parser import Parser, ParsingContext
from sixtypical.analyzer import Analyzer
from sixtypical.emitter import Emitter, Byte, Word
from sixtypical.compiler import Compiler
def merge_programs(programs):
"""Assumes that the programs do not have any conflicts."""
from sixtypical.ast import Program
full = Program(1, defns=[], routines=[])
for p in programs:
full.defns.extend(p.defns)
full.routines.extend(p.routines)
return full
def process_input_files(filenames, options):
context = ParsingContext()
programs = []
for filename in options.filenames:
text = open(filename).read()
parser = Parser(context, text, filename)
if options.debug:
print(context)
program = parser.program()
programs.append(program)
if options.parse_only:
return
program = merge_programs(programs)
analyzer = Analyzer(debug=options.debug)
analyzer.analyze_program(program)
compilation_roster = None
if options.optimize_fallthru:
from sixtypical.fallthru import FallthruAnalyzer
def dump(data, label=None):
import json
if not options.dump_fallthru_info:
return
if label:
sys.stdout.write("*** {}:\n".format(label))
sys.stdout.write(json.dumps(data, indent=4, sort_keys=True))
sys.stdout.write("\n")
fa = FallthruAnalyzer(debug=options.debug)
fa.analyze_program(program)
compilation_roster = fa.serialize()
dump(compilation_roster)
if options.analyze_only:
return
fh = sys.stdout
if options.origin.startswith('0x'):
start_addr = int(options.origin, 16)
else:
start_addr = int(options.origin, 10)
output_format = options.output_format
prelude = []
if options.prelude == 'c64':
output_format = 'prg'
start_addr = 0x0801
prelude = [0x10, 0x08, 0xc9, 0x07, 0x9e, 0x32,
0x30, 0x36, 0x31, 0x00, 0x00, 0x00]
elif options.prelude == 'vic20':
output_format = 'prg'
start_addr = 0x1001
prelude = [0x0b, 0x10, 0xc9, 0x07, 0x9e, 0x34,
0x31, 0x30, 0x39, 0x00, 0x00, 0x00]
elif options.prelude == 'atari2600':
output_format = 'crtbb'
start_addr = 0xf000
prelude = [0x78, 0xd8, 0xa2, 0xff, 0x9a, 0xa9,
0x00,0x95, 0x00, 0xca, 0xd0, 0xfb]
elif options.prelude:
raise NotImplementedError("Unknown prelude: {}".format(options.prelude))
# If we are outputting a .PRG, we output the load address first.
# We don't use the Emitter for this b/c not part of addr space.
if output_format == 'prg':
fh.write(Word(start_addr).serialize(0))
emitter = Emitter(start_addr)
for byte in prelude:
emitter.emit(Byte(byte))
compiler = Compiler(emitter)
compiler.compile_program(program, compilation_roster=compilation_roster)
# If we are outputting a cartridge with boot and BRK address
# at the end, pad to ROM size minus 4 bytes, and emit addresses.
if output_format == 'crtbb':
emitter.pad_to_size(4096 - 4)
emitter.emit(Word(start_addr))
emitter.emit(Word(start_addr))
if options.debug:
pprint(emitter.accum)
else:
emitter.serialize(fh)
if __name__ == '__main__':
argparser = ArgumentParser(__doc__.strip())
@ -31,11 +139,7 @@ if __name__ == '__main__':
'filenames', metavar='FILENAME', type=str, nargs='+',
help="The SixtyPical source files to compile."
)
argparser.add_argument(
"--analyze-only",
action="store_true",
help="Only parse and analyze the program; do not compile it."
)
argparser.add_argument(
"--origin", type=str, default='0xc000',
help="Location in memory where the `main` routine will be "
@ -43,26 +147,43 @@ if __name__ == '__main__':
)
argparser.add_argument(
"--output-format", type=str, default='prg',
help="Executable format to produce. Options are: prg (.PRG file "
"for Commodore 8-bit). Default: prg."
help="Executable format to produce. Options are: prg, crtbb. "
"Default: prg."
)
argparser.add_argument(
"--prelude", type=str,
help="Insert a snippet before the compiled program "
"so that it can be LOADed and RUN on a certain platforms. "
help="Insert a snippet of code before the compiled program so that "
"it can be booted automatically on a particular platform. "
"Also sets the origin and format. "
"Options are: c64 or vic20."
"Options are: c64, vic20, atari2600."
)
argparser.add_argument(
"--analyze-only",
action="store_true",
help="Only parse and analyze the program; do not compile it."
)
argparser.add_argument(
"--debug",
"--optimize-fallthru",
action="store_true",
help="Display debugging information when analyzing and compiling."
help="Reorder the routines in the program to maximize the number of tail calls "
"that can be removed by having execution 'fall through' to the next routine."
)
argparser.add_argument(
"--dump-fallthru-info",
action="store_true",
help="Dump the fallthru map and ordering to stdout after analyzing the program."
)
argparser.add_argument(
"--parse-only",
action="store_true",
help="Only parse the program; do not analyze or compile it."
)
argparser.add_argument(
"--debug",
action="store_true",
help="Display debugging information when analyzing and compiling."
)
argparser.add_argument(
"--traceback",
action="store_true",
@ -72,69 +193,11 @@ if __name__ == '__main__':
options, unknown = argparser.parse_known_args(sys.argv[1:])
remainder = ' '.join(unknown)
for filename in options.filenames:
text = open(filename).read()
try:
parser = Parser(text)
program = parser.program()
except Exception as e:
if options.traceback:
raise
else:
traceback.print_exception(e.__class__, e, None)
sys.exit(1)
if options.parse_only:
sys.exit(0)
try:
analyzer = Analyzer(debug=options.debug)
analyzer.analyze_program(program)
except Exception as e:
if options.traceback:
raise
else:
traceback.print_exception(e.__class__, e, None)
sys.exit(1)
if options.analyze_only:
sys.exit(0)
fh = sys.stdout
if options.origin.startswith('0x'):
start_addr = int(options.origin, 16)
try:
process_input_files(options.filenames, options)
except Exception as e:
if options.traceback:
raise
else:
start_addr = int(options.origin, 10)
output_format = options.output_format
prelude = []
if options.prelude == 'c64':
output_format = 'prg'
start_addr = 0x0801
prelude = [0x10, 0x08, 0xc9, 0x07, 0x9e, 0x32,
0x30, 0x36, 0x31, 0x00, 0x00, 0x00]
elif options.prelude == 'vic20':
output_format = 'prg'
start_addr = 0x1001
prelude = [0x0b, 0x10, 0xc9, 0x07, 0x9e, 0x34,
0x31, 0x30, 0x39, 0x00, 0x00, 0x00]
elif options.prelude:
raise NotImplementedError("Unknown prelude: {}".format(options.prelude))
# If we are outputting a .PRG, we output the load address first.
# We don't use the Emitter for this b/c not part of addr space.
if output_format == 'prg':
fh.write(Word(start_addr).serialize(0))
emitter = Emitter(start_addr)
for byte in prelude:
emitter.emit(Byte(byte))
compiler = Compiler(emitter)
compiler.compile_program(program)
if options.debug:
pprint(emitter.accum)
else:
emitter.serialize(fh)
traceback.print_exception(e.__class__, e, None)
sys.exit(1)

View File

@ -1,7 +1,7 @@
SixtyPical
==========
This document describes the SixtyPical programming language version 0.14,
This document describes the SixtyPical programming language version 0.15,
both its static semantics (the capabilities and limits of the static
analyses it defines) and its runtime semantics (with reference to the
semantics of 6502 machine code.)
@ -555,9 +555,10 @@ The block is always executed as least once.
Grammar
-------
Program ::= {TypeDefn} {Defn} {Routine}.
Program ::= {ConstDefn | TypeDefn} {Defn} {Routine}.
ConstDefn::= "const" Ident<new> Const.
TypeDefn::= "typedef" Type Ident<new>.
Defn ::= Type Ident<new> [Constraints] (":" Literal | "@" LitWord).
Defn ::= Type Ident<new> [Constraints] (":" Const | "@" LitWord).
Type ::= TypeTerm ["table" TypeSize].
TypeExpr::= "byte"
| "word"
@ -573,12 +574,13 @@ Grammar
| "routine" Ident<new> Constraints (Block | "@" LitWord)
.
LocExprs::= LocExpr {"," LocExpr}.
LocExpr ::= Register | Flag | Literal | Ident.
LocExpr ::= Register | Flag | Const | Ident.
Register::= "a" | "x" | "y".
Flag ::= "c" | "z" | "n" | "v".
Const ::= Literal | Ident<const>.
Literal ::= LitByte | LitWord | LitBit.
LitByte ::= "0" ... "255".
LitWord ::= "0" ... "65535".
LitWord ::= ["word"] "0" ... "65535".
LitBit ::= "on" | "off".
Block ::= "{" {Instr} "}".
Instr ::= "ld" LocExpr "," LocExpr ["+" LocExpr]
@ -598,6 +600,6 @@ Grammar
| "copy" LocExpr "," LocExpr ["+" LocExpr]
| "if" ["not"] LocExpr Block ["else" Block]
| "repeat" Block ("until" ["not"] LocExpr | "forever")
| "for" LocExpr ("up"|"down") "to" Literal Block
| "for" LocExpr ("up"|"down") "to" Const Block
| "with" "interrupts" LitBit Block
.

View File

@ -30,7 +30,8 @@ elaborate demos:
the P65 assembler (now Ophis) and re-released on April 1st, 2008 (a
hint as to its nature).
Translated to SixtyPical (in 2018), it's 48 bytes.
Translated to SixtyPical (in 2018), after adding some optimizations
to the SixtyPical compiler, the resulting executable is still 44 bytes!
### vic20
@ -38,4 +39,11 @@ In the [vic20](vic20/) directory are programs that run on the
Commodore VIC-20. The directory itself contains some simple demos,
for example [hearts.60p](vic20/hearts.60p).
### atari2600
In the [atari2600](atari2600/) directory are programs that run on the
Atari 2600 (4K cartridge). The directory itself contains a simple
demo, [smiley.60p](atari2600/smiley.60p) which was converted from an
older Atari 2600 skeleton program written in [Ophis][].
[Ophis]: http://michaelcmartin.github.io/Ophis/

2
eg/atari2600/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.bin
*.disasm.txt

View File

@ -0,0 +1,340 @@
;
; atari-2600-example.oph
; Skeleton code for an Atari 2600 ROM,
; plus an example of reading the joystick.
; By Chris Pressey, November 2, 2012.
;
; This work is in the public domain. See the file UNLICENSE for more info.
;
; Based on Chris Cracknell's Atari 2600 clock (also in the public domain):
; http://everything2.com/title/An+example+of+Atari+2600+source+code
;
; to build and run in Stella:
; ophis atari-2600-example.oph -o example.bin
; stella example.bin
;
; More useful information can be found in the Stella Programmer's Guide:
; http://alienbill.com/2600/101/docs/stella.html
;
;
; Useful system addresses (TODO: briefly describe each of these.)
;
.alias VSYNC $00
.alias VBLANK $01
.alias WSYNC $02
.alias NUSIZ0 $04
.alias NUSIZ1 $05
.alias COLUPF $08
.alias COLUBK $09
.alias PF0 $0D
.alias PF1 $0E
.alias PF2 $0F
.alias SWCHA $280
.alias INTIM $284
.alias TIM64T $296
.alias CTRLPF $0A
.alias COLUP0 $06
.alias COLUP1 $07
.alias GP0 $1B
.alias GP1 $1C
.alias HMOVE $2a
.alias RESP0 $10
.alias RESP1 $11
;
; Cartridge ROM occupies the top 4K of memory ($F000-$FFFF).
; Thus, typically, the program will occupy all that space too.
;
; Zero-page RAM we can use with impunity starts at $80 and goes
; upward (at least until $99, but probably further.)
;
.alias colour $80
.alias luminosity $81
.alias joystick_delay $82
.org $F000
;
; Standard prelude for Atari 2600 cartridge code.
;
; Get various parts of the machine into a known state:
;
; - Disable interrupts
; - Clear the Decimal flag
; - Initialize the Stack Pointer
; - Zero all bytes in Zero Page memory
;
start:
sei
cld
ldx #$FF
txs
lda #$00
zero_loop:
sta $00, x
dex
bne zero_loop
; and fall through to...
;
; Initialization.
;
; - Clear the Playfield Control register.
; - Set the player (sprite) colour to light green (write to COLUP0.)
; - Set the player (sprite) size/repetion to normal (write to NUSIZ0.)
;
lda #$00
sta CTRLPF
lda #$0c
sta colour
lda #$0a
sta luminosity
lda #$00
sta NUSIZ0
; and fall through to...
;
; Main loop.
;
; A typical main loop consists of:
; - Waiting for the frame to start (vertical blank period)
; - Displaying stuff on the screen (the _display kernel_)
; - Doing any processing you like (reading joysticks, updating program state,
; etc.), as long as you get it all done before the next frame starts!
;
main:
jsr vertical_blank
jsr display_frame
jsr read_joystick
jmp main
;
; Vertical blank routine.
;
; In brief: wait until it is time for the next frame of video.
; TODO: describe this in more detail.
;
vertical_blank:
ldx #$00
lda #$02
sta WSYNC
sta WSYNC
sta WSYNC
sta VSYNC
sta WSYNC
sta WSYNC
lda #$2C
sta TIM64T
lda #$00
sta WSYNC
sta VSYNC
rts
;
; Display kernal.
;
; First, wait until it's time to display the frame.
;
.scope
display_frame:
lda INTIM
bne display_frame
;
; (After that loop finishes, we know the accumulator must contain 0.)
; Wait for the next scanline, zero HMOVE (for some reason; TODO discover
; this), then turn on the screen.
;
sta WSYNC
sta HMOVE
sta VBLANK
;
; Actual work in the display kernal is done here.
;
; This is a pathological approach to writing a display kernal.
; This wouldn't be how you'd do things in a game. So be it.
; One day I may improve it. For now, be happy that it displays
; anything at all!
;
;
; Wait for $3f (plus one?) scan lines to pass, by waiting for
; WSYNC that many times.
;
ldx #$3F
_wsync_loop:
sta WSYNC
dex
bpl _wsync_loop
sta WSYNC
;
; Delay while the raster scans across the screen. The more
; we delay here, the more to the right the player will be when
; we draw it.
;
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
;
; OK, *now* display the player.
;
sta RESP0
;
; Loop over the rows of the sprite data, drawing each to the screen
; over four scan lines.
;
; TODO understand this better and describe it!
;
ldy #$07
_image_loop:
lda image_data, y
sta GP0
sta WSYNC
sta WSYNC
sta WSYNC
sta WSYNC
dey
bpl _image_loop
lda #$00
sta GP0
;
; Turn off screen display and clear display registers.
;
lda #$02
sta WSYNC
sta VBLANK
lda #$00
sta PF0
sta PF1
sta PF2
sta COLUPF
sta COLUBK
rts
.scend
;
; Read the joystick and use it to modify the colour and luminosity
; of the player.
;
.scope
read_joystick:
lda joystick_delay
beq _continue
dec joystick_delay
rts
_continue:
lda SWCHA
and #$f0
cmp #$e0
beq _up
cmp #$d0
beq _down
cmp #$b0
beq _left
cmp #$70
beq _right
jmp _tail
_up:
inc luminosity
jmp _tail
_down:
dec luminosity
jmp _tail
_left:
dec colour
jmp _tail
_right:
inc colour
;jmp _tail
_tail:
lda colour
and #$0f
sta colour
lda luminosity
and #$0f
sta luminosity
lda colour
clc
rol
rol
rol
rol
ora luminosity
sta COLUP0
lda #$06
sta joystick_delay
rts
.scend
;
; Player (sprite) data.
;
; Because we loop over these bytes with the Y register counting *down*,
; this image is stored "upside-down".
;
image_data:
.byte %01111110
.byte %10000001
.byte %10011001
.byte %10100101
.byte %10000001
.byte %10100101
.byte %10000001
.byte %01111110
;
; Standard postlude for Atari 2600 cartridge code.
; Give BRK and boot vectors that point to the start of the code.
;
.advance $FFFC
.word start
.word start

10
eg/atari2600/build.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
sixtypical --prelude=atari2600 smiley.60p > smiley-60p.bin
if [ "x$COMPARE" != "x" ]; then
ophis smiley.oph -o smiley.bin
dcc6502 -o 0xf000 -m 200 smiley.bin > smiley.bin.disasm.txt
dcc6502 -o 0xf000 -m 200 smiley-60p.bin > smiley-60p.bin.disasm.txt
paste smiley.bin.disasm.txt smiley-60p.bin.disasm.txt | pr -t -e24
#diff -ru smiley.bin.disasm.txt smiley-60p.bin.disasm.txt
fi

184
eg/atari2600/smiley.60p Normal file
View File

@ -0,0 +1,184 @@
// smiley.60p - SixtyPical translation of smiley.oph (2018),
// which is itself a stripped-down version of atari-2600-example.oph
byte VSYNC @ $00
byte VBLANK @ $01
byte WSYNC @ $02
byte NUSIZ0 @ $04
byte NUSIZ1 @ $05
byte COLUPF @ $08
byte COLUBK @ $09
byte PF0 @ $0D
byte PF1 @ $0E
byte PF2 @ $0F
byte SWCHA @ $280
byte INTIM @ $284
byte TIM64T @ $296
byte CTRLPF @ $0A
byte COLUP0 @ $06
byte COLUP1 @ $07
byte GP0 @ $1B
byte GP1 @ $1C
byte HMOVE @ $2a
byte RESP0 @ $10
byte RESP1 @ $11
byte colour @ $80
byte luminosity @ $81
byte joystick_delay @ $82
byte table[8] image_data : 126, 129, 153, 165, 129, 165, 129, 126
// %01111110
// %10000001
// %10011001
// %10100101
// %10000001
// %10100101
// %10000001
// %01111110
define vertical_blank routine
outputs VSYNC, WSYNC, TIM64T
trashes a, x, z, n
{
ld x, $00
ld a, $02
st a, WSYNC
st a, WSYNC
st a, WSYNC
st a, VSYNC
st a, WSYNC
st a, WSYNC
ld a, $2C
st a, TIM64T
ld a, $00
st a, WSYNC
st a, VSYNC
}
define display_frame routine
inputs INTIM, image_data
outputs WSYNC, HMOVE, VBLANK, RESP0, GP0, PF0, PF1, PF2, COLUPF, COLUBK
trashes a, x, y, z, n
{
repeat {
ld a, INTIM
} until z
//; (After that loop finishes, we know the accumulator must contain 0.)
st a, WSYNC
st a, HMOVE
st a, VBLANK
//;
//; Wait for $3f (plus one?) scan lines to pass, by waiting for
//; WSYNC that many times.
//;
ld x, $3F
repeat {
st a, WSYNC
dec x
} until n
st a, WSYNC
//;
//; Delay while the raster scans across the screen. The more
//; we delay here, the more to the right the player will be when
//; we draw it.
//;
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
//;
//; OK, *now* display the player.
//;
st a, RESP0
//;
//; Loop over the rows of the sprite data, drawing each to the screen
//; over four scan lines.
//;
//; TODO understand this better and describe it!
//;
ld y, $07
for y down to 0 {
ld a, image_data + y
st a, GP0
st a, WSYNC
st a, WSYNC
st a, WSYNC
st a, WSYNC
} // FIXME original was "dec y; bpl _image_loop"
ld a, $00
st a, GP0
//;
//; Turn off screen display and clear display registers.
//;
ld a, $02
st a, WSYNC
st a, VBLANK
ld a, $00
st a, PF0
st a, PF1
st a, PF2
st a, COLUPF
st a, COLUBK
}
define colourize_player routine
inputs colour, luminosity
outputs COLUP0
trashes a, z, c, n
{
ld a, colour
st off, c
shl a
shl a
shl a
shl a
or a, luminosity
st a, COLUP0
}
define main routine
inputs image_data, INTIM
outputs CTRLPF, colour, luminosity, NUSIZ0, VSYNC, WSYNC, TIM64T, HMOVE, VBLANK, RESP0, GP0, PF0, PF1, PF2, COLUPF, COLUBK, COLUP0
trashes a, x, y, z, c, n
{
ld a, $00
st a, CTRLPF
ld a, $0c
st a, colour
ld a, $0a
st a, luminosity
ld a, $00
st a, NUSIZ0
repeat {
call vertical_blank
call display_frame
call colourize_player
} forever
}

285
eg/atari2600/smiley.oph Normal file
View File

@ -0,0 +1,285 @@
;
; smiley.oph (2018)
; stripped-down version of atari-2600-example.oph (2012)
;
; This work is in the public domain. See the file UNLICENSE for more info.
;
; to build and run in Stella:
; ophis smiley.oph -o smiley.bin
; stella smiley.bin
;
; Useful system addresses (TODO: briefly describe each of these.)
;
.alias VSYNC $00
.alias VBLANK $01
.alias WSYNC $02
.alias NUSIZ0 $04
.alias NUSIZ1 $05
.alias COLUPF $08
.alias COLUBK $09
.alias PF0 $0D
.alias PF1 $0E
.alias PF2 $0F
.alias SWCHA $280
.alias INTIM $284
.alias TIM64T $296
.alias CTRLPF $0A
.alias COLUP0 $06
.alias COLUP1 $07
.alias GP0 $1B
.alias GP1 $1C
.alias HMOVE $2a
.alias RESP0 $10
.alias RESP1 $11
;
; Cartridge ROM occupies the top 4K of memory ($F000-$FFFF).
; Thus, typically, the program will occupy all that space too.
;
; Zero-page RAM we can use with impunity starts at $80 and goes
; upward (at least until $99, but probably further.)
;
.alias colour $80
.alias luminosity $81
.alias joystick_delay $82
.org $F000
;
; Standard prelude for Atari 2600 cartridge code.
;
; Get various parts of the machine into a known state:
;
; - Disable interrupts
; - Clear the Decimal flag
; - Initialize the Stack Pointer
; - Zero all bytes in Zero Page memory
;
start:
sei
cld
ldx #$FF
txs
lda #$00
zero_loop:
sta $00, x
dex
bne zero_loop
; and fall through to...
;
; Initialization.
;
; - Clear the Playfield Control register.
; - Set the player (sprite) colour to light green (write to COLUP0.)
; - Set the player (sprite) size/repetion to normal (write to NUSIZ0.)
;
lda #$00
sta CTRLPF
lda #$0c
sta colour
lda #$0a
sta luminosity
lda #$00
sta NUSIZ0
; and fall through to...
;
; Main loop.
;
; A typical main loop consists of:
; - Waiting for the frame to start (vertical blank period)
; - Displaying stuff on the screen (the _display kernel_)
; - Doing any processing you like (reading joysticks, updating program state,
; etc.), as long as you get it all done before the next frame starts!
;
main:
jsr vertical_blank
jsr display_frame
jsr colourize_player
jmp main
rts ; NOTE just to pad out to match the SixtyPical version
;
; Vertical blank routine.
;
; In brief: wait until it is time for the next frame of video.
; TODO: describe this in more detail.
;
vertical_blank:
ldx #$00
lda #$02
sta WSYNC
sta WSYNC
sta WSYNC
sta VSYNC
sta WSYNC
sta WSYNC
lda #$2C
sta TIM64T
lda #$00
sta WSYNC
sta VSYNC
rts
;
; Display kernal.
;
; First, wait until it's time to display the frame.
;
.scope
display_frame:
lda INTIM
bne display_frame
;
; (After that loop finishes, we know the accumulator must contain 0.)
; Wait for the next scanline, zero HMOVE (for some reason; TODO discover
; this), then turn on the screen.
;
sta WSYNC
sta HMOVE
sta VBLANK
;
; Actual work in the display kernal is done here.
;
; This is a pathological approach to writing a display kernal.
; This wouldn't be how you'd do things in a game. So be it.
; One day I may improve it. For now, be happy that it displays
; anything at all!
;
;
; Wait for $3f (plus one?) scan lines to pass, by waiting for
; WSYNC that many times.
;
ldx #$3F
_wsync_loop:
sta WSYNC
dex
bpl _wsync_loop
sta WSYNC
;
; Delay while the raster scans across the screen. The more
; we delay here, the more to the right the player will be when
; we draw it.
;
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
;
; OK, *now* display the player.
;
sta RESP0
;
; Loop over the rows of the sprite data, drawing each to the screen
; over four scan lines.
;
; TODO understand this better and describe it!
;
ldy #$07
_image_loop:
lda image_data, y
sta GP0
sta WSYNC
sta WSYNC
sta WSYNC
sta WSYNC
dey
bpl _image_loop
lda #$00
sta GP0
;
; Turn off screen display and clear display registers.
;
lda #$02
sta WSYNC
sta VBLANK
lda #$00
sta PF0
sta PF1
sta PF2
sta COLUPF
sta COLUBK
rts
.scend
;
; Modify the colour and luminosity of the player.
;
.scope
colourize_player:
lda colour
clc
rol
rol
rol
rol
ora luminosity
sta COLUP0
rts
.scend
;
; Player (sprite) data.
;
; Because we loop over these bytes with the Y register counting *down*,
; this image is stored "upside-down".
;
image_data:
.byte %01111110
.byte %10000001
.byte %10011001
.byte %10100101
.byte %10000001
.byte %10100101
.byte %10000001
.byte %01111110
;
; Standard postlude for Atari 2600 cartridge code.
; Give BRK and boot vectors that point to the start of the code.
;
.advance $FFFC
.word start
.word start

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,21 @@
// This will not compile on its own, because there is no `main`.
// But this and `vector-main.60p` together will compile.
routine chrout
inputs a
trashes a
@ 65490
routine printa
trashes a, z, n
{
ld a, 65
call chrout
}
routine printb
trashes a, z, n
{
ld a, 66
call chrout
}

View File

@ -0,0 +1,22 @@
// This will not compile on its own, because `printa` and `printb` are not defined.
// But `vector-inc.60p` and this together will compile.
vector routine
trashes a, z, n
print
// routine printb
// trashes a, z, n
// {
// ld a, 66
// call chrout
// }
routine main
trashes print, a, z, n
{
copy printa, print
call print
copy printb, print
call print
}

View File

@ -1,6 +1,6 @@
#!/bin/sh
usage="Usage: loadngo.sh (c64|vic20) [--dry-run] <source.60p>"
usage="Usage: loadngo.sh (c64|vic20|atari2600) [--dry-run] <source.60p>"
arch="$1"
shift 1
@ -18,6 +18,9 @@ elif [ "X$arch" = "Xvic20" ]; then
else
emu="xvic"
fi
elif [ "X$arch" = "Xatari2600" ]; then
prelude='atari2600'
emu='stella'
else
echo $usage && exit 1
fi

View File

@ -101,7 +101,7 @@ class Context(object):
self._touched = set()
self._range = dict()
self._writeable = set()
self._has_encountered_goto = False
self._gotos_encountered = set()
for ref in inputs:
if ref.is_constant():
@ -273,11 +273,11 @@ class Context(object):
for ref in refs:
self._writeable.add(ref)
def set_encountered_goto(self):
self._has_encountered_goto = True
def encounter_gotos(self, gotos):
self._gotos_encountered |= gotos
def has_encountered_goto(self):
return self._has_encountered_goto
def encountered_gotos(self):
return self._gotos_encountered
class Analyzer(object):
@ -311,7 +311,8 @@ class Analyzer(object):
assert isinstance(program, Program)
self.routines = {r.location: r for r in program.routines}
for routine in program.routines:
self.analyze_routine(routine)
context = self.analyze_routine(routine)
routine.encountered_gotos = list(context.encountered_gotos()) if context else []
def analyze_routine(self, routine):
assert isinstance(routine, Routine)
@ -346,13 +347,14 @@ class Analyzer(object):
if ref in type_.outputs:
raise UnmeaningfulOutputError(routine, ref.name)
if not context.has_encountered_goto():
if not context.encountered_gotos():
for ref in type_.outputs:
context.assert_meaningful(ref, exception_class=UnmeaningfulOutputError)
for ref in context.each_touched():
if ref not in type_.outputs and ref not in type_.trashes and not routine_has_static(routine, ref):
raise ForbiddenWriteError(routine, ref.name)
self.current_routine = None
return context
def analyze_block(self, block, context):
assert isinstance(block, Block)
@ -379,7 +381,7 @@ class Analyzer(object):
dest = instr.dest
src = instr.src
if context.has_encountered_goto():
if context.encountered_gotos():
raise IllegalJumpError(instr, instr)
if opcode == 'ld':
@ -595,10 +597,12 @@ class Analyzer(object):
self.assert_affected_within('outputs', type_, current_type)
self.assert_affected_within('trashes', type_, current_type)
context.set_encountered_goto()
context.encounter_gotos(set([instr.location]))
elif opcode == 'trash':
context.set_touched(instr.dest)
context.set_unmeaningful(instr.dest)
elif opcode == 'nop':
pass
else:
raise NotImplementedError(opcode)
@ -636,8 +640,7 @@ class Analyzer(object):
context._touched = set(context1._touched) | set(context2._touched)
context.set_meaningful(*list(outgoing_meaningful))
context._writeable = set(context1._writeable) | set(context2._writeable)
if context1.has_encountered_goto() or context2.has_encountered_goto():
context.set_encountered_goto()
context.encounter_gotos(context1.encountered_gotos() | context2.encountered_gotos())
for ref in outgoing_trashes:
context.set_touched(ref)

View File

@ -15,9 +15,10 @@ from sixtypical.gen6502 import (
CLC, SEC, ADC, SBC, ROL, ROR,
INC, INX, INY, DEC, DEX, DEY,
CMP, CPX, CPY, AND, ORA, EOR,
BCC, BCS, BNE, BEQ,
BCC, BCS, BNE, BEQ, BPL, BMI,
JMP, JSR, RTS,
SEI, CLI,
NOP,
)
@ -66,9 +67,15 @@ class Compiler(object):
return static_label
return self.labels[name]
def absolute_or_zero_page(self, label):
if label.addr is not None and label.addr < 256:
return ZeroPage(label)
else:
return Absolute(label)
# visitor methods
def compile_program(self, program):
def compile_program(self, program, compilation_roster=None):
assert isinstance(program, Program)
defn_labels = []
@ -95,10 +102,14 @@ class Compiler(object):
defn_labels.append((defn, label))
self.routine_statics[routine.name] = static_labels
self.compile_routine(self.routines['main'])
for routine in program.routines:
if routine.name != 'main':
self.compile_routine(routine)
if compilation_roster is None:
compilation_roster = [['main']] + [[routine.name] for routine in program.routines if routine.name != 'main']
for roster_row in compilation_roster:
for routine_name in roster_row[0:-1]:
self.compile_routine(self.routines[routine_name], skip_final_goto=True)
routine_name = roster_row[-1]
self.compile_routine(self.routines[routine_name])
for location, label in self.trampolines.iteritems():
self.emitter.resolve_label(label)
@ -115,7 +126,9 @@ class Compiler(object):
elif type_ == TYPE_WORD:
initial_data = Word(defn.initial)
elif TableType.is_a_table_type(type_, TYPE_BYTE):
initial_data = Table(defn.initial, type_.size)
initial_data = Table([Byte(i) for i in defn.initial], type_.size)
elif TableType.is_a_table_type(type_, TYPE_WORD):
initial_data = Table([Word(i) for i in defn.initial], type_.size)
else:
raise NotImplementedError(type_)
label.set_length(initial_data.size())
@ -127,14 +140,18 @@ class Compiler(object):
if defn.initial is None and defn.addr is None:
self.emitter.resolve_bss_label(label)
def compile_routine(self, routine):
def compile_routine(self, routine, skip_final_goto=False):
self.current_routine = routine
self.skip_final_goto = skip_final_goto
self.final_goto_seen = False
assert isinstance(routine, Routine)
if routine.block:
self.emitter.resolve_label(self.get_label(routine.name))
self.compile_block(routine.block)
self.emitter.emit(RTS())
if not self.final_goto_seen:
self.emitter.emit(RTS())
self.current_routine = None
self.skip_final_goto = False
def compile_block(self, block):
assert isinstance(block, Block)
@ -176,7 +193,7 @@ class Compiler(object):
elif isinstance(src, IndirectRef) and isinstance(src.ref.type, PointerType):
self.emitter.emit(LDA(IndirectY(self.get_label(src.ref.name))))
else:
self.emitter.emit(LDA(Absolute(self.get_label(src.name))))
self.emitter.emit(LDA(self.absolute_or_zero_page(self.get_label(src.name))))
elif dest == REG_X:
if src == REG_A:
self.emitter.emit(TAX())
@ -185,7 +202,7 @@ class Compiler(object):
elif isinstance(src, IndexedRef) and src.index == REG_Y:
self.emitter.emit(LDX(AbsoluteY(self.get_label(src.ref.name))))
else:
self.emitter.emit(LDX(Absolute(self.get_label(src.name))))
self.emitter.emit(LDX(self.absolute_or_zero_page(self.get_label(src.name))))
elif dest == REG_Y:
if src == REG_A:
self.emitter.emit(TAY())
@ -194,7 +211,7 @@ class Compiler(object):
elif isinstance(src, IndexedRef) and src.index == REG_X:
self.emitter.emit(LDY(AbsoluteX(self.get_label(src.ref.name))))
else:
self.emitter.emit(LDY(Absolute(self.get_label(src.name))))
self.emitter.emit(LDY(self.absolute_or_zero_page(self.get_label(src.name))))
else:
raise UnsupportedOpcodeError(instr)
elif opcode == 'st':
@ -214,17 +231,15 @@ class Compiler(object):
REG_X: AbsoluteX,
REG_Y: AbsoluteY,
}[dest.index]
label = self.get_label(dest.ref.name)
operand = mode_cls(self.get_label(dest.ref.name))
elif isinstance(dest, IndirectRef) and isinstance(dest.ref.type, PointerType):
mode_cls = IndirectY
label = self.get_label(dest.ref.name)
operand = IndirectY(self.get_label(dest.ref.name))
else:
mode_cls = Absolute
label = self.get_label(dest.name)
operand = self.absolute_or_zero_page(self.get_label(dest.name))
if op_cls is None or mode_cls is None:
if op_cls is None:
raise UnsupportedOpcodeError(instr)
self.emitter.emit(op_cls(mode_cls(label)))
self.emitter.emit(op_cls(operand))
elif opcode == 'add':
if dest == REG_A:
if isinstance(src, ConstantRef):
@ -342,18 +357,24 @@ class Compiler(object):
else:
raise NotImplementedError
elif opcode == 'goto':
location = instr.location
label = self.get_label(instr.location.name)
if isinstance(location.type, RoutineType):
self.emitter.emit(JMP(Absolute(label)))
elif isinstance(location.type, VectorType):
self.emitter.emit(JMP(Indirect(label)))
self.final_goto_seen = True
if self.skip_final_goto:
pass
else:
raise NotImplementedError
location = instr.location
label = self.get_label(instr.location.name)
if isinstance(location.type, RoutineType):
self.emitter.emit(JMP(Absolute(label)))
elif isinstance(location.type, VectorType):
self.emitter.emit(JMP(Indirect(label)))
else:
raise NotImplementedError
elif opcode == 'copy':
self.compile_copy(instr, instr.src, instr.dest)
elif opcode == 'trash':
pass
elif opcode == 'nop':
self.emitter.emit(NOP())
else:
raise NotImplementedError(opcode)
@ -509,10 +530,12 @@ class Compiler(object):
False: {
'c': BCC,
'z': BNE,
'n': BPL,
},
True: {
'c': BCS,
'z': BEQ,
'n': BMI,
},
}[instr.inverted].get(instr.src.name)
if cls is None:
@ -539,10 +562,12 @@ class Compiler(object):
False: {
'c': BCC,
'z': BNE,
'n': BPL,
},
True: {
'c': BCS,
'z': BEQ,
'n': BMI,
},
}[instr.inverted].get(instr.src.name)
if cls is None:

View File

@ -13,6 +13,8 @@ class Emittable(object):
class Byte(Emittable):
def __init__(self, value):
if isinstance(value, basestring):
value = ord(value)
if value < -127 or value > 255:
raise IndexError(value)
if value < 0:
@ -49,6 +51,7 @@ class Word(Emittable):
class Table(Emittable):
def __init__(self, value, size):
"""`value` should be an iterable of Emittables."""
# TODO: range-checking
self.value = value
self._size = size
@ -57,12 +60,10 @@ class Table(Emittable):
return self._size
def serialize(self, addr=None):
bytes = []
for b in self.value:
bytes.append(chr(ord(b)))
while len(bytes) < self.size():
bytes.append(chr(0))
return ''.join(bytes)
buf = ''.join([emittable.serialize() for emittable in self.value])
while len(buf) < self.size():
buf += chr(0)
return buf
def __repr__(self):
return "%s()" % (self.__class__.__name__)
@ -186,3 +187,14 @@ class Emitter(object):
advance the address for the next label, but don't emit anything."""
self.resolve_label(label)
self.addr += label.length
def size(self):
return sum(emittable.size() for emittable in self.accum)
def pad_to_size(self, size):
self_size = self.size()
if self_size > size:
raise IndexError("Emitter size {} exceeds pad size {}".format(self_size, size))
num_bytes = size - self_size
if num_bytes > 0:
self.accum.extend([Byte(0)] * num_bytes)

View File

@ -0,0 +1,52 @@
# encoding: UTF-8
from copy import copy
from sixtypical.model import RoutineType
class FallthruAnalyzer(object):
def __init__(self, debug=False):
self.debug = debug
def analyze_program(self, program):
self.program = program
self.fallthru_map = {}
for routine in program.routines:
encountered_gotos = list(routine.encountered_gotos)
if len(encountered_gotos) == 1 and isinstance(encountered_gotos[0].type, RoutineType):
self.fallthru_map[routine.name] = encountered_gotos[0].name
else:
self.fallthru_map[routine.name] = None
def find_chain(self, routine_name, available):
chain = [routine_name]
seen = set(chain)
while True:
next = self.fallthru_map.get(routine_name)
if next is None or next in seen or next not in available:
return chain
seen.add(next)
chain.append(next)
routine_name = next
def serialize(self):
pending_routines = copy(self.fallthru_map)
roster = []
main_chain = self.find_chain('main', pending_routines)
roster.append(main_chain)
for k in main_chain:
del pending_routines[k]
while pending_routines:
chains = [self.find_chain(k, pending_routines) for k in pending_routines.keys()]
chains.sort(key=len, reverse=True)
c = chains[0]
roster.append(c)
for k in c:
del pending_routines[k]
return roster

View File

@ -160,6 +160,18 @@ class BNE(Instruction):
}
class BPL(Instruction):
opcodes = {
Relative: 0x10,
}
class BMI(Instruction):
opcodes = {
Relative: 0x30,
}
class CLC(Instruction):
opcodes = {
Implied: 0x18
@ -312,6 +324,12 @@ class RTS(Instruction):
}
class NOP(Instruction):
opcodes = {
Implied: 0xEA,
}
class SBC(Instruction):
opcodes = {
Immediate: 0xe9,

View File

@ -6,7 +6,7 @@ from sixtypical.model import (
RoutineType, VectorType, TableType, BufferType, PointerType,
LocationRef, ConstantRef, IndirectRef, IndexedRef, AddressRef,
)
from sixtypical.scanner import Scanner, SixtyPicalSyntaxError
from sixtypical.scanner import Scanner
class SymEntry(object):
@ -18,30 +18,40 @@ class SymEntry(object):
return "%s(%r, %r)" % (self.__class__.__name__, self.ast_node, self.model)
class Parser(object):
def __init__(self, text):
self.scanner = Scanner(text)
class ParsingContext(object):
def __init__(self):
self.symbols = {} # token -> SymEntry
self.current_statics = {} # token -> SymEntry
self.statics = {} # token -> SymEntry
self.typedefs = {} # token -> Type AST
self.consts = {} # token -> Loc
for token in ('a', 'x', 'y'):
self.symbols[token] = SymEntry(None, LocationRef(TYPE_BYTE, token))
for token in ('c', 'z', 'n', 'v'):
self.symbols[token] = SymEntry(None, LocationRef(TYPE_BIT, token))
self.backpatch_instrs = []
def syntax_error(self, msg):
raise SixtyPicalSyntaxError(self.scanner.line_number, msg)
def __str__(self):
return "Symbols: {}\nStatics: {}\nTypedefs: {}\nConsts: {}".format(self.symbols, self.statics, self.typedefs, self.consts)
def soft_lookup(self, name):
if name in self.current_statics:
return self.current_statics[name].model
def lookup(self, name):
if name in self.statics:
return self.statics[name].model
if name in self.symbols:
return self.symbols[name].model
return None
class Parser(object):
def __init__(self, context, text, filename):
self.context = context
self.scanner = Scanner(text, filename)
self.backpatch_instrs = []
def syntax_error(self, msg):
self.scanner.syntax_error(msg)
def lookup(self, name):
model = self.soft_lookup(name)
model = self.context.lookup(name)
if model is None:
self.syntax_error('Undefined symbol "{}"'.format(name))
return model
@ -51,16 +61,19 @@ class Parser(object):
def program(self):
defns = []
routines = []
while self.scanner.on('typedef'):
typedef = self.typedef()
while self.scanner.on('typedef', 'const'):
if self.scanner.on('typedef'):
self.typedef()
if self.scanner.on('const'):
self.defn_const()
typenames = ['byte', 'word', 'table', 'vector', 'buffer', 'pointer'] # 'routine',
typenames.extend(self.typedefs.keys())
typenames.extend(self.context.typedefs.keys())
while self.scanner.on(*typenames):
defn = self.defn()
name = defn.name
if name in self.symbols:
if self.context.lookup(name):
self.syntax_error('Symbol "%s" already declared' % name)
self.symbols[name] = SymEntry(defn, defn.location)
self.context.symbols[name] = SymEntry(defn, defn.location)
defns.append(defn)
while self.scanner.on('define', 'routine'):
if self.scanner.consume('define'):
@ -70,14 +83,14 @@ class Parser(object):
else:
routine = self.legacy_routine()
name = routine.name
if name in self.symbols:
if self.context.lookup(name):
self.syntax_error('Symbol "%s" already declared' % name)
self.symbols[name] = SymEntry(routine, routine.location)
self.context.symbols[name] = SymEntry(routine, routine.location)
routines.append(routine)
self.scanner.check_type('EOF')
# now backpatch the executable types.
#for type_name, type_ in self.typedefs.iteritems():
#for type_name, type_ in self.context.typedefs.iteritems():
# type_.backpatch_constraint_labels(lambda w: self.lookup(w))
for defn in defns:
defn.location.type.backpatch_constraint_labels(lambda w: self.lookup(w))
@ -86,18 +99,16 @@ class Parser(object):
for instr in self.backpatch_instrs:
if instr.opcode in ('call', 'goto'):
name = instr.location
if name not in self.symbols:
self.syntax_error('Undefined routine "%s"' % name)
if not isinstance(self.symbols[name].model.type, (RoutineType, VectorType)):
model = self.lookup(name)
if not isinstance(model.type, (RoutineType, VectorType)):
self.syntax_error('Illegal call of non-executable "%s"' % name)
instr.location = self.symbols[name].model
instr.location = model
if instr.opcode in ('copy',) and isinstance(instr.src, basestring):
name = instr.src
if name not in self.symbols:
self.syntax_error('Undefined routine "%s"' % name)
if not isinstance(self.symbols[name].model.type, (RoutineType, VectorType)):
model = self.lookup(name)
if not isinstance(model.type, (RoutineType, VectorType)):
self.syntax_error('Illegal copy of non-executable "%s"' % name)
instr.src = self.symbols[name].model
instr.src = model
return Program(self.scanner.line_number, defns=defns, routines=routines)
@ -105,23 +116,37 @@ class Parser(object):
self.scanner.expect('typedef')
type_ = self.defn_type()
name = self.defn_name()
if name in self.typedefs:
if name in self.context.typedefs:
self.syntax_error('Type "%s" already declared' % name)
self.typedefs[name] = type_
self.context.typedefs[name] = type_
return type_
def defn_const(self):
self.scanner.expect('const')
name = self.defn_name()
if name in self.context.consts:
self.syntax_error('Const "%s" already declared' % name)
loc = self.const()
self.context.consts[name] = loc
return loc
def defn(self):
type_ = self.defn_type()
name = self.defn_name()
initial = None
if self.scanner.consume(':'):
if isinstance(type_, TableType) and self.scanner.on_type('string literal'):
initial = self.scanner.token
if isinstance(type_, TableType):
if self.scanner.on_type('string literal'):
initial = self.scanner.token
self.scanner.scan()
else:
initial = []
initial.append(self.const().value)
while self.scanner.consume(','):
initial.append(self.const().value)
else:
self.scanner.check_type('integer literal')
initial = int(self.scanner.token)
self.scanner.scan()
initial = self.const().value
addr = None
if self.scanner.consume('@'):
@ -136,21 +161,31 @@ class Parser(object):
return Defn(self.scanner.line_number, name=name, addr=addr, initial=initial, location=location)
def literal_int(self):
self.scanner.check_type('integer literal')
c = int(self.scanner.token)
self.scanner.scan()
return c
def literal_int_const(self):
value = self.literal_int()
type_ = TYPE_WORD if value > 255 else TYPE_BYTE
loc = ConstantRef(type_, value)
return loc
def const(self):
if self.scanner.token in ('on', 'off'):
loc = ConstantRef(TYPE_BIT, 1 if self.scanner.token == 'on' else 0)
self.scanner.scan()
return loc
elif self.scanner.on_type('integer literal'):
value = int(self.scanner.token)
self.scanner.scan()
type_ = TYPE_WORD if value > 255 else TYPE_BYTE
loc = ConstantRef(type_, value)
return loc
elif self.scanner.consume('word'):
loc = ConstantRef(TYPE_WORD, int(self.scanner.token))
self.scanner.scan()
return loc
elif self.scanner.token in self.context.consts:
loc = self.context.consts[self.scanner.token]
self.scanner.scan()
return loc
else:
self.syntax_error('bad constant "%s"' % self.scanner.token)
def defn_size(self):
self.scanner.expect('[')
size = self.literal_int()
size = self.const().value
self.scanner.expect(']')
return size
@ -193,9 +228,9 @@ class Parser(object):
else:
type_name = self.scanner.token
self.scanner.scan()
if type_name not in self.typedefs:
if type_name not in self.context.typedefs:
self.syntax_error("Undefined type '%s'" % type_name)
type_ = self.typedefs[type_name]
type_ = self.context.typedefs[type_name]
return type_
@ -251,9 +286,9 @@ class Parser(object):
else:
statics = self.statics()
self.current_statics = self.compose_statics_dict(statics)
self.context.statics = self.compose_statics_dict(statics)
block = self.block()
self.current_statics = {}
self.context.statics = {}
addr = None
location = LocationRef(type_, name)
@ -267,7 +302,7 @@ class Parser(object):
c = {}
for defn in statics:
name = defn.name
if name in self.symbols or name in self.current_statics:
if self.context.lookup(name):
self.syntax_error('Symbol "%s" already declared' % name)
c[name] = SymEntry(defn, defn.location)
return c
@ -294,20 +329,12 @@ class Parser(object):
return accum
def locexpr(self, forward=False):
if self.scanner.token in ('on', 'off'):
loc = ConstantRef(TYPE_BIT, 1 if self.scanner.token == 'on' else 0)
self.scanner.scan()
return loc
elif self.scanner.on_type('integer literal'):
return self.literal_int_const()
elif self.scanner.consume('word'):
loc = ConstantRef(TYPE_WORD, int(self.scanner.token))
self.scanner.scan()
return loc
if self.scanner.token in ('on', 'off', 'word') or self.scanner.token in self.context.consts or self.scanner.on_type('integer literal'):
return self.const()
elif forward:
name = self.scanner.token
self.scanner.scan()
loc = self.soft_lookup(name)
loc = self.context.lookup(name)
if loc is not None:
return loc
else:
@ -387,7 +414,7 @@ class Parser(object):
else:
self.syntax_error('expected "up" or "down", found "%s"' % self.scanner.token)
self.scanner.expect('to')
final = self.literal_int_const()
final = self.const()
block = self.block()
return For(self.scanner.line_number, dest=dest, direction=direction, final=final, block=block)
elif self.scanner.token in ("ld",):
@ -417,6 +444,10 @@ class Parser(object):
self.scanner.scan()
dest = self.locexpr()
return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=None)
elif self.scanner.token in ("nop",):
opcode = self.scanner.token
self.scanner.scan()
return SingleOp(self.scanner.line_number, opcode=opcode, dest=None, src=None)
elif self.scanner.token in ("call", "goto"):
opcode = self.scanner.token
self.scanner.scan()

View File

@ -4,16 +4,17 @@ import re
class SixtyPicalSyntaxError(ValueError):
def __init__(self, line_number, message):
super(SixtyPicalSyntaxError, self).__init__(line_number, message)
def __init__(self, filename, line_number, message):
super(SixtyPicalSyntaxError, self).__init__(filename, line_number, message)
def __str__(self):
return "Line {}: {}".format(self.args[0], self.args[1])
return "{}, line {}: {}".format(self.args[0], self.args[1], self.args[2])
class Scanner(object):
def __init__(self, text):
def __init__(self, text, filename):
self.text = text
self.filename = filename
self.token = None
self.type = None
self.line_number = 1
@ -62,9 +63,7 @@ class Scanner(object):
if self.token == token:
self.scan()
else:
raise SixtyPicalSyntaxError(self.line_number, "Expected '{}', but found '{}'".format(
token, self.token
))
self.syntax_error("Expected '{}', but found '{}'".format(token, self.token))
def on(self, *tokens):
return self.token in tokens
@ -74,9 +73,7 @@ class Scanner(object):
def check_type(self, type):
if not self.type == type:
raise SixtyPicalSyntaxError(self.line_number, "Expected {}, but found '{}'".format(
self.type, self.token
))
self.syntax_error("Expected {}, but found '{}'".format(self.type, self.token))
def consume(self, token):
if self.token == token:
@ -84,3 +81,6 @@ class Scanner(object):
return True
else:
return False
def syntax_error(self, msg):
raise SixtyPicalSyntaxError(self.filename, self.line_number, msg)

View File

@ -3,4 +3,5 @@
falderal --substring-error \
tests/SixtyPical\ Syntax.md \
tests/SixtyPical\ Analysis.md \
tests/SixtyPical\ Fallthru.md \
tests/SixtyPical\ Compilation.md

View File

@ -1010,7 +1010,7 @@ Can't `dec` a `word` type.
### cmp ###
Some rudimentary tests for cmp.
Some rudimentary tests for `cmp`.
| routine main
| inputs a
@ -1037,7 +1037,7 @@ Some rudimentary tests for cmp.
### and ###
Some rudimentary tests for and.
Some rudimentary tests for `and`.
| routine main
| inputs a
@ -1064,7 +1064,7 @@ Some rudimentary tests for and.
### or ###
Writing unit tests on a train. Wow.
Some rudimentary tests for `or`.
| routine main
| inputs a
@ -1091,7 +1091,7 @@ Writing unit tests on a train. Wow.
### xor ###
Writing unit tests on a train. Wow.
Some rudimentary tests for `xor`.
| routine main
| inputs a
@ -1118,7 +1118,7 @@ Writing unit tests on a train. Wow.
### shl ###
Some rudimentary tests for shl.
Some rudimentary tests for `shl`.
| routine main
| inputs a, c
@ -1146,7 +1146,7 @@ Some rudimentary tests for shl.
### shr ###
Some rudimentary tests for shr.
Some rudimentary tests for `shr`.
| routine main
| inputs a, c
@ -1172,6 +1172,16 @@ Some rudimentary tests for shr.
| }
? UnmeaningfulReadError: c
### nop ###
Some rudimentary tests for `nop`.
| routine main
| {
| nop
| }
= ok
### call ###
When calling a routine, all of the locations it lists as inputs must be
@ -1659,6 +1669,18 @@ The body of `repeat forever` can be empty.
| }
= ok
While `repeat` is most often used with `z`, it can also be used with `n`.
| routine main
| outputs y, n, z
| {
| ld y, 15
| repeat {
| dec y
| } until n
| }
= ok
### for ###
Basic "open-faced for" loop. We'll start with the "upto" variant.

View File

@ -18,6 +18,15 @@ Null program.
| }
= $080D RTS
`nop` program.
| routine main
| {
| nop
| }
= $080D NOP
= $080E RTS
Rudimentary program.
| routine main
@ -103,6 +112,27 @@ Memory location with explicit address.
= $080F STA $0400
= $0812 RTS
Accesses to memory locations in zero-page with `ld` and `st` use zero-page addressing.
| byte zp @ $00
| byte screen @ 100
|
| routine main
| inputs screen, zp
| outputs screen, zp
| trashes a, z, n
| {
| ld a, screen
| st a, screen
| ld a, zp
| st a, zp
| }
= $080D LDA $64
= $080F STA $64
= $0811 LDA $00
= $0813 STA $00
= $0815 RTS
Memory location with initial value.
| byte lives : 3
@ -137,7 +167,7 @@ Word memory locations with explicit address, initial value.
= $081A .byte $BB
= $081B .byte $0B
Initialized byte table. Bytes allocated, but beyond the string, are 0's.
Initialized byte table, initialized with ASCII string. Bytes allocated, but beyond the string, are 0's.
| byte table[8] message : "WHAT?"
|
@ -159,6 +189,45 @@ Initialized byte table. Bytes allocated, but beyond the string, are 0's.
= $0819 BRK
= $081A BRK
Initialized byte table, initialized with list of byte values.
| byte table[8] message : 255, 0, 129, 128, 127
|
| routine main
| inputs message
| outputs x, a, z, n
| {
| ld x, 0
| ld a, message + x
| }
= $080D LDX #$00
= $080F LDA $0813,X
= $0812 RTS
= $0813 .byte $FF
= $0814 BRK
= $0815 STA ($80,X)
= $0817 .byte $7F
= $0818 BRK
= $0819 BRK
= $081A BRK
Initialized word table, initialized with list of word values.
| word table[8] message : 65535, 0, 127
|
| routine main
| {
| }
= $080D RTS
= $080E .byte $FF
= $080F .byte $FF
= $0810 BRK
= $0811 BRK
= $0812 .byte $7F
= $0813 BRK
= $0814 BRK
= $0815 BRK
Some instructions.
| byte foo
@ -288,7 +357,7 @@ Compiling `if` without `else`.
= $0813 LDY #$01
= $0815 RTS
Compiling `repeat`.
Compiling `repeat ... until z`.
| routine main
| trashes a, y, z, n, c
@ -307,7 +376,7 @@ Compiling `repeat`.
= $0813 BNE $080F
= $0815 RTS
Compiling `repeat until not`.
Compiling `repeat ... until not z`.
| routine main
| trashes a, y, z, n, c
@ -326,6 +395,40 @@ Compiling `repeat until not`.
= $0813 BEQ $080F
= $0815 RTS
Compiling `repeat ... until n`.
| routine main
| trashes a, y, z, n, c
| {
| ld y, 65
| repeat {
| ld a, y
| dec y
| } until n
| }
= $080D LDY #$41
= $080F TYA
= $0810 DEY
= $0811 BPL $080F
= $0813 RTS
Compiling `repeat ... until not n`.
| routine main
| trashes a, y, z, n, c
| {
| ld y, 199
| repeat {
| ld a, y
| inc y
| } until not n
| }
= $080D LDY #$C7
= $080F TYA
= $0810 INY
= $0811 BMI $080F
= $0813 RTS
Compiling `repeat forever`.
| routine main
@ -673,7 +776,7 @@ Indirect call.
= $081E JMP ($0822)
= $0821 RTS
goto.
Compiling `goto`. Note that no `RTS` is emitted after the `JMP`.
| routine bar
| inputs y
@ -691,10 +794,9 @@ goto.
| goto bar
| }
= $080D LDY #$C8
= $080F JMP $0813
= $0812 RTS
= $0813 LDX #$C8
= $0815 RTS
= $080F JMP $0812
= $0812 LDX #$C8
= $0814 RTS
### Vector tables

View File

@ -0,0 +1,427 @@
SixtyPical Fallthru
===================
This is a test suite, written in [Falderal][] format, for SixtyPical's
ability to detect which routines make tail calls to other routines,
and thus can be re-arranged to simply "fall through" to them.
The theory is as follows.
SixtyPical supports a `goto`, but it can only appear in tail position.
If a routine r1 ends with a unique `goto` to a fixed routine r2 it is said
to *potentially fall through* to r2.
A *unique* `goto` means that there are not multiple different `goto`s in
tail position (which can happen if, for example, an `if` is the last thing
in a routine, and each branch of that `if` ends with a different `goto`.)
A *fixed* routine means, a routine which is known at compile time, not a
`goto` through a vector.
Consider the set R of all available routines in the program.
Every routine either potentially falls through to a single other routine
or it does not potentially fall through to any routine.
More formally, we can say
> fall : R → R {nil}, fall(r) ≠ r
where `nil` is an atom that represents no routine.
Now consider an operation chain() vaguely similar to a transitive closure
on fall(). Starting with r, we construct a list of r, fall(r),
fall(fall(r)), ... with the following restrictions:
- we stop when we reach `nil` (because fall(`nil`) is not defined)
- we stop when we see an element that is not in R.
- we stop when we see an element that we have already added to the
list (this is to prevent infinite lists due to cycles.)
With these definitions, our algorithm is something like this.
Treat R as a mutable set and start with an empty list of lists L. Then,
- For all r ∈ R, find all chain(r).
- Pick a longest such chain. Call it C.
- Append C to L.
- Remove all elements occurring in C, from R.
- Repeat until R is empty.
When times comes to generate code, generate it in the order given by L.
In addition, each sublist in L represents a number of routines to
generate; all except the final routine in such a sublist need not have
any jump instruction generated for its final `goto`.
The tests in this document test against the list L.
Note that this optimization is a feature of the SixtyPical's reference
compiler, not the language. So an implementation is not required
to pass these tests to be considered an implementation of SixtyPical.
[Falderal]: http://catseye.tc/node/Falderal
-> Functionality "Dump fallthru info for SixtyPical program" is implemented by
-> shell command "bin/sixtypical --optimize-fallthru --dump-fallthru-info --analyze-only --traceback %(test-body-file)"
-> Functionality "Compile SixtyPical program with fallthru optimization" is implemented by
-> shell command "bin/sixtypical --prelude=c64 --optimize-fallthru --traceback %(test-body-file) >/tmp/foo && tests/appliances/bin/dcc6502-adapter </tmp/foo"
-> Tests for functionality "Dump fallthru info for SixtyPical program"
A single routine, obviously, falls through to nothing and has nothing fall
through to it.
| define main routine
| {
| }
= [
= [
= "main"
= ]
= ]
If `main` does a `goto foo`, then it can fall through to `foo`.
| define foo routine trashes a, z, n
| {
| ld a, 0
| }
|
| define main routine trashes a, z, n
| {
| goto foo
| }
= [
= [
= "main",
= "foo"
= ]
= ]
More than one routine can fall through to a routine. We pick one
of them to fall through, when selecting the order of routines.
| define foo routine trashes a, z, n
| {
| ld a, 0
| }
|
| define bar routine trashes a, z, n
| {
| ld a, 0
| goto foo
| }
|
| define main routine trashes a, z, n
| {
| goto foo
| }
= [
= [
= "main",
= "foo"
= ],
= [
= "bar"
= ]
= ]
Because `main` is always serialized first (so that the entry
point of the entire program appears at the beginning of the code),
nothing ever falls through to `main`.
| define foo routine trashes a, z, n
| {
| ld a, 0
| goto main
| }
|
| define main routine trashes a, z, n
| {
| ld a, 1
| }
= [
= [
= "main"
= ],
= [
= "foo"
= ]
= ]
There is nothing stopping two routines from tail-calling each
other, but we will only be able to make one of them, at most,
fall through to the other.
| define foo routine trashes a, z, n
| {
| ld a, 0
| goto bar
| }
|
| define bar routine trashes a, z, n
| {
| ld a, 0
| goto foo
| }
|
| define main routine trashes a, z, n
| {
| }
= [
= [
= "main"
= ],
= [
= "bar",
= "foo"
= ]
= ]
If a routine does two tail calls (which is possible because they
can be in different branches of an `if`) it cannot fall through to another
routine.
| define foo routine trashes a, z, n
| {
| ld a, 0
| }
|
| define bar routine trashes a, z, n
| {
| ld a, 0
| }
|
| define main routine inputs z trashes a, z, n
| {
| if z {
| goto foo
| } else {
| goto bar
| }
| }
= [
= [
= "main"
= ],
= [
= "bar"
= ],
= [
= "foo"
= ]
= ]
If, however, they are the same goto, one can be optimized away.
| define foo routine trashes a, z, n
| {
| ld a, 0
| if z {
| ld a, 1
| goto bar
| } else {
| ld a, 2
| goto bar
| }
| }
|
| define bar routine trashes a, z, n
| {
| ld a, 255
| }
|
| define main routine trashes a, z, n
| {
| }
= [
= [
= "main"
= ],
= [
= "foo",
= "bar"
= ]
= ]
Similarly, a tail call to a vector can't be turned into a fallthru,
because we don't necessarily know what actual routine the vector contains.
| vector routine trashes a, z, n
| vec
|
| define foo routine trashes a, z, n
| {
| ld a, 0
| }
|
| define bar routine trashes a, z, n
| {
| ld a, 0
| }
|
| define main routine outputs vec trashes a, z, n
| {
| copy bar, vec
| goto vec
| }
= [
= [
= "main"
= ],
= [
= "bar"
= ],
= [
= "foo"
= ]
= ]
Our algorithm might not be strictly optimal, but it does a good job.
| define r1 routine trashes a, z, n
| {
| ld a, 0
| goto r2
| }
|
| define r2 routine trashes a, z, n
| {
| ld a, 0
| goto r3
| }
|
| define r3 routine trashes a, z, n
| {
| ld a, 0
| goto r4
| }
|
| define r4 routine trashes a, z, n
| {
| ld a, 0
| }
|
| define r5 routine trashes a, z, n
| {
| ld a, 0
| goto r6
| }
|
| define r6 routine trashes a, z, n
| {
| ld a, 0
| goto r3
| }
|
| define main routine trashes a, z, n
| {
| goto r1
| }
= [
= [
= "main",
= "r1",
= "r2",
= "r3",
= "r4"
= ],
= [
= "r5",
= "r6"
= ]
= ]
-> Tests for functionality "Compile SixtyPical program with fallthru optimization"
Basic test for actually applying this optimization when compiling SixtyPical programs.
| define foo routine trashes a, z, n
| {
| ld a, 0
| }
|
| define bar routine trashes a, z, n
| {
| ld a, 255
| goto foo
| }
|
| define main routine trashes a, z, n
| {
| goto foo
| }
= $080D LDA #$00
= $080F RTS
= $0810 LDA #$FF
= $0812 JMP $080D
It can optimize out one of the `goto`s if they are the same.
| define foo routine trashes a, z, n
| {
| ld a, 0
| if z {
| ld a, 1
| goto bar
| } else {
| ld a, 2
| goto bar
| }
| }
|
| define bar routine trashes a, z, n
| {
| ld a, 255
| }
|
| define main routine trashes a, z, n
| {
| }
= $080D RTS
= $080E LDA #$00
= $0810 BNE $0817
= $0812 LDA #$01
= $0814 JMP $0819
= $0817 LDA #$02
= $0819 LDA #$FF
= $081B RTS
It cannot optimize out the `goto`s if they are different.
Note, this currently produces unfortunately unoptimized code,
because generating code for the "true" branch of an `if` always
generates a jump out of the `if`, even if the last instruction
in the "true" branch is a `goto`.
| define foo routine trashes a, z, n
| {
| ld a, 0
| if z {
| ld a, 1
| goto bar
| } else {
| ld a, 2
| goto main
| }
| }
|
| define bar routine trashes a, z, n
| {
| ld a, 255
| }
|
| define main routine trashes a, z, n
| {
| }
= $080D RTS
= $080E LDA #$FF
= $0810 RTS
= $0811 LDA #$00
= $0813 BNE $081D
= $0815 LDA #$01
= $0817 JMP $080E
= $081A JMP $0822
= $081D LDA #$02
= $081F JMP $080D

View File

@ -78,6 +78,14 @@ Trash.
| }
= ok
`nop`.
| routine main
| {
| nop
| }
= ok
If with not
| routine foo {
@ -228,6 +236,31 @@ Can't have two typedefs with the same name.
| }
? SyntaxError
Constants.
| const lives 3
| const days lives
| const w1 1000
| const w2 word 0
|
| typedef byte table[days] them
|
| byte lark: lives
|
| routine main {
| ld a, lives
| }
= ok
Can't have two constants with the same name.
| const w1 1000
| const w1 word 0
|
| routine main {
| }
? SyntaxError
Explicit memory address.
| byte screen @ 1024
@ -269,7 +302,7 @@ User-defined locations of other types.
| }
= ok
Initialized byte table.
Initialized byte table, initialized with ASCII string.
| byte table[32] message : "WHAT DO YOU WANT TO DO NEXT?"
|
@ -285,6 +318,14 @@ Can't initialize anything but a byte table with a string.
| }
? SyntaxError
Initialized byte table, initialized with list of bytes.
| byte table[8] charmap : 0, 255, 129, 192, 0, 1, 2, 4
|
| routine main {
| }
= ok
Can't access an undeclared memory location.
| routine main {