1
0
mirror of https://github.com/catseye/SixtyPical.git synced 2024-06-02 03:41:28 +00:00

Merge pull request #10 from catseye/develop-0.14

Develop 0.14
This commit is contained in:
Chris Pressey 2018-03-27 09:50:24 +01:00 committed by GitHub
commit 330e61f327
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1448 additions and 114 deletions

View File

@ -1,6 +1,20 @@
History of SixtyPical
=====================
0.14
----
* Added the so-called "open-faced `for` loop", which spans a loop
variable over a finite range, the end of which is fixed.
* "Tail position" is now more correctly determined for the purposes of
insisting that `goto` only appears in it.
* New `--origin` and `--output-format` options added to the compiler.
* Fixed bug when `--prelude` option was missing.
* Fixed bug when reporting line numbers of scanner-level syntax errors.
* Translated the small demo projects Ribos and "The PETulant Cursor" to
SixtyPical, and added them to the `eg/c64/` section of the repo.
* Added a `eg/vic20` example directory, with one VIC-20 example program.
0.13
----

View File

@ -1,12 +1,12 @@
SixtyPical
==========
_Version 0.13. Work-in-progress, everything is subject to change._
_Version 0.14. Work-in-progress, everything is subject to change._
**SixtyPical** is a 6502-assembly-like programming language with advanced
**SixtyPical** is a 6502-like programming language with advanced
static analysis.
"6502-assembly-like" means that it has similar restrictions as programming
"6502-like" means that it has similar restrictions as programming
in 6502 assembly (e.g. the programmer must choose the registers that
values will be stored in) and is concomitantly easy for a compiler to
translate it to 6502 machine language code.
@ -15,8 +15,8 @@ translate it to 6502 machine language code.
go through the program step by step, tracking not just the changes that
happen during a _specific_ execution of the program, but _sets_ of changes
that could _possibly_ happen in any run of the program. This lets us
determine that certain things can never happen, which we can present as
safety guarantees.
determine that certain things can never happen, which we can then formulate
as safety checks.
In practice, this means it catches things like
@ -51,7 +51,8 @@ automatically start it in the `x64` emulator, and you should see:
![Screenshot of result of running hearts.60p](https://raw.github.com/catseye/SixtyPical/master/images/hearts.png)
You can try the `loadngo.sh` script on other sources in the `eg` directory
tree. There is an entire small game(-like program) in [demo-game.60p](eg/c64/demo-game.60p).
tree, which contains more extensive examples, including an entire
game(-like program); see [eg/README.md](eg/README.md) for a listing.
Documentation
-------------
@ -67,37 +68,6 @@ Documentation
TODO
----
### `for`-like loop
We have range-checking in the abstract analysis now, but we lack practical ways
to use it.
We can `and` a value to ensure it is within a certain range. However, in the 6502
ISA the only register you can `and` is `A`, while loops are done with `X` or `Y`.
Insisting this as the way to do it would result in a lot of `TXA`s and `TAX`s.
What would be better is a dedicated `for` loop, like
for x in 0 to 15 {
// in here, we know the range of x is exactly 0-15 inclusive
// also in here: we are disallowed from changing x
}
However, this is slightly restrictive, and hides a lot.
However however, options which do not hide a lot, require a lot of looking at
(to ensure: did you increment the loop variable? only once? etc.)
The leading compromise so far is an "open-faced for loop", like
ld x, 15
for x downto 0 {
// same as above
}
This makes it a little more explicit, at least, even though the loop
decrementation is still hidden.
### Save registers on stack
This preserves them, so that, semantically, they can be used later even though they
@ -114,6 +84,7 @@ is probably NP-complete. But doing it adequately is probably not that hard.
* `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.
(Associate each pointer with the buffer it points into.)
* `static` pointers -- currently not possible because pointers must be zero-page, thus `@`, thus uninitialized.
@ -123,5 +94,7 @@ 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?
[VICE]: http://vice-emu.sourceforge.net/

View File

@ -36,11 +36,21 @@ if __name__ == '__main__':
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 "
"located. Default: 0xc000."
)
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."
)
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. "
"Also sets the origin. "
"Also sets the origin and format. "
"Options are: c64 or vic20."
)
argparser.add_argument(
@ -92,22 +102,31 @@ if __name__ == '__main__':
sys.exit(0)
fh = sys.stdout
start_addr = 0xc000
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]
else:
raise NotImplementedError
elif options.prelude:
raise NotImplementedError("Unknown prelude: {}".format(options.prelude))
# we are outputting a .PRG, so we output the load address first
# we don't use the Emitter for this b/c not part of addr space
if not options.debug:
# 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)

View File

@ -1,7 +1,7 @@
SixtyPical
==========
This document describes the SixtyPical programming language version 0.11,
This document describes the SixtyPical programming language version 0.14,
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.)
@ -234,9 +234,12 @@ changed by this instruction; they must be named in the WRITES, and they
are considered initialized after it has executed.
If and only if src is a byte table, the index-memory-location must be given.
In this case, it is illegal if the value of the index-memory-location falls
outside of the range of the table.
Some combinations, such as `ld x, y`, are illegal because they do not map to
underlying opcodes.
underlying opcodes. (For an instruction which maps more flexibly to underlying
opcodes, see `copy`.)
There is another mode of `ld` which reads into `a` indirectly through a pointer.
@ -265,6 +268,8 @@ After execution, dest is considered initialized. No flags are
changed by this instruction (unless of course dest is a flag.)
If and only if dest is a byte table, the index-memory-location must be given.
In this case, it is illegal if the value of the index-memory-location falls
outside of the range of the table.
There is another mode of `st` which write `a` into memory, indirectly through
a pointer.
@ -531,6 +536,22 @@ The sense of the test can be inverted with `not`.
}
} until not z
### for ###
for <dest-memory-location> (up|down) to <literal-byte> {
<block>
}
Executes the block repeatedly, incrementing or decrementing the
dest-memory-location at the end of the block, until the value of
the dest-memory-location has gone past the literal-byte.
The block is always executed as least once.
* It is illegal if any memory location is uninitialized at the exit of
the loop when that memory location is initialized at the start of
the loop.
Grammar
-------
@ -555,9 +576,10 @@ Grammar
LocExpr ::= Register | Flag | Literal | Ident.
Register::= "a" | "x" | "y".
Flag ::= "c" | "z" | "n" | "v".
Literal ::= LitByte | LitWord.
Literal ::= LitByte | LitWord | LitBit.
LitByte ::= "0" ... "255".
LitWord ::= "0" ... "65535".
LitBit ::= "on" | "off".
Block ::= "{" {Instr} "}".
Instr ::= "ld" LocExpr "," LocExpr ["+" LocExpr]
| "st" LocExpr "," LocExpr ["+" LocExpr]
@ -573,7 +595,9 @@ Grammar
| "dec" LocExpr
| "call" Ident<routine>
| "goto" Ident<executable>
| "copy" LocExpr "," LocExpr ["+" LocExpr]
| "if" ["not"] LocExpr Block ["else" Block]
| "repeat" Block ("until" ["not"] LocExpr | "forever")
| "copy" LocExpr "," LocExpr ["+" LocExpr]
| "for" LocExpr ("up"|"down") "to" Literal Block
| "with" "interrupts" LitBit Block
.

41
eg/README.md Normal file
View File

@ -0,0 +1,41 @@
This directory contains SixtyPical example programs, categorized
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.
### 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), it's 48 bytes.
### vic20
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).
[Ophis]: http://michaelcmartin.github.io/Ophis/

5
eg/c64/README.md Normal file
View File

@ -0,0 +1,5 @@
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.

View File

@ -90,7 +90,7 @@ word delta
vector logic_routine table[256] actor_logic
vector logic_routine dispatch_logic
byte table[32] press_fire_msg: "PRESS`FIRE`TO`PLAY"
byte table[18] press_fire_msg: "PRESS`FIRE`TO`PLAY"
//
// Points to the routine that implements the current game state.
@ -384,23 +384,14 @@ define enemy_logic logic_routine
define game_state_title_screen game_state_routine
{
ld y, 0
repeat {
// First we "clip" the index to 0-31 to ensure we don't
// read outside the bounds of the table:
ld a, y
and a, 31
ld y, a
for y up to 17 {
ld a, press_fire_msg + y
st on, c
sub a, 64 // yuck. oh well
st a, screen1 + y
inc y
cmp y, 18
} until z
}
st off, c
call check_button

View File

@ -12,16 +12,17 @@ byte vic_border @ 53280
// a future version of SixtyPical.
//
vector cinv
vector routine
inputs vic_border
outputs vic_border
trashes z, n
@ 788
cinv @ 788
vector save_cinv
vector routine
inputs vic_border
outputs vic_border
trashes z, n
save_cinv
routine our_cinv
inputs vic_border

9
eg/c64/petulant/build.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
sixtypical --output-format=prg --origin=0x02a7 petulant.60p > petulant-60p.prg
if [ "x$COMPARE" != "x" ]; then
dcc6502 petulant.prg > petulant.prg.disasm.txt
dcc6502 petulant-60p.prg > petulant-60p.prg.disasm.txt
paste petulant.prg.disasm.txt petulant-60p.prg.disasm.txt | pr -t -e24
rm -f *.disasm.txt
fi

Binary file not shown.

View File

@ -0,0 +1,54 @@
// petulant.60p - The PETulant Cursor, a "display hack" for the Commodore 64
// Originally written by Chris Pressey sometime in the late 1980's
// Rewritten in P65 assembly and released March 2008
// Rewritten in SixtyPical in March 2018
// This work is part of the public domain.
// ----- Types -----
typedef routine
inputs border, blnon, color, background
outputs border
trashes a, z, n
irq_handler
// ----- Addresses -----
vector irq_handler cinv @ $314 // hw irq interrupt, 60x per second
byte blnon @ $cf // was last cursor blink on or off?
byte color @ $286 // current foreground colour for text
byte border @ $d020 // colour of border of the screen
byte background @ $d021 // colour of background of the screen
// ----- Variables -----
vector irq_handler save_cinv
// ----- Interrupt Handler -----
define our_cinv irq_handler
{
ld a, blnon
if not z {
ld a, color
} else {
ld a, background
}
st a, border
goto save_cinv
}
// ----- Main Program -----
define main routine
inputs cinv
outputs cinv, save_cinv
trashes a, n, z
{
with interrupts off {
copy cinv, save_cinv
copy our_cinv, cinv
}
}

View File

@ -0,0 +1,52 @@
; petulant.p65 - The PETulant Cursor, a "display hack" for the Commodore 64
; Originally written by Chris Pressey sometime in the late 1980's
; Rewritten in P65 assembly and released March 2008
; This work is part of the public domain.
; ----- BEGIN petulant.p65 -----
; PRG file header
.org 0
.word $02a7
.org $02a7
; ----- Constants -----
.alias cinv $0314 ; hw irq interrupt, 60x per second
.alias blnon $cf ; was last cursor blink on or off?
.alias color $286 ; current foreground colour for text
.alias vic $d000 ; base address of VIC-II chip
.alias border vic+$20 ; colour of border of the screen
.alias background vic+$21 ; colour of background of the screen
; ----- Start of Program -----
start: sei ; disable interrupts
lda cinv ; save low byte of existing handler
sta savecinv
lda cinv+1 ; save high byte
sta savecinv+1
lda #<newcinv ; install new interrupt handler
sta cinv
lda #>newcinv
sta cinv+1
cli ; re-enable interrupts
rts
newcinv: lda blnon ; is the cursor on?
beq cursor_off
cursor_on: lda color ; yes, get its colour
jmp egress
cursor_off: lda background ; no, get the background colour
egress: sta border ; colour the border
jmp (savecinv) ; continue pre-existing interrupt handler
; ----- Uninitialized Data -----
.space savecinv 2
; ----- END of petulant.p65 -----

View File

@ -0,0 +1 @@
§x­<03>Ó­<03>Ô©À<C2A9>©<02>X`¥Ïð­†­<> ÐlÓ

View File

@ -0,0 +1,32 @@
The PETulant Cursor
===================
This is a tiny (44 bytes long) machine-language demo for the Commodore 64,
somewhat in the style of later "display hacks" for the Amiga -- surprising
and silly ways to play with the user interface.
So as not to not spoil the fun, try running it before reading the source.
To run it, make the file PETULANT.PRG accessible to your favourite Commodore
64 (or Commodore 64 emulator) in your favourite way, then
LOAD "PETULANT.PRG",8,1
SYS 679
For further fun, try changing the text colour (hold the C= or CTRL key while
pressing a number) or the background colour (POKE 53281 with a number from
0 to 15) while it is running.
I must have originally wrote this sometime in the late 80's. I disassembled
and rewrote it (to make it play more nicely with existing interrupt handlers)
in 2008.
Who knows -- this could actually be useful, in a ridiculously minor way: it
makes it much more obvious when control has returned to BASIC immediate mode
(e.g., when a program has finished loading.)
Enjoy!
-Chris Pressey
April 1, 2008
Chicago, IL

9
eg/c64/ribos/build.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
sixtypical --output-format=prg --origin=0xc000 ribos2.60p > ribos2-60p.prg
if [ "x$COMPARE" != "x" ]; then
dcc6502 ribos2.prg > ribos2.prg.disasm.txt
dcc6502 ribos2-60p.prg > ribos2-60p.prg.disasm.txt
paste ribos2.prg.disasm.txt ribos2-60p.prg.disasm.txt | pr -t -e24
rm -f *.disasm.txt
fi

79
eg/c64/ribos/readme.txt Normal file
View File

@ -0,0 +1,79 @@
Ribos
=====
This little demo is intended to be a well-commented example of how to
program a raster interrupt in 6502 assembly language on a Commodore 64.
This (r)aster (i)nterrupt changes the colour of a region of the
(bo)rder of the C64 (s)creen; thus, RIBOS. Also, it's the name of a
planet from Dr. Who, if that means anything.
How to Run the Demo (using the VICE C64 emulator, x64)
------------------------------------------------------
0. Obtain VICE from http://www.viceteam.org/, install it,
and run x64
1. Mount this project's directory as drive 8:
Make sure
Peripheral settings > Device #8 > Enable IEC Device
is checked, then select
Peripheral settings > Device #8 > File system directory...
and enter the path to the project directory.
2. LOAD "RIBOS.PRG",8,1
3. SYS 49152
4. You should see the colour of the middle of the border change
while you get a READY. prompt and can continue working.
How to Assemble the Program (using the p65 assembler)
-----------------------------------------------------
0. Obtain p65 from http://hkn.berkeley.edu/~mcmartin/P65/
(I used p65-Perl version 1.1) and install it somewhere
on your path. If your Perl interpreter isn't located at
/usr/bin/perl, change the first line of p65 appropriately.
1. p65 -v -t -b ribos.p65 ribos.prg
The switches aren't necessary, but they make it feel like
p65 is doing something difficult and important. It also
isn't necessary to add the '.prg' extension on the end of
the binary object's filename, since it will appear as a
PRG file to VICE anyway, but it's nice as a reminder when
you're working in a modern operating system.
2. Follow the steps under 'How to Run this Demo' to see that
it worked.
How it Works
------------
Read the source! I've tried to make it very well-commented,
including what happens when you leave out some steps.
I wrote this demo because it was a long time since I had done any C64
programming, and, having just obtained a copy of the 'Commodore 64
Programmer's Reference Guide,' I wanted to code something challenging,
yet not too involved. I remembered raster interrupts as one of those
quintessential C64 low-level graphics tricks, so I decided to try my
hand at that. Looking around on the Internet, I found this page:
http://everything2.com/index.pl?node_id=79254
Although it's a fairly detailed description, it took me a couple of
frustrating hours to implement it successfully - both the everything2
article and the Reference Guide were pretty muddy on a couple of
points. What I learned in the process is written into the comments.
Happy raster-interrupting!
-Chris Pressey
April 10, 2007
Vancouver, BC

291
eg/c64/ribos/ribos.p65 Normal file
View File

@ -0,0 +1,291 @@
; ribos.p65 - p65 assembly source for RIBOS:
; Demonstration of the VIC-II raster interrupt on the Commodore 64:
; Alter the border colour in the middle part of the screen only.
; Original (hardware IRQ vector) version.
; By Chris Pressey, Cat's Eye Technologies.
; This work has been placed in the public domain.
; ----- BEGIN ribos.p65 -----
; This source file is intented to be assembled to produce a PRG file
; which can be loaded into the C64's memory from a peripheral device.
; All C64 PRG files start with a 16-bit word which represents the
; location in memory to which they will be loaded. We can provide this
; using p65 directives as follows:
.org 0
.word $C000
; Now the actual assembly starts (at memory location 49152.)
.org $C000
; ----- Constants -----
; We first define some symbolic constants to add some clarity to our
; references to hardware registers and other locations in memory.
; Descriptions of the registers follow those given in the 'Commodore 64
; Programmer's Reference Guide'.
; The CIA #1 chip.
.alias cia1 $dc00 ; pp. 328-331
.alias intr_ctrl cia1+$d ; "CIA Interrupt Control Register
; (Read IRQs/Write Mask)"
; The VIC-II chip.
.alias vic $d000 ; Appendix G:
.alias vic_ctrl vic+$11 ; "Y SCROLL MODE"
.alias vic_raster vic+$12 ; "RASTER"
.alias vic_intr vic+$19 ; "Interrupt Request's" (sic)
.alias vic_intr_enable vic+$1a ; "Interrupt Request MASKS"
.alias border_color vic+$20 ; "BORDER COLOR"
; The address at which the IRQ vector is stored.
.alias irq_vec $fffe ; p. 411
; The zero-page address of the 6510's I/O register.
.alias io_ctrl $01 ; p. 310, "6510 On-Chip 8-Bit
; Input/Output Register"
; KERNAL and BASIC ROMs, p. 320
.alias kernal $e000
.alias basic $a000
; Zero-page addresses that the memory-copy routine uses for
; scratch space: "FREKZP", p. 316
.alias zp $fb
.alias stop_at $fd
; ----- Main Routine -----
; This routine is intended to be called by the user (by, e.g., SYS 49152).
; It installs the raster interrupt handler and returns to the caller.
; Key to installing the interrupt handler is altering the IRQ service
; vector. However, under normal circumstances, the address at which
; this vector is stored ($ffffe) maps to the C64's KERNAL ROM, which
; cannot be changed. So, in order to alter the vector, we must enable
; the RAM that underlies the ROM (i.e. the RAM that maps to the same
; address space as the KERNAL ROM.) If we were writing a bare-metal
; game, and didn't need any KERNAL routines or support, we could just
; switch it off. But for this demo, we'd like the raster effect to
; occur in the background as we use BASIC and whatnot, so we need to
; continue to have access to the KERNAL ROM. So what we do is copy the
; KERNAL ROM to the underlying RAM, then switch the RAM for the ROM.
jsr copy_rom_to_ram
; Interrupts can occur at any time. If one were to occur while we were
; changing the interrupt vector - for example, after we have stored the
; low byte of the address but before we have stored the high byte -
; unpredictable behaviour would result. To be safe, we disable interrupts
; with the 'sei' instruction before changing anything.
sei
; We obtain the address of the current IRQ service routine and save it
; in the variable 'saved_irq_vec'.
lda irq_vec ; save low byte
sta saved_irq_vec
lda irq_vec+1 ; save high byte
sta saved_irq_vec+1
; We then store the address of our IRQ service routine in its place.
lda #<our_service_routine
sta irq_vec
lda #>our_service_routine
sta irq_vec+1
; Now we must specify the raster line at which the interrupt gets called.
lda scanline
sta vic_raster
; Note that the position of the raster line is given by a 9-bit value,
; and we can't just assume that, because the raster line we want is less
; than 256, that the high bit will automatically be set to zero, because
; it won't. We have to explicitly set it high or low. It is found as
; the most significant bit of a VIC-II control register which has many
; different functions, so we must be careful to preserve all other bits.
lda vic_ctrl
and #%01111111
sta vic_ctrl
; Then we enable the raster interrupt on the VIC-II chip.
lda #$01
sta vic_intr_enable
; The article at everything2 suggests that we read the interrupt control
; port of the CIA #1 chip, presumably to acknowledge any pending IRQ and
; avoid the problem of having some sort of lockup due to a spurious IRQ.
; I've tested leaving this out, and the interrupt handler still seems get
; installed alright. But, I haven't tested it very demandingly, and it's
; likely to open up a race condition that I just haven't encountered (much
; like if we were to forget to execute the 'sei' instruction, above.)
; So, to play it safe, we read the port here.
lda intr_ctrl
; We re-enable interrupts to resume normal operation - normal, that is,
; except that our raster interrupt service routine will be called the next
; time the raster reaches the line stored in the 'vic_raster' register of
; the VIC-II chip.
cli
; Finally, we return to the caller.
rts
; ----- Raster Interrupt Service Routine ------
our_service_routine:
; This is an interrupt service routine (a.k.a. interrupt handler,) and as
; such, it can be called from anywhere. Since the code that was interrupted
; likely cares deeply about the values in its registers, we must be careful
; to save any that we change, and restore them before switching back to it.
; In this case, we only affect the processor flags and the accumulator, so
; we push them onto the stack.
php
pha
; The interrupt service routine on the Commodore 64 is very general-purpose,
; and may be invoked by any number of different kinds of interrupts. We,
; however, only care about a certain kind - the VIC-II's raster interrupt.
; We check to see if the current interrupt was caused by the raster by
; looking at the low bit of the VIC-II interrupt register. Note that we
; immediately store back the value found there before testing it. This is
; to acknowledge to the VIC-II chip that we got the interrupt. If we don't
; do this, it won't send us another interrupt next time.
lda vic_intr
sta vic_intr
and #$01
cmp #$01
beq we_handle_it
; If the interrupt was not caused by the raster, we restore the values
; of the registers from the stack, and continue execution as normal with
; the existing interrupt service routine.
pla
plp
jmp (saved_irq_vec)
we_handle_it:
; If we got here, the interrupt _was_ caused by the raster. So, we get
; to do our thing. To keep things simple, we just invert the border colour.
lda border_color
eor #$ff
sta border_color
; Now, we make the interrupt trigger on a different scan line so that we'll
; invert the colour back to normal lower down on the screen.
lda scanline
eor #$ff
sta scanline
sta vic_raster
; Restore the registers that we saved at the beginning of this routine.
pla
plp
; Return to normal operation. Note that we must issue an 'rti' instruction
; here, not 'rts', as we are returning from an interrupt.
rti
; ----- Utility Routine: copy KERNAL ROM to underlying RAM -----
copy_rom_to_ram:
; This is somewhat more involved than I let on above. The memory mapping
; facilities of the C64 are a bit convoluted. The Programmer's Reference
; Guide states on page 261 that the way to map out the KERNAL ROM, and
; map in the RAM underlying it, is to set the HIRAM signal on the 6510's
; I/O line (which is memory-mapped to address $0001) to 0. This is true.
; However, it is not the whole story: setting HIRAM to 0 *also* maps out
; BASIC ROM and maps in the RAM underlying *it*. I suppose this makes
; sense from a design point of view; after all, BASIC uses the KERNAL, so
; there wouldn't be much sense leaving it mapped when the KERNAL is mapped
; out. Anyway, what this means for us is that we must copy both of these
; ROMs to their respective underlying RAMs if we want to survive returning
; to BASIC.
ldx #>basic
ldy #$c0
jsr copy_block
ldx #>kernal
ldy #$00
jsr copy_block
; To actually substitute the RAM for the ROM in the memory map, we
; set HIRAM (the second least significant bit) to 0.
lda io_ctrl
and #%11111101
sta io_ctrl
rts
; ----- Utility Routine: copy a ROM memory block to the underlying RAM -----
; Input: x register = high byte of start address (low byte = #$00)
; y register = high byte of end address (stops at address $yy00 - 1)
; This subroutine is a fairly straightforward memory copy loop. A somewhat
; counter-intuitive feature is that we immediately store each byte in the
; same location where we just read it from. We can do this because, even
; when the KERNAL or BASIC ROM is mapped in, writes to those locations still
; go to the underlying RAM.
copy_block: stx zp+1
sty stop_at
ldy #$00
sty zp
copy_loop: lda (zp), y
sta (zp), y
iny
cpy #$00
bne copy_loop
ldx zp+1
inx
stx zp+1
cpx stop_at
bne copy_loop
rts
; ----- Variables -----
; 'scanline' stores the raster line that we want the interrupt to trigger
; on; it gets loaded into the VIC-II's 'vic_raster' register.
scanline: .byte %01010101
; We also reserve space to store the address of the interrupt service
; routine that we are replacing in the IRQ vector, so that we can transfer
; control to it at the end of our routine.
.space saved_irq_vec 2
; ----- END of ribos.p65 -----

BIN
eg/c64/ribos/ribos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
eg/c64/ribos/ribos.prg Normal file

Binary file not shown.

BIN
eg/c64/ribos/ribos2-60p.prg Normal file

Binary file not shown.

103
eg/c64/ribos/ribos2.60p Normal file
View File

@ -0,0 +1,103 @@
// ribos2.60p - SixtyPical source for RIBOS2:
// Demonstration of the VIC-II raster interrupt on the Commodore 64:
// Alter the border colour in the middle part of the screen only,
// Simplified (KERNAL IRQ vector) version.
// By Chris Pressey, Cat's Eye Technologies.
// This work has been placed in the public domain.
// For comments, see ribos2.p65.
// ----- Types -----
typedef routine
inputs border_color, vic_intr, scanline
outputs border_color, vic_intr, scanline
trashes a, z, n, c, vic_raster
irq_handler
// ----- Addresses -----
// The CIA #1 chip.
byte cia1 @ $dc00 // pp. 328-331
byte intr_ctrl @ $dc0d // "CIA Interrupt Control Register
// (Read IRQs/Write Mask)"
// The VIC-II chip.
byte vic @ $d000 // Appendix G:
byte vic_ctrl @ $d011 // "Y SCROLL MODE"
byte vic_raster @ $d012 // "RASTER"
byte vic_intr @ $d019 // "Interrupt Request's" (sic)
byte vic_intr_enable @ $d01a // "Interrupt Request MASKS"
byte border_color @ $d020 // "BORDER COLOR"
// The address at which the IRQ vector is stored.
vector irq_handler cinv @ $314 // p. 319, "Vector: Hardware
// IRQ Interrupt"
// ----- Variables -----
vector irq_handler saved_irq_vec
byte scanline : 85 // %01010101
// ----- Externals ------
// An address in ROM which contains "PLA TAY PLA TAX RTI".
// ribos2.p65 just contains these instructions itself, but
// generating them as part of a SixtyPical program would not
// be practical. So we just jump to this location instead.
routine pla_tay_pla_tax_pla_rti
inputs a
trashes a
@ $EA81
// ----- Interrupt Handler -----
define our_service_routine irq_handler
{
ld a, vic_intr
st a, vic_intr
and a, 1
cmp a, 1
if not z {
goto saved_irq_vec
} else {
ld a, border_color
xor a, $ff
st a, border_color
ld a, scanline
xor a, $ff
st a, scanline
st a, vic_raster
trash vic_raster
goto pla_tay_pla_tax_pla_rti
}
}
// ----- Main Program -----
define main routine
inputs cinv, scanline, vic_ctrl, intr_ctrl
outputs cinv, saved_irq_vec, vic_ctrl, vic_intr_enable
trashes a, n, z, vic_raster
{
with interrupts off {
copy cinv, saved_irq_vec
copy our_service_routine, cinv
ld a, scanline
st a, vic_raster
ld a, vic_ctrl
and a, 127
st a, vic_ctrl
ld a, 1
st a, vic_intr_enable
ld a, intr_ctrl
}
}

179
eg/c64/ribos/ribos2.p65 Normal file
View File

@ -0,0 +1,179 @@
; ribos2.p65 - p65 assembly source for RIBOS2:
; Demonstration of the VIC-II raster interrupt on the Commodore 64:
; Alter the border colour in the middle part of the screen only,
; Simplified (KERNAL IRQ vector) version.
; By Chris Pressey, Cat's Eye Technologies.
; This work has been placed in the public domain.
; ----- BEGIN ribos2.p65 -----
; This is a simplified version of ribos.p65 which uses the Commodore 64
; KERNAL's support for interrupt handling. Whereas ribos.p65 could run
; with the KERNAL completely disabled, ribos2.p65 relies on it.
; I'll assume you've read through ribos.p65 at least once, and won't
; say much about the code that's shared between the two programs.
; Again, page references are from the 'Commodore 64 Programmer's
; Reference Guide'.
.org 0
.word $C000
.org $C000
; ----- Constants -----
; The CIA #1 chip.
.alias cia1 $dc00 ; pp. 328-331
.alias intr_ctrl cia1+$d ; "CIA Interrupt Control Register
; (Read IRQs/Write Mask)"
; The VIC-II chip.
.alias vic $d000 ; Appendix G:
.alias vic_ctrl vic+$11 ; "Y SCROLL MODE"
.alias vic_raster vic+$12 ; "RASTER"
.alias vic_intr vic+$19 ; "Interrupt Request's" (sic)
.alias vic_intr_enable vic+$1a ; "Interrupt Request MASKS"
.alias border_color vic+$20 ; "BORDER COLOR"
; The address at which the IRQ vector is stored.
.alias cinv $0314 ; p. 319, "Vector: Hardware
; IRQ Interrupt"
; ----- Main Routine -----
; This routine is intended to be called by the user (by, e.g., SYS 49152).
; It installs the raster interrupt handler and returns to the caller.
; Key to installing the interrupt handler is altering the IRQ service
; vector. As I learned from a careful reading of Sheldon Leemon's
; "Mapping the Commodore 64", if we wish to leave the KERNAL intact and
; operational, it gives us an easy way to do that. Whenever it handles
; an IRQ, it has to decide whether the IRQ was generated by a BRK
; instruction, or some external device issuing an interrupt request. In
; either case, it jumps indirectly through its IRQ-handling vector. We
; can simply modify this vector (at $314) to point to our routine. In
; fact, this is the "CINV" vector, which may already be familiar to you
; if you have done any modest amount of C64 machine language programming;
; it is normally called every 1/60 of a second due to the interrupt
; request generated by CIA#1 Timer B. Here we will simply be using it
; to detect when a raster interrupt has occurred, as well.
; First, disable interrupts before changing interrupt vectors.
sei
; We obtain the address of the current IRQ service routine in CINV
; and save it in the variable 'saved_irq_vec'.
lda cinv
sta saved_irq_vec
lda cinv+1
sta saved_irq_vec+1
; We then store the address of our IRQ service routine in its place.
lda #<our_service_routine
sta cinv
lda #>our_service_routine
sta cinv+1
; Now we must specify the raster line at which the interrupt gets called,
; setting the ninth bit to zero.
lda scanline
sta vic_raster
lda vic_ctrl
and #%01111111
sta vic_ctrl
; Then we enable the raster interrupt on the VIC-II chip.
lda #$01
sta vic_intr_enable
; We read the interrupt control port of CIA #1 for good measure.
lda intr_ctrl
; And we re-enable interrupts to resume normal operation -- plus our jazz.
cli
; Finally, we return to the caller.
rts
; ----- Raster Interrupt Service Routine ------
our_service_routine:
; First, we check to see if the current interrupt was caused by the raster.
lda vic_intr
sta vic_intr
and #$01
cmp #$01
beq we_handle_it
; If the interrupt was not caused by the raster, we continue execution as
; normal, with the existing interrupt service routine.
jmp (saved_irq_vec)
we_handle_it:
; If we got here, the interrupt was caused by the raster, so we do our thing.
lda border_color
eor #$ff
sta border_color
; Now, we make the interrupt trigger on the other scan line next time.
lda scanline
eor #$ff
sta scanline
sta vic_raster
; Return to normal operation. If we simply continue with the standard
; interrupt service routine by jumping through saved_irq_req, we will
; confuse it a bit, as it expects to be called 60 times per second, and
; continuing it here would make it occur more frequently. The result
; would be the cursor blinking more frequently and the time of day clock
; running fast.
; So, we issue a plain return from interrupt (RTI) here. But first, we
; must make sure that the state of the system is back to the way it was
; when the interrupt service routine was called. Since it can be called
; from anywhere, and since the code that was interrupted likely cares
; deeply about the values in its registers, the KERNAL routine which
; dispatches through CINV first carefully saves the contents of the
; registers on the stack. We must be equally careful about restoring
; those values before switching back to that interrupted code.
pla
tay
pla
tax
pla
rti
; ----- Variables -----
; 'scanline' stores the raster line that we want the interrupt to trigger
; on; it gets loaded into the VIC-II's 'vic_raster' register.
scanline: .byte %01010101
; We also reserve space to store the address of the interrupt service
; routine that we are replacing in the IRQ vector, so that we can transfer
; control to it at the end of our routine.
.space saved_irq_vec 2
; ----- END of ribos2.p65 -----

BIN
eg/c64/ribos/ribos2.prg Normal file

Binary file not shown.

16
eg/vic20/hearts.60p Normal file
View File

@ -0,0 +1,16 @@
// Displays 256 hearts at the top of the VIC-20's screen.
// Define where the screen starts in memory:
byte table[256] screen @ 7680
routine main
// These are the values that will be written to by this routine:
trashes a, x, z, n, screen
{
ld x, 0
ld a, 83 // 83 = screen code for heart
repeat {
st a, screen + x
inc x
} until z // this flag will be set when x wraps around from 255 to 0
}

View File

@ -1,6 +1,6 @@
# encoding: UTF-8
from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, If, Repeat, WithInterruptsOff
from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, If, Repeat, For, WithInterruptsOff
from sixtypical.model import (
TYPE_BYTE, TYPE_WORD,
TableType, BufferType, PointerType, VectorType, RoutineType,
@ -92,7 +92,8 @@ class Context(object):
the range of a word is 0..65535.
A location is writeable if it was listed in the outputs and trashes
lists of this routine.
lists of this routine. A location can also be temporarily marked
unwriteable in certain contexts, such as `for` loops.
"""
def __init__(self, routines, routine, inputs, outputs, trashes):
self.routines = routines # Location -> AST node
@ -100,6 +101,7 @@ class Context(object):
self._touched = set()
self._range = dict()
self._writeable = set()
self._has_encountered_goto = False
for ref in inputs:
if ref.is_constant():
@ -206,6 +208,15 @@ class Context(object):
(bottom, _) = self._range[ref]
self._range[ref] = (bottom, top)
def set_bottom_of_range(self, ref, bottom):
self.assert_meaningful(ref)
(top, _) = self._range[ref]
self._range[ref] = (bottom, top)
def set_range(self, ref, bottom, top):
self.assert_meaningful(ref)
self._range[ref] = (bottom, top)
def get_top_of_range(self, ref):
if isinstance(ref, ConstantRef):
return ref.value
@ -213,6 +224,20 @@ class Context(object):
(_, top) = self._range[ref]
return top
def get_bottom_of_range(self, ref):
if isinstance(ref, ConstantRef):
return ref.value
self.assert_meaningful(ref)
(bottom, _) = self._range[ref]
return bottom
def get_range(self, ref):
if isinstance(ref, ConstantRef):
return (ref.value, ref.value)
self.assert_meaningful(ref)
(bottom, top) = self._range[ref]
return bottom, top
def copy_range(self, src, dest):
self.assert_meaningful(src)
if src in self._range:
@ -238,12 +263,27 @@ class Context(object):
self.set_touched(*refs)
self.set_meaningful(*refs)
def set_unwriteable(self, *refs):
"""Intended to be used for implementing analyzing `for`."""
for ref in refs:
self._writeable.remove(ref)
def set_writeable(self, *refs):
"""Intended to be used for implementing analyzing `for`."""
for ref in refs:
self._writeable.add(ref)
def set_encountered_goto(self):
self._has_encountered_goto = True
def has_encountered_goto(self):
return self._has_encountered_goto
class Analyzer(object):
def __init__(self, debug=False):
self.current_routine = None
self.has_encountered_goto = False
self.routines = {}
self.debug = debug
@ -276,7 +316,6 @@ class Analyzer(object):
def analyze_routine(self, routine):
assert isinstance(routine, Routine)
self.current_routine = routine
self.has_encountered_goto = False
if routine.block is None:
# it's an extern, that's fine
return
@ -307,7 +346,7 @@ class Analyzer(object):
if ref in type_.outputs:
raise UnmeaningfulOutputError(routine, ref.name)
if not self.has_encountered_goto:
if not context.has_encountered_goto():
for ref in type_.outputs:
context.assert_meaningful(ref, exception_class=UnmeaningfulOutputError)
for ref in context.each_touched():
@ -318,8 +357,6 @@ class Analyzer(object):
def analyze_block(self, block, context):
assert isinstance(block, Block)
for i in block.instrs:
if self.has_encountered_goto:
raise IllegalJumpError(i, i)
self.analyze_instr(i, context)
def analyze_instr(self, instr, context):
@ -329,6 +366,8 @@ class Analyzer(object):
self.analyze_if(instr, context)
elif isinstance(instr, Repeat):
self.analyze_repeat(instr, context)
elif isinstance(instr, For):
self.analyze_for(instr, context)
elif isinstance(instr, WithInterruptsOff):
self.analyze_block(instr.block, context)
else:
@ -339,7 +378,10 @@ class Analyzer(object):
opcode = instr.opcode
dest = instr.dest
src = instr.src
if context.has_encountered_goto():
raise IllegalJumpError(instr, instr)
if opcode == 'ld':
if isinstance(src, IndexedRef):
if TableType.is_a_table_type(src.ref.type, TYPE_BYTE) and dest.type == TYPE_BYTE:
@ -553,7 +595,7 @@ class Analyzer(object):
self.assert_affected_within('outputs', type_, current_type)
self.assert_affected_within('trashes', type_, current_type)
self.has_encountered_goto = True
context.set_encountered_goto()
elif opcode == 'trash':
context.set_touched(instr.dest)
context.set_unmeaningful(instr.dest)
@ -594,6 +636,8 @@ 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()
for ref in outgoing_trashes:
context.set_touched(ref)
@ -611,3 +655,40 @@ class Analyzer(object):
self.analyze_block(instr.block, context)
if instr.src is not None:
context.assert_meaningful(instr.src)
def analyze_for(self, instr, context):
context.assert_meaningful(instr.dest)
context.assert_writeable(instr.dest)
bottom, top = context.get_range(instr.dest)
final = instr.final.value
if instr.direction > 0:
if top >= final:
raise RangeExceededError(instr, "Top of range of {} is {} but must be lower than {}".format(
instr.dest, top, final
))
top = final
if instr.direction < 0:
if bottom <= final:
raise RangeExceededError(instr, "Bottom of range of {} is {} but must be higher than {}".format(
instr.dest, bottom, final
))
bottom = final
# inside the block, the loop variable cannot be modified, and we know its range.
context.set_range(instr.dest, bottom, top)
context.set_unwriteable(instr.dest)
# it will always be executed at least once, so analyze it having
# been executed the first time.
self.analyze_block(instr.block, context)
# now analyze it having been executed a second time, with the context
# of it having already been executed.
self.analyze_block(instr.block, context)
# after it is executed, we know the range of the loop variable.
context.set_range(instr.dest, instr.final, instr.final)
context.set_writeable(instr.dest)

View File

@ -74,12 +74,17 @@ class SingleOp(Instr):
class If(Instr):
value_attrs = ('src', 'inverted')
value_attrs = ('src', 'inverted',)
child_attrs = ('block1', 'block2',)
class Repeat(Instr):
value_attrs = ('src', 'inverted')
value_attrs = ('src', 'inverted',)
child_attrs = ('block',)
class For(Instr):
value_attrs = ('dest', 'direction', 'final',)
child_attrs = ('block',)

View File

@ -1,6 +1,6 @@
# encoding: UTF-8
from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, If, Repeat, WithInterruptsOff
from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, If, Repeat, For, WithInterruptsOff
from sixtypical.model import (
ConstantRef, LocationRef, IndexedRef, IndirectRef, AddressRef,
TYPE_BIT, TYPE_BYTE, TYPE_WORD,
@ -148,6 +148,8 @@ class Compiler(object):
return self.compile_if(instr)
elif isinstance(instr, Repeat):
return self.compile_repeat(instr)
elif isinstance(instr, For):
return self.compile_for(instr)
elif isinstance(instr, WithInterruptsOff):
return self.compile_with_interrupts_off(instr)
else:
@ -300,31 +302,11 @@ class Compiler(object):
else:
raise UnsupportedOpcodeError(instr)
elif opcode == 'inc':
if dest == REG_X:
self.emitter.emit(INX())
elif dest == REG_Y:
self.emitter.emit(INY())
else:
self.emitter.emit(INC(Absolute(self.get_label(dest.name))))
self.compile_inc(instr, instr.dest)
elif opcode == 'dec':
if dest == REG_X:
self.emitter.emit(DEX())
elif dest == REG_Y:
self.emitter.emit(DEY())
else:
self.emitter.emit(DEC(Absolute(self.get_label(dest.name))))
self.compile_dec(instr, instr.dest)
elif opcode == 'cmp':
cls = {
'a': CMP,
'x': CPX,
'y': CPY,
}.get(dest.name)
if cls is None:
raise UnsupportedOpcodeError(instr)
if isinstance(src, ConstantRef):
self.emitter.emit(cls(Immediate(Byte(src.value))))
else:
self.emitter.emit(cls(Absolute(self.get_label(src.name))))
self.compile_cmp(instr, instr.src, instr.dest)
elif opcode in ('and', 'or', 'xor',):
cls = {
'and': AND,
@ -369,18 +351,45 @@ class Compiler(object):
else:
raise NotImplementedError
elif opcode == 'copy':
self.compile_copy_op(instr)
self.compile_copy(instr, instr.src, instr.dest)
elif opcode == 'trash':
pass
else:
raise NotImplementedError(opcode)
def compile_copy_op(self, instr):
def compile_cmp(self, instr, src, dest):
"""`instr` is only for reporting purposes"""
cls = {
'a': CMP,
'x': CPX,
'y': CPY,
}.get(dest.name)
if cls is None:
raise UnsupportedOpcodeError(instr)
if isinstance(src, ConstantRef):
self.emitter.emit(cls(Immediate(Byte(src.value))))
else:
self.emitter.emit(cls(Absolute(self.get_label(src.name))))
opcode = instr.opcode
dest = instr.dest
src = instr.src
def compile_inc(self, instr, dest):
"""`instr` is only for reporting purposes"""
if dest == REG_X:
self.emitter.emit(INX())
elif dest == REG_Y:
self.emitter.emit(INY())
else:
self.emitter.emit(INC(Absolute(self.get_label(dest.name))))
def compile_dec(self, instr, dest):
"""`instr` is only for reporting purposes"""
if dest == REG_X:
self.emitter.emit(DEX())
elif dest == REG_Y:
self.emitter.emit(DEY())
else:
self.emitter.emit(DEC(Absolute(self.get_label(dest.name))))
def compile_copy(self, instr, src, dest):
if isinstance(src, ConstantRef) and isinstance(dest, IndirectRef) and src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType):
### copy 123, [ptr] + y
dest_label = self.get_label(dest.ref.name)
@ -540,6 +549,20 @@ class Compiler(object):
raise UnsupportedOpcodeError(instr)
self.emitter.emit(cls(Relative(top_label)))
def compile_for(self, instr):
top_label = self.emitter.make_label()
self.compile_block(instr.block)
if instr.direction > 0:
self.compile_inc(instr, instr.dest)
final = instr.final.succ()
elif instr.direction < 0:
self.compile_dec(instr, instr.dest)
final = instr.final.pred()
self.compile_cmp(instr, final, instr.dest)
self.emitter.emit(BNE(Relative(top_label)))
def compile_with_interrupts_off(self, instr):
self.emitter.emit(SEI())
self.compile_block(instr.block)

View File

@ -296,6 +296,20 @@ class ConstantRef(Ref):
def low_byte(self):
return self.value & 255
def pred(self):
assert self.type == TYPE_BYTE
value = self.value - 1
while value < 0:
value += 256
return ConstantRef(self.type, value)
def succ(self):
assert self.type == TYPE_BYTE
value = self.value + 1
while value > 255:
value -= 256
return ConstantRef(self.type, value)
REG_A = LocationRef(TYPE_BYTE, 'a')
REG_X = LocationRef(TYPE_BYTE, 'x')

View File

@ -1,6 +1,6 @@
# encoding: UTF-8
from sixtypical.ast import Program, Defn, Routine, Block, SingleOp, If, Repeat, WithInterruptsOff
from sixtypical.ast import Program, Defn, Routine, Block, SingleOp, If, Repeat, For, WithInterruptsOff
from sixtypical.model import (
TYPE_BIT, TYPE_BYTE, TYPE_WORD,
RoutineType, VectorType, TableType, BufferType, PointerType,
@ -136,11 +136,21 @@ 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 defn_size(self):
self.scanner.expect('[')
self.scanner.check_type('integer literal')
size = int(self.scanner.token)
self.scanner.scan()
size = self.literal_int()
self.scanner.expect(']')
return size
@ -289,11 +299,7 @@ class Parser(object):
self.scanner.scan()
return loc
elif self.scanner.on_type('integer literal'):
value = int(self.scanner.token)
type_ = TYPE_WORD if value > 255 else TYPE_BYTE
loc = ConstantRef(type_, value)
self.scanner.scan()
return loc
return self.literal_int_const()
elif self.scanner.consume('word'):
loc = ConstantRef(TYPE_WORD, int(self.scanner.token))
self.scanner.scan()
@ -372,6 +378,18 @@ class Parser(object):
else:
self.scanner.expect('forever')
return Repeat(self.scanner.line_number, src=src, block=block, inverted=inverted)
elif self.scanner.consume('for'):
dest = self.locexpr()
if self.scanner.consume('down'):
direction = -1
elif self.scanner.consume('up'):
direction = 1
else:
self.syntax_error('expected "up" or "down", found "%s"' % self.scanner.token)
self.scanner.expect('to')
final = self.literal_int_const()
block = self.block()
return For(self.scanner.line_number, dest=dest, direction=direction, final=final, block=block)
elif self.scanner.token in ("ld",):
# the same as add, sub, cmp etc below, except supports an indlocexpr for the src
opcode = self.scanner.token

View File

@ -62,7 +62,7 @@ class Scanner(object):
if self.token == token:
self.scan()
else:
raise SixtyPicalSyntaxError(self.scanner.line_number, "Expected '{}', but found '{}'".format(
raise SixtyPicalSyntaxError(self.line_number, "Expected '{}', but found '{}'".format(
token, self.token
))
@ -74,7 +74,7 @@ class Scanner(object):
def check_type(self, type):
if not self.type == type:
raise SixtyPicalSyntaxError(self.scanner.line_number, "Expected {}, but found '{}'".format(
raise SixtyPicalSyntaxError(self.line_number, "Expected {}, but found '{}'".format(
self.type, self.token
))

View File

@ -1659,6 +1659,204 @@ The body of `repeat forever` can be empty.
| }
= ok
### for ###
Basic "open-faced for" loop. We'll start with the "upto" variant.
In a "for" loop, we know the exact range the loop variable takes on.
| byte table[16] tab
|
| define foo routine inputs tab trashes a, x, c, z, v, n {
| ld x, 0
| for x up to 15 {
| ld a, tab + x
| }
| }
= ok
| byte table[15] tab
|
| define foo routine inputs tab trashes a, x, c, z, v, n {
| ld x, 0
| for x up to 15 {
| ld a, tab + x
| }
| }
? RangeExceededError
You need to initialize the loop variable before the loop.
| byte table[16] tab
|
| define foo routine inputs tab trashes a, x, c, z, v, n {
| for x up to 15 {
| ld a, 0
| }
| }
? UnmeaningfulReadError
You cannot modify the loop variable in a "for" loop.
| byte table[16] tab
|
| define foo routine inputs tab trashes a, x, c, z, v, n {
| ld x, 0
| for x up to 15 {
| ld x, 0
| }
| }
? ForbiddenWriteError
This includes nesting a loop on the same variable.
| byte table[16] tab
|
| define foo routine inputs tab trashes a, x, c, z, v, n {
| ld x, 0
| for x up to 8 {
| for x up to 15 {
| ld a, 0
| }
| }
| }
? ForbiddenWriteError
But nesting with two different variables is okay.
| byte table[16] tab
|
| define foo routine inputs tab trashes a, x, y, c, z, v, n {
| ld x, 0
| for x up to 8 {
| ld a, x
| ld y, a
| for y up to 15 {
| ld a, 0
| }
| }
| }
= ok
Inside the inner loop, the outer variable is still not writeable.
| byte table[16] tab
|
| define foo routine inputs tab trashes a, x, y, c, z, v, n {
| ld x, 0
| for x up to 8 {
| ld a, x
| ld y, a
| for y up to 15 {
| ld x, 0
| }
| }
| }
? ForbiddenWriteError
If the range isn't known to be smaller than the final value, you can't go up to it.
| byte table[32] tab
|
| define foo routine inputs tab trashes a, x, c, z, v, n {
| ld x, 16
| for x up to 15 {
| ld a, tab + x
| }
| }
? RangeExceededError
In a "for" loop (downward-counting variant), we know the exact range the loop variable takes on.
| byte table[16] tab
|
| define foo routine inputs tab trashes a, x, c, z, v, n {
| ld x, 15
| for x down to 0 {
| ld a, tab + x
| }
| }
= ok
| byte table[15] tab
|
| define foo routine inputs tab trashes a, x, c, z, v, n {
| ld x, 15
| for x down to 0 {
| ld a, tab + x
| }
| }
? RangeExceededError
You need to initialize the loop variable before a "for" loop (downward variant).
| byte table[16] tab
|
| define foo routine inputs tab trashes a, x, c, z, v, n {
| for x down to 15 {
| ld a, 0
| }
| }
? UnmeaningfulReadError
You cannot modify the loop variable in a "for" loop (downward variant).
| byte table[16] tab
|
| define foo routine inputs tab trashes a, x, c, z, v, n {
| ld x, 15
| for x down to 0 {
| ld x, 0
| }
| }
? ForbiddenWriteError
If the range isn't known to be larger than the final value, you can't go down to it.
| byte table[32] tab
|
| define foo routine inputs tab trashes a, x, c, z, v, n {
| ld x, 0
| for x down to 0 {
| ld a, tab + x
| }
| }
? RangeExceededError
You can initialize something inside the loop that was uninitialized outside.
| routine main
| outputs x, y, n, z
| trashes c
| {
| ld x, 0
| for x up to 15 {
| ld y, 15
| }
| }
= ok
But you can't UNinitialize something at the end of the loop that you need
initialized at the start of that loop.
| routine foo
| trashes y
| {
| }
|
| routine main
| outputs x, y, n, z
| trashes c
| {
| ld x, 0
| ld y, 15
| for x up to 15 {
| inc y
| call foo
| }
| }
? UnmeaningfulReadError: y
### copy ###
Can't `copy` from a memory location that isn't initialized.
@ -2181,6 +2379,52 @@ Calling the vector does indeed trash the things the vector says it does.
| }
? IllegalJumpError
| routine bar trashes x, z, n {
| ld x, 200
| }
|
| routine main trashes x, z, n {
| ld x, 0
| if z {
| ld x, 1
| goto bar
| } else {
| ld x, 0
| goto bar
| }
| }
= ok
| routine bar trashes x, z, n {
| ld x, 200
| }
|
| routine main trashes x, z, n {
| ld x, 0
| if z {
| ld x, 1
| goto bar
| } else {
| ld x, 0
| }
| }
= ok
For the purposes of `goto`, the end of a loop is never tail position.
| routine bar trashes x, z, n {
| ld x, 200
| }
|
| routine main trashes x, z, n {
| ld x, 0
| repeat {
| inc x
| goto bar
| } until z
| }
? IllegalJumpError
Can't `goto` a routine that outputs or trashes more than the current routine.
| routine bar trashes x, y, z, n {

View File

@ -351,6 +351,46 @@ The body of `repeat forever` can be empty.
= $080D JMP $080D
= $0810 RTS
Compiling `for ... up to`.
| byte table[256] tab
|
| define main routine
| inputs tab
| trashes a, x, c, z, v, n
| {
| ld x, 0
| for x up to 15 {
| ld a, tab + x
| }
| }
= $080D LDX #$00
= $080F LDA $0818,X
= $0812 INX
= $0813 CPX #$10
= $0815 BNE $080F
= $0817 RTS
Compiling `for ... down to`.
| byte table[256] tab
|
| define main routine
| inputs tab
| trashes a, x, c, z, v, n
| {
| ld x, 15
| for x down to 0 {
| ld a, tab + x
| }
| }
= $080D LDX #$0F
= $080F LDA $0818,X
= $0812 DEX
= $0813 CPX #$FF
= $0815 BNE $080F
= $0817 RTS
Indexed access.
| byte one

View File

@ -131,6 +131,22 @@ Repeat with not
| }
= ok
Basic "open-faced for" loops, up and down.
| byte table[256] tab
|
| routine foo trashes a, x, c, z, v {
| ld x, 0
| for x up to 15 {
| ld a, tab + x
| }
| ld x, 15
| for x down to 0 {
| ld a, tab + x
| }
| }
= ok
User-defined memory addresses of different types.
| byte byt