diff --git a/il65/parse.py b/il65/parse.py index 2500623d0..143dea4d3 100644 --- a/il65/parse.py +++ b/il65/parse.py @@ -13,7 +13,7 @@ import sys import shutil import enum from collections import defaultdict -from typing import Set, List, Tuple, Optional, Any, Dict, Union +from typing import Set, List, Tuple, Optional, Any, Dict, Union, Generator from .astparse import ParseError, parse_expr_as_int, parse_expr_as_number, parse_expr_as_primitive,\ parse_expr_as_string, parse_arguments, parse_expr_as_comparison from .symbols import SourceRef, SymbolTable, DataType, SymbolDefinition, SubroutineDef, LabelDef, \ @@ -38,6 +38,12 @@ class ParseResult: self.blocks = [] # type: List['ParseResult.Block'] self.subroutine_usage = defaultdict(set) # type: Dict[Tuple[str, str], Set[str]] + def all_blocks(self) -> Generator['ParseResult.Block', None, None]: + for block in self.blocks: + yield block + for sub in block.symbols.iter_subroutines(True): + yield sub.sub_block + class Block: _unnamed_block_labels = {} # type: Dict[ParseResult.Block, str] @@ -75,18 +81,12 @@ class ParseResult: except (SymbolError, LookupError): return None, None - def flatten_statement_list(self) -> None: - if all(isinstance(stmt, ParseResult._AstNode) for stmt in self.statements): - # this is the common case - return - statements = [] + def all_statements(self) -> Generator[Tuple['ParseResult.Block', Optional[SubroutineDef], 'ParseResult._AstNode'], None, None]: for stmt in self.statements: - if isinstance(stmt, (tuple, list)): - statements.extend(stmt) - else: - assert isinstance(stmt, ParseResult._AstNode) - statements.append(stmt) - self.statements = statements + yield self, None, stmt + for sub in self.symbols.iter_subroutines(True): + for stmt in sub.sub_block.statements: + yield sub.sub_block, sub, stmt class Value: def __init__(self, datatype: DataType, name: str=None, constant: bool=False) -> None: @@ -606,7 +606,10 @@ class Parser: if sys.stderr.isatty(): print("\x1b[1m", file=sys.stderr) print("\nERROR: internal parser error: ", x, file=sys.stderr) - print(" file:", self.sourceref.file, "block:", self.cur_block.name, "line:", self.sourceref.line, file=sys.stderr) + if self.cur_block: + print(" file:", self.sourceref.file, "block:", self.cur_block.name, "line:", self.sourceref.line, file=sys.stderr) + else: + print(" file:", self.sourceref.file, file=sys.stderr) if sys.stderr.isatty(): print("\x1b[0m", file=sys.stderr) raise # XXX temporary solution to get stack trace info in the event of parse errors @@ -697,37 +700,30 @@ class Parser: self.sourceref.line = -1 self.sourceref.column = 0 + def desugar_immediate_strings(stmt: ParseResult._AstNode) -> None: + if isinstance(stmt, ParseResult.CallStmt): + for s in stmt.desugared_call_arguments: + self.sourceref.line = s.lineno + self.sourceref.column = 0 + s.desugar_immediate_string(self) + for s in stmt.desugared_output_assignments: + self.sourceref.line = s.lineno + self.sourceref.column = 0 + s.desugar_immediate_string(self) + if isinstance(stmt, ParseResult.AssignmentStmt): + self.sourceref.line = stmt.lineno + self.sourceref.column = 0 + stmt.desugar_immediate_string(self) + for block in self.result.blocks: self.cur_block = block - # create parameter loads for calls - for index, stmt in enumerate(list(block.statements)): + self.sourceref = block.sourceref.copy() + self.sourceref.column = 0 + for block, sub, stmt in block.all_statements(): if isinstance(stmt, ParseResult.CallStmt): self.sourceref.line = stmt.lineno - self.sourceref.column = 0 stmt.desugar_call_arguments_and_outputs(self) - # create parameter loads for calls, in subroutine blocks - for sub in block.symbols.iter_subroutines(True): - for stmt in sub.sub_block.statements: - if isinstance(stmt, ParseResult.CallStmt): - self.sourceref.line = stmt.lineno - self.sourceref.column = 0 - stmt.desugar_call_arguments_and_outputs(self) - block.flatten_statement_list() - # desugar immediate string value assignments - for index, stmt in enumerate(list(block.statements)): - if isinstance(stmt, ParseResult.CallStmt): - for s in stmt.desugared_call_arguments: - self.sourceref.line = s.lineno - self.sourceref.column = 0 - s.desugar_immediate_string(self) - for s in stmt.desugared_output_assignments: - self.sourceref.line = s.lineno - self.sourceref.column = 0 - s.desugar_immediate_string(self) - if isinstance(stmt, ParseResult.AssignmentStmt): - self.sourceref.line = stmt.lineno - self.sourceref.column = 0 - stmt.desugar_immediate_string(self) + desugar_immediate_strings(stmt) def next_line(self) -> str: self._cur_lineidx += 1 @@ -787,38 +783,50 @@ class Parser: self.result.with_sys = False self.result.format = ProgramFormat.RAW output_specified = False + zp_specified = False while True: self._parse_comments() line = self.next_line() - if line.startswith("output"): + if line.startswith(("output ", "output\t")): if output_specified: - raise self.PError("multiple occurrences of 'output'") + raise self.PError("can only specify output options once") output_specified = True - _, _, arg = line.partition(" ") - arg = arg.lstrip() + _, _, optionstr = line.partition(" ") + options = set(optionstr.replace(' ', '').split(',')) self.result.with_sys = False self.result.format = ProgramFormat.RAW - if arg == "raw": - pass - elif arg == "prg": + if "raw" in options: + options.remove("raw") + if "prg" in options: + options.remove("prg") self.result.format = ProgramFormat.PRG - elif arg.replace(' ', '') == "prg,sys": - self.result.with_sys = True - self.result.format = ProgramFormat.PRG - else: - raise self.PError("invalid output format") - elif line.startswith("clobberzp"): - if self.result.clobberzp: - raise self.PError("multiple occurrences of 'clobberzp'") - self.result.clobberzp = True - _, _, arg = line.partition(" ") - arg = arg.lstrip() - if arg == "restore": - self.result.restorezp = True - elif arg == "": - pass - else: - raise self.PError("invalid arg for clobberzp") + if "basic" in options: + options.remove("basic") + if self.result.format == ProgramFormat.PRG: + self.result.with_sys = True + else: + raise self.PError("can only use basic output option with prg, not raw") + if options: + raise self.PError("invalid output option(s): " + str(options)) + elif line.startswith(("zp ", "zp\t")): + if zp_specified: + raise self.PError("can only specify ZP options once") + zp_specified = True + _, _, optionstr = line.partition(" ") + options = set(optionstr.replace(' ', '').split(',')) + self.result.clobberzp = False + self.result.restorezp = False + if "clobber" in options: + options.remove("clobber") + self.result.clobberzp = True + if "restore" in options: + options.remove("restore") + if self.result.clobberzp: + self.result.restorezp = True + else: + raise self.PError("can only use restore zp option if clobber zp is used as well") + if options: + raise self.PError("invalid zp option(s): " + str(options)) elif line.startswith("address"): if self.result.start_address: raise self.PError("multiple occurrences of 'address'") @@ -1644,17 +1652,12 @@ class Optimizer: def optimize(self) -> ParseResult: print("\noptimizing parse tree") - for block in self.parsed.blocks: + for block in self.parsed.all_blocks(): self.remove_identity_assigns(block) self.combine_assignments_into_multi(block) self.optimize_multiassigns(block) self.remove_unused_subroutines(block) self.optimize_compare_with_zero(block) - for sub in block.symbols.iter_subroutines(True): - self.remove_identity_assigns(sub.sub_block) - self.combine_assignments_into_multi(sub.sub_block) - self.optimize_multiassigns(sub.sub_block) - self.optimize_compare_with_zero(sub.sub_block) return self.parsed def optimize_compare_with_zero(self, block: ParseResult.Block) -> None: diff --git a/reference.md b/reference.md index bdb440866..ff4583ba9 100644 --- a/reference.md +++ b/reference.md @@ -165,13 +165,36 @@ The default format of the generated program is a "raw" binary where code starts You can generate other types of programs as well, by telling IL65 via an output mode statement at the beginning of your program: -| mode declaration | meaning | -|--------------------|------------------------------------------------------------------------------------| -| ``output raw`` | no load address bytes | -| ``output prg`` | include the first two load address bytes, (default is ``$0801``), no BASIC program | -| ``output prg,sys`` | include the first two load address bytes, BASIC start program with sys call to code, default code start is immediately after the BASIC program at ``$081d``, or beyond | -| | | -| ``address $0801`` | override program start address (default is set to ``$c000`` for raw mode and ``$0801`` for C-64 prg mode). Cannot be used if output mode is ``prg,sys`` because BASIC programs always have to start at ``$0801``. | +| mode declaration | meaning | +|----------------------|------------------------------------------------------------------------------------| +| ``output raw`` | no load address bytes | +| ``output prg`` | include the first two load address bytes, (default is ``$0801``), no BASIC program | +| ``output prg,basic`` | as 'prg', but include a BASIC start program with SYS call, default code start is immediately after the BASIC program at ``$081d``, or after that. | +| | | +| ``address $0801`` | override program start address (default is set to ``$c000`` for raw mode and ``$0801`` for C-64 prg mode). Cannot be used if output mode is ``prg,basic`` because BASIC programs always have to start at ``$0801``. | + + +### ZeroPage Options + +You can tell the compiler how to treat the *zero page*. Normally it is considered a 'no-go' area +except for the frew free locations mentioned under "Memory Model". +However you can specify some options globally in your program to change this behavior: + +- ``zp clobber`` + Use the whole zeropage for variables. It is not possible to exit your program + correctly back to BASIC, other than resetting the machine. + It does make your program smaller and faster because many more variables can + be stored in the ZP, which is more efficient. +- ``zp clobber, restore`` + Use the whole zeropage, but make a backup copy of the original values at program start. + When your program exits, the original ZP is restored and you drop back to the BASIC prompt. + +If you use ``zp clobber``, you can no longer use BASIC or KERNAL routines, +because these depend on most of the locations in the ZP. This includes most of the floating-point +logic and several utility routines that do I/O, such as ``print_string``. + +@todo default IRQ handling will still change certain values in ZP... + ### Program Entry Point diff --git a/testsource/conditionals.ill b/testsource/conditionals.ill index 9b071b05a..652ad199e 100644 --- a/testsource/conditionals.ill +++ b/testsource/conditionals.ill @@ -1,4 +1,4 @@ -output prg,sys +output prg,basic import "c64lib" diff --git a/testsource/dtypes.ill b/testsource/dtypes.ill index 57eb47634..7728202b3 100644 --- a/testsource/dtypes.ill +++ b/testsource/dtypes.ill @@ -1,7 +1,8 @@ ; var definitions and immediate primitive data type tests output raw -clobberzp +zp clobber + import "c64lib" diff --git a/testsource/floats.ill b/testsource/floats.ill index c9af77eaf..87c3b5e2a 100644 --- a/testsource/floats.ill +++ b/testsource/floats.ill @@ -1,6 +1,6 @@ ; floating point tests -output prg, sys +output prg, basic import "c64lib" diff --git a/testsource/input.ill b/testsource/input.ill index b79f2e2e7..7ad45dd8d 100644 --- a/testsource/input.ill +++ b/testsource/input.ill @@ -1,4 +1,4 @@ -output prg,sys +output prg,basic import "c64lib" diff --git a/testsource/large.ill b/testsource/large.ill index 043188175..cea963d38 100644 --- a/testsource/large.ill +++ b/testsource/large.ill @@ -1,7 +1,7 @@ ; var definitions and immediate primitive data type tests -output prg, sys -clobberzp +output prg, basic +zp clobber import "c64lib" diff --git a/testsource/numbergame.ill b/testsource/numbergame.ill index 69fce80d4..4fddfca26 100644 --- a/testsource/numbergame.ill +++ b/testsource/numbergame.ill @@ -1,4 +1,4 @@ -output prg,sys ; @todo basic +output prg,basic ;reg_preserve off ; @todo global option import "c64lib" @@ -40,7 +40,13 @@ start printloop c64util.print_string("\nYou have ") c64util.print_byte_decimal(attempts_left) - c64util.print_string(" guesses left.\nWhat is your next guess? ") + c64util.print_string(" guess") + A = attempts_left + A -= 1 ; @todo wrong instruction adc + if_zero A goto ask_guess + c64util.print_string("es") +ask_guess + c64util.print_string(" left.\nWhat is your next guess? ") A = c64util.input_chars(guess) c64.CHROUT('\n') [$22.word] = guess @@ -54,8 +60,8 @@ printloop goto continue correct_guess - c64util.print_string("\nImpressive!\n") - bye() + c64util.print_string("\nThat's my number, impressive!\n") + goodbye() return too_high @@ -71,16 +77,15 @@ game_over c64util.print_string("\nToo bad! It was: ") c64util.print_byte_decimal(secretnumber) c64.CHROUT('\n') - bye() + goodbye() return -sub bye ()->() { +sub goodbye ()->() { ;var x ; @todo vars in sub ;memory y = $c000 ; @todo vars in sub ;const q = 22 ; @todo const in sub - c64.CHROUT('\n') - ;c64util.print_string("Thanks for playing. Bye!\n\n") ;@todo string values should work in sub too + c64util.print_string("\nThanks for playing. Bye!\n") return } diff --git a/testsource/source1.ill b/testsource/source1.ill index b2bd18d05..85885bf5e 100644 --- a/testsource/source1.ill +++ b/testsource/source1.ill @@ -4,9 +4,8 @@ ; line 3 comment -output prg,sys ; create a c-64 program with basic SYS call to launch it -;clobberzp restore ; clobber over the zp memory normally used by basic/kernel rom, frees up more zp - +output basic , prg ; create a c-64 program with basic SYS call to launch it +zp restore , clobber ; clobber over the zp memory normally used by basic/kernel rom, frees up more zp ~main $0a00 diff --git a/testsource/source2.ill b/testsource/source2.ill index 00edddbbe..d979ebd83 100644 --- a/testsource/source2.ill +++ b/testsource/source2.ill @@ -1,8 +1,8 @@ ; source IL file ; these are comments -output prg,sys ; create a c-64 program with basic SYS call to launch it -clobberzp restore ; clobber over the zp memory normally used by basic/kernel rom, frees up more zp +output prg, basic ; create a c-64 program with basic SYS call to launch it +zp clobber,restore ; clobber over the zp memory normally used by basic/kernel rom, frees up more zp ~main { diff --git a/testsource/source3.ill b/testsource/source3.ill index 1f1c7781f..6209bb316 100644 --- a/testsource/source3.ill +++ b/testsource/source3.ill @@ -1,8 +1,5 @@ -; source IL file - ; these are comments -output prg,sys ; create a c-64 program with basic SYS call to launch it -; clobberzp restore ; clobber over the zp memory normally used by basic/kernel rom, frees up more zp +output prg,basic ; create a c-64 program with basic SYS call to launch it import "c64lib" ; searched in several locations and with .ill file extension added diff --git a/testsource/source4.ill b/testsource/source4.ill index 3b6b2d65a..8caac1b4c 100644 --- a/testsource/source4.ill +++ b/testsource/source4.ill @@ -1,4 +1,4 @@ -output prg,sys ; create a c-64 program with basic SYS to() launch it +output prg,basic ; create a c-64 program with basic SYS to() launch it import "c64lib.ill"