diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 5c5c1fa..ecb3b57 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -1,6 +1,6 @@ # encoding: UTF-8 -from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, If, Repeat, For, WithInterruptsOff +from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, If, Repeat, For, WithInterruptsOff, Save from sixtypical.model import ( TYPE_BYTE, TYPE_WORD, TableType, BufferType, PointerType, VectorType, RoutineType, @@ -269,7 +269,7 @@ class Context(object): self._writeable.remove(ref) def set_writeable(self, *refs): - """Intended to be used for implementing analyzing `for`.""" + """Intended to be used for implementing analyzing `for`, but also used in `save`.""" for ref in refs: self._writeable.add(ref) @@ -292,6 +292,41 @@ class Context(object): self.assert_in_range(dest.index, dest.ref) self.set_written(dest.ref) + def extract(self, location): + """Sets the given location as writeable in the context, and returns a 'baton' representing + the previous state of context for that location. This 'baton' can be used to later restore + this state of context.""" + # Used in `save`. + baton = ( + location, + location in self._touched, + self._range.get(location, None), + location in self._writeable, + ) + self.set_writeable(location) + return baton + + def re_introduce(self, baton): + """Given a 'baton' produced by `extract()`, restores the context for that saved location + to what it was before `extract()` was called.""" + # Used in `save`. + location, was_touched, was_range, was_writeable = baton + + if was_touched: + self._touched.add(location) + elif location in self._touched: + self._touched.remove(location) + + if was_range is not None: + self._range[location] = was_range + elif location in self._range: + del self._range[location] + + if was_writeable: + self._writeable.add(location) + elif location in self._writeable: + self._writeable.remove(location) + class Analyzer(object): @@ -300,9 +335,9 @@ class Analyzer(object): self.routines = {} self.debug = debug - def assert_type(self, type, *locations): + def assert_type(self, type_, *locations): for location in locations: - if location.type != type: + if location.type != type_: raise TypeMismatchError(self.current_routine, location.name) def assert_affected_within(self, name, affecting_type, limiting_type): @@ -385,6 +420,10 @@ class Analyzer(object): self.analyze_for(instr, context) elif isinstance(instr, WithInterruptsOff): self.analyze_block(instr.block, context) + if context.encountered_gotos(): + raise IllegalJumpError(instr, instr) + elif isinstance(instr, Save): + self.analyze_save(instr, context) else: raise NotImplementedError @@ -427,7 +466,7 @@ class Analyzer(object): context.assert_meaningful(dest.ref, REG_Y) context.set_written(dest.ref) elif src.type != dest.type: - raise TypeMismatchError(instr, '{} and {}'.format(src, name)) + raise TypeMismatchError(instr, '{} and {}'.format(src, dest)) else: context.set_written(dest) # FIXME: context.copy_range(src, dest) ? @@ -730,3 +769,21 @@ class Analyzer(object): # 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) + + def analyze_save(self, instr, context): + if len(instr.locations) != 1: + raise NotImplementedError("Only 1 location in save is supported right now") + location = instr.locations[0] + self.assert_type(TYPE_BYTE, location) + + baton = context.extract(location) + self.analyze_block(instr.block, context) + if context.encountered_gotos(): + raise IllegalJumpError(instr, instr) + context.re_introduce(baton) + + if location == REG_A: + pass + else: + context.set_touched(REG_A) + context.set_unmeaningful(REG_A) diff --git a/src/sixtypical/ast.py b/src/sixtypical/ast.py index afd2944..718636f 100644 --- a/src/sixtypical/ast.py +++ b/src/sixtypical/ast.py @@ -90,3 +90,8 @@ class For(Instr): class WithInterruptsOff(Instr): child_attrs = ('block',) + + +class Save(Instr): + value_attrs = ('locations',) + child_attrs = ('block',) diff --git a/src/sixtypical/compiler.py b/src/sixtypical/compiler.py index a7d0f64..34ed70f 100644 --- a/src/sixtypical/compiler.py +++ b/src/sixtypical/compiler.py @@ -1,6 +1,6 @@ # encoding: UTF-8 -from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, If, Repeat, For, WithInterruptsOff +from sixtypical.ast import Program, Routine, Block, Instr, SingleOp, If, Repeat, For, WithInterruptsOff, Save from sixtypical.model import ( ConstantRef, LocationRef, IndexedRef, IndirectRef, AddressRef, TYPE_BIT, TYPE_BYTE, TYPE_WORD, @@ -12,6 +12,7 @@ from sixtypical.gen6502 import ( Immediate, Absolute, AbsoluteX, AbsoluteY, ZeroPage, Indirect, IndirectY, Relative, LDA, LDX, LDY, STA, STX, STY, TAX, TAY, TXA, TYA, + PHA, PLA, CLC, SEC, ADC, SBC, ROL, ROR, INC, INX, INY, DEC, DEX, DEY, CMP, CPX, CPY, AND, ORA, EOR, @@ -169,6 +170,8 @@ class Compiler(object): return self.compile_for(instr) elif isinstance(instr, WithInterruptsOff): return self.compile_with_interrupts_off(instr) + elif isinstance(instr, Save): + return self.compile_save(instr) else: raise NotImplementedError @@ -613,3 +616,29 @@ class Compiler(object): self.emitter.emit(SEI()) self.compile_block(instr.block) self.emitter.emit(CLI()) + + def compile_save(self, instr): + location = instr.locations[0] + if location == REG_A: + self.emitter.emit(PHA()) + self.compile_block(instr.block) + self.emitter.emit(PLA()) + elif location == REG_X: + self.emitter.emit(TXA()) + self.emitter.emit(PHA()) + self.compile_block(instr.block) + self.emitter.emit(PLA()) + self.emitter.emit(TAX()) + elif location == REG_Y: + self.emitter.emit(TYA()) + self.emitter.emit(PHA()) + self.compile_block(instr.block) + self.emitter.emit(PLA()) + self.emitter.emit(TAY()) + else: + src_label = self.get_label(location.name) + self.emitter.emit(LDA(Absolute(src_label))) + self.emitter.emit(PHA()) + self.compile_block(instr.block) + self.emitter.emit(PLA()) + self.emitter.emit(STA(Absolute(src_label))) diff --git a/src/sixtypical/gen6502.py b/src/sixtypical/gen6502.py index c3c91d9..7b8d07c 100644 --- a/src/sixtypical/gen6502.py +++ b/src/sixtypical/gen6502.py @@ -306,6 +306,18 @@ class ORA(Instruction): } +class PHA(Instruction): + opcodes = { + Implied: 0x48, + } + + +class PLA(Instruction): + opcodes = { + Implied: 0x68, + } + + class ROL(Instruction): opcodes = { Implied: 0x2a, # Accumulator diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index 7ca2f69..e16cf66 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -1,6 +1,6 @@ # encoding: UTF-8 -from sixtypical.ast import Program, Defn, Routine, Block, SingleOp, If, Repeat, For, WithInterruptsOff +from sixtypical.ast import Program, Defn, Routine, Block, SingleOp, If, Repeat, For, WithInterruptsOff, Save from sixtypical.model import ( TYPE_BIT, TYPE_BYTE, TYPE_WORD, RoutineType, VectorType, TableType, BufferType, PointerType, @@ -470,6 +470,10 @@ class Parser(object): self.scanner.expect("off") block = self.block() return WithInterruptsOff(self.scanner.line_number, block=block) + elif self.scanner.consume("save"): + locations = self.locexprs() + block = self.block() + return Save(self.scanner.line_number, locations=locations, block=block) elif self.scanner.consume("trash"): dest = self.locexpr() return SingleOp(self.scanner.line_number, opcode='trash', src=None, dest=dest) diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index 4670794..3e75d97 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -1938,6 +1938,290 @@ initialized at the start of that loop. | } ? UnmeaningfulReadError: y +### save ### + +Basic neutral test, where the `save` makes no difference. + + | routine main + | inputs a, x + | outputs a, x + | trashes z, n + | { + | ld a, 1 + | save x { + | ld a, 2 + | } + | ld a, 3 + | } + = ok + +Saving any location (other than `a`) will trash `a`. + + | routine main + | inputs a, x + | outputs a, x + | trashes z, n + | { + | ld a, 1 + | save x { + | ld a, 2 + | } + | } + ? UnmeaningfulOutputError + +Saving `a` does not trash anything. + + | routine main + | inputs a, x + | outputs a, x + | trashes z, n + | { + | ld x, 1 + | save a { + | ld x, 2 + | } + | ld x, 3 + | } + = ok + +A defined value that has been saved can be trashed inside the block. +It will continue to be defined outside the block. + + | routine main + | outputs x, y + | trashes a, z, n + | { + | ld x, 0 + | save x { + | ld y, 0 + | trash x + | } + | } + = ok + +A trashed value that has been saved can be used inside the block. +It will continue to be trashed outside the block. + + | routine main + | inputs a + | outputs a, x + | trashes z, n + | { + | ld x, 0 + | trash x + | save x { + | ld a, 0 + | ld x, 1 + | } + | } + ? UnmeaningfulOutputError: x + +The known range of a value will be preserved outside the block as well. + + | word one: 77 + | word table[32] many + | + | routine main + | inputs a, many, one + | outputs many, one + | trashes a, x, n, z + | { + | and a, 31 + | ld x, a + | save x { + | ld x, 255 + | } + | copy one, many + x + | copy many + x, one + | } + = ok + + | word one: 77 + | word table[32] many + | + | routine main + | inputs a, many, one + | outputs many, one + | trashes a, x, n, z + | { + | and a, 63 + | ld x, a + | save x { + | ld x, 1 + | } + | copy one, many + x + | copy many + x, one + | } + ? RangeExceededError + +The known properties of a value are preserved inside the block, too. + + | word one: 77 + | word table[32] many + | + | routine main + | inputs a, many, one + | outputs many, one + | trashes a, x, n, z + | { + | and a, 31 + | ld x, a + | save x { + | copy one, many + x + | copy many + x, one + | } + | copy one, many + x + | copy many + x, one + | } + = ok + +A value which is not output from the routine, is preserved by the +routine; and can appear in a `save` exactly because a `save` preserves it. + + | routine main + | outputs y + | trashes a, z, n + | { + | save x { + | ld y, 0 + | ld x, 1 + | } + | } + = ok + +Because saving anything except `a` trashes `a`, a common idiom is to save `a` +first in a nested series of `save`s. + + | routine main + | inputs a + | outputs a + | trashes z, n + | { + | save a { + | save x { + | ld a, 0 + | ld x, 1 + | } + | } + | } + = ok + +Not just registers, but also user-defined locations can be saved. + + | byte foo + | + | routine main + | trashes a, z, n + | { + | save foo { + | st 5, foo + | } + | } + = ok + +But only if they are bytes. + + | word foo + | + | routine main + | trashes a, z, n + | { + | save foo { + | copy 555, foo + | } + | } + ? TypeMismatchError + + | byte table[16] tab + | + | routine main + | trashes a, y, z, n + | { + | save tab { + | ld y, 0 + | st 5, tab + y + | } + | } + ? TypeMismatchError + +A `goto` cannot appear within a `save` block, even if it is otherwise in tail position. + + | routine other + | trashes a, z, n + | { + | ld a, 0 + | } + | + | routine main + | trashes a, z, n + | { + | ld a, 1 + | save x { + | ld x, 2 + | goto other + | } + | } + ? IllegalJumpError + +### with interrupts ### + + | vector routine + | inputs x + | outputs x + | trashes z, n + | bar + | + | routine foo + | inputs x + | outputs x + | trashes z, n + | { + | inc x + | } + | + | routine main + | outputs bar + | trashes a, n, z + | { + | with interrupts off { + | copy foo, bar + | } + | } + = ok + +A `goto` cannot appear within a `with interrupts` block, even if it is +otherwise in tail position. + + | vector routine + | inputs x + | outputs x + | trashes z, n + | bar + | + | routine foo + | inputs x + | outputs x + | trashes z, n + | { + | inc x + | } + | + | routine other + | trashes bar, a, n, z + | { + | ld a, 0 + | } + | + | routine main + | trashes bar, a, n, z + | { + | with interrupts off { + | copy foo, bar + | goto other + | } + | } + ? IllegalJumpError + ### copy ### Can't `copy` from a memory location that isn't initialized. diff --git a/tests/SixtyPical Compilation.md b/tests/SixtyPical Compilation.md index fb45bc0..955e21e 100644 --- a/tests/SixtyPical Compilation.md +++ b/tests/SixtyPical Compilation.md @@ -579,6 +579,49 @@ Compiling `for ... down to`. = $0815 BNE $080F = $0817 RTS +Compiling `save`. + + | routine main + | inputs a + | outputs a + | trashes z, n + | { + | save a { + | save x { + | ld a, 0 + | ld x, 1 + | } + | } + | } + = $080D PHA + = $080E TXA + = $080F PHA + = $0810 LDA #$00 + = $0812 LDX #$01 + = $0814 PLA + = $0815 TAX + = $0816 PLA + = $0817 RTS + +Compiling `save` on a user-defined location. + + | byte foo + | routine main + | trashes a, z, n + | { + | save foo { + | ld a, 0 + | st a, foo + | } + | } + = $080D LDA $081B + = $0810 PHA + = $0811 LDA #$00 + = $0813 STA $081B + = $0816 PLA + = $0817 STA $081B + = $081A RTS + Indexed access. | byte one diff --git a/tests/SixtyPical Syntax.md b/tests/SixtyPical Syntax.md index 88bdc67..e2ef953 100644 --- a/tests/SixtyPical Syntax.md +++ b/tests/SixtyPical Syntax.md @@ -161,6 +161,20 @@ Basic "open-faced for" loops, up and down. | } = ok +Other blocks. + + | routine main trashes a, x, c, z, v { + | with interrupts off { + | save a, x, c { + | ld a, 0 + | } + | } + | save a, x, c { + | ld a, 0 + | } + | } + = ok + User-defined memory addresses of different types. | byte byt