diff --git a/HISTORY.md b/HISTORY.md index 98440e2..3f6ebdb 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -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 ---- diff --git a/README.md b/README.md index c5f30a8..1c25b7a 100644 --- a/README.md +++ b/README.md @@ -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/ diff --git a/bin/sixtypical b/bin/sixtypical index 710926a..2197c2b 100755 --- a/bin/sixtypical +++ b/bin/sixtypical @@ -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) diff --git a/doc/SixtyPical.md b/doc/SixtyPical.md index 22b5604..1955fe8 100644 --- a/doc/SixtyPical.md +++ b/doc/SixtyPical.md @@ -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 Const. TypeDefn::= "typedef" Type Ident. - Defn ::= Type Ident [Constraints] (":" Literal | "@" LitWord). + Defn ::= Type Ident [Constraints] (":" Const | "@" LitWord). Type ::= TypeTerm ["table" TypeSize]. TypeExpr::= "byte" | "word" @@ -573,12 +574,13 @@ Grammar | "routine" Ident 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. 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 . diff --git a/eg/README.md b/eg/README.md index 79c0d91..51c3d70 100644 --- a/eg/README.md +++ b/eg/README.md @@ -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/ diff --git a/eg/atari2600/.gitignore b/eg/atari2600/.gitignore new file mode 100644 index 0000000..92ac558 --- /dev/null +++ b/eg/atari2600/.gitignore @@ -0,0 +1,2 @@ +*.bin +*.disasm.txt diff --git a/eg/atari2600/atari-2600-example.oph b/eg/atari2600/atari-2600-example.oph new file mode 100644 index 0000000..836a64a --- /dev/null +++ b/eg/atari2600/atari-2600-example.oph @@ -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 diff --git a/eg/atari2600/build.sh b/eg/atari2600/build.sh new file mode 100755 index 0000000..be05acf --- /dev/null +++ b/eg/atari2600/build.sh @@ -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 diff --git a/eg/atari2600/smiley.60p b/eg/atari2600/smiley.60p new file mode 100644 index 0000000..eebf314 --- /dev/null +++ b/eg/atari2600/smiley.60p @@ -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 +} diff --git a/eg/atari2600/smiley.oph b/eg/atari2600/smiley.oph new file mode 100644 index 0000000..19d4335 --- /dev/null +++ b/eg/atari2600/smiley.oph @@ -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 diff --git a/eg/c64/petulant/petulant-60p.prg b/eg/c64/petulant/petulant-60p.prg index 2ce0000..3ecb862 100644 Binary files a/eg/c64/petulant/petulant-60p.prg and b/eg/c64/petulant/petulant-60p.prg differ diff --git a/eg/c64/ribos/ribos2-60p.prg b/eg/c64/ribos/ribos2-60p.prg index c11bfc5..35896e0 100644 Binary files a/eg/c64/ribos/ribos2-60p.prg and b/eg/c64/ribos/ribos2-60p.prg differ diff --git a/eg/rudiments/vector-inc.60p b/eg/rudiments/vector-inc.60p new file mode 100644 index 0000000..69a69c7 --- /dev/null +++ b/eg/rudiments/vector-inc.60p @@ -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 +} diff --git a/eg/rudiments/vector-main.60p b/eg/rudiments/vector-main.60p new file mode 100644 index 0000000..b6e45e9 --- /dev/null +++ b/eg/rudiments/vector-main.60p @@ -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 +} diff --git a/loadngo.sh b/loadngo.sh index b3d0e1d..ca9f198 100755 --- a/loadngo.sh +++ b/loadngo.sh @@ -1,6 +1,6 @@ #!/bin/sh -usage="Usage: loadngo.sh (c64|vic20) [--dry-run] " +usage="Usage: loadngo.sh (c64|vic20|atari2600) [--dry-run] " 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 diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 6dc3262..df76933 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -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) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index 11d2e9e..de01fba 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -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: diff --git a/src/sixtypical/emitter.py b/src/sixtypical/emitter.py index aa14a59..a1b962c 100644 --- a/src/sixtypical/emitter.py +++ b/src/sixtypical/emitter.py @@ -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) diff --git a/src/sixtypical/fallthru.py b/src/sixtypical/fallthru.py new file mode 100644 index 0000000..85024bd --- /dev/null +++ b/src/sixtypical/fallthru.py @@ -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 diff --git a/src/sixtypical/gen6502.py b/src/sixtypical/gen6502.py index 8659ab6..f166391 100644 --- a/src/sixtypical/gen6502.py +++ b/src/sixtypical/gen6502.py @@ -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, diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 2391d05..fa05526 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -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() diff --git a/src/sixtypical/scanner.py b/src/sixtypical/scanner.py index d15ac4f..a85da67 100644 --- a/src/sixtypical/scanner.py +++ b/src/sixtypical/scanner.py @@ -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) diff --git a/test.sh b/test.sh index 931611f..bed307e 100755 --- a/test.sh +++ b/test.sh @@ -3,4 +3,5 @@ falderal --substring-error \ tests/SixtyPical\ Syntax.md \ tests/SixtyPical\ Analysis.md \ + tests/SixtyPical\ Fallthru.md \ tests/SixtyPical\ Compilation.md diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index 2cbba37..2b279a9 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -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. diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index 0021f0c..d6562d0 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -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 diff --git a/tests/SixtyPical Fallthru.md b/tests/SixtyPical Fallthru.md new file mode 100644 index 0000000..28023ca --- /dev/null +++ b/tests/SixtyPical Fallthru.md @@ -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 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 diff --git a/tests/SixtyPical Syntax.md b/tests/SixtyPical Syntax.md index 2add5ea..feddf0b 100644 --- a/tests/SixtyPical Syntax.md +++ b/tests/SixtyPical Syntax.md @@ -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 {