prog8/il65/parse.py

1827 lines
92 KiB
Python
Raw Normal View History

2017-12-21 13:52:30 +00:00
"""
2017-12-25 15:00:25 +00:00
Programming Language for 6502/6510 microprocessors
2017-12-21 13:52:30 +00:00
This is the parser of the IL65 code, that generates a parse tree.
Written by Irmen de Jong (irmen@razorvine.net)
License: GNU GPL 3.0, see LICENSE
"""
import math
2017-12-21 13:52:30 +00:00
import re
import os
2017-12-26 00:30:22 +00:00
import sys
2017-12-21 13:52:30 +00:00
import shutil
import enum
2017-12-25 18:09:10 +00:00
from collections import defaultdict
2017-12-28 22:03:59 +00:00
from typing import Set, List, Tuple, Optional, Any, Dict, Union, Generator
2017-12-21 13:52:30 +00:00
from .astparse import ParseError, parse_expr_as_int, parse_expr_as_number, parse_expr_as_primitive,\
2017-12-27 18:01:14 +00:00
parse_expr_as_string, parse_arguments, parse_expr_as_comparison
2017-12-21 13:52:30 +00:00
from .symbols import SourceRef, SymbolTable, DataType, SymbolDefinition, SubroutineDef, LabelDef, \
2017-12-29 02:52:26 +00:00
Zeropage, check_value_in_range, char_to_bytevalue, \
PrimitiveType, VariableDef, ConstantDef, SymbolError, STRING_DATATYPES, \
2017-12-28 03:20:59 +00:00
REGISTER_SYMBOLS, REGISTER_WORDS, REGISTER_BYTES, REGISTER_SBITS, RESERVED_NAMES
2017-12-21 13:52:30 +00:00
class ProgramFormat(enum.Enum):
PRG = "prg"
RAW = "raw"
class ParseResult:
def __init__(self, sourcefile: str) -> None:
self.format = ProgramFormat.RAW
self.with_sys = False
self.sourcefile = sourcefile
self.clobberzp = False
self.restorezp = False
self.start_address = 0
self.blocks = [] # type: List['ParseResult.Block']
2017-12-25 18:09:10 +00:00
self.subroutine_usage = defaultdict(set) # type: Dict[Tuple[str, str], Set[str]]
2017-12-29 02:52:26 +00:00
self.zeropage = Zeropage()
2017-12-21 13:52:30 +00:00
2017-12-28 22:03:59 +00:00
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
2017-12-21 13:52:30 +00:00
class Block:
_unnamed_block_labels = {} # type: Dict[ParseResult.Block, str]
def __init__(self, name: str, sourceref: SourceRef, parent_scope: SymbolTable) -> None:
2017-12-21 22:05:35 +00:00
self.sourceref = sourceref.copy()
2017-12-21 13:52:30 +00:00
self.address = 0
self.name = name
self.statements = [] # type: List[ParseResult._AstNode]
2017-12-21 13:52:30 +00:00
self.symbols = SymbolTable(name, parent_scope, self)
2017-12-28 18:08:33 +00:00
@property
def ignore(self) -> bool:
return not self.name and not self.address
2017-12-21 13:52:30 +00:00
@property
def label_names(self) -> Set[str]:
return {symbol.name for symbol in self.symbols.iter_labels()}
@property
def label(self) -> str:
if self.name:
return self.name
if self in self._unnamed_block_labels:
return self._unnamed_block_labels[self]
label = "il65_block_{:d}".format(len(self._unnamed_block_labels))
self._unnamed_block_labels[self] = label
return label
def lookup(self, dottedname: str) -> Tuple[Optional['ParseResult.Block'], Optional[Union[SymbolDefinition, SymbolTable]]]:
2017-12-25 21:22:19 +00:00
# Searches a name in the current block or globally, if the name is scoped (=contains a '.').
# Does NOT utilize a symbol table from a preprocessing parse phase, only looks in the current.
2017-12-21 13:52:30 +00:00
try:
scope, result = self.symbols.lookup(dottedname)
return scope.owning_block, result
except (SymbolError, LookupError):
return None, None
2017-12-28 22:03:59 +00:00
def all_statements(self) -> Generator[Tuple['ParseResult.Block', Optional[SubroutineDef], 'ParseResult._AstNode'], None, None]:
2017-12-21 13:52:30 +00:00
for stmt in self.statements:
2017-12-28 22:03:59 +00:00
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
2017-12-21 13:52:30 +00:00
class Value:
2017-12-23 00:53:48 +00:00
def __init__(self, datatype: DataType, name: str=None, constant: bool=False) -> None:
2017-12-21 13:52:30 +00:00
self.datatype = datatype
self.name = name
self.constant = constant
def assignable_from(self, other: 'ParseResult.Value') -> Tuple[bool, str]:
if self.constant:
return False, "cannot assign to a constant"
return False, "incompatible value for assignment"
2017-12-23 00:53:48 +00:00
class IndirectValue(Value):
2017-12-28 18:08:33 +00:00
# only constant integers, memmapped and register values are wrapped in this.
2017-12-23 00:53:48 +00:00
def __init__(self, value: 'ParseResult.Value', type_modifier: DataType) -> None:
2017-12-23 13:36:23 +00:00
assert type_modifier
2017-12-23 00:53:48 +00:00
super().__init__(type_modifier, value.name, False)
self.value = value
def __str__(self):
return "<IndirectValue {} itype={} name={}>".format(self.value, self.datatype, self.name)
2017-12-24 23:15:04 +00:00
def __hash__(self):
return hash((self.datatype, self.name, self.value))
def __eq__(self, other: Any) -> bool:
if not isinstance(other, ParseResult.IndirectValue):
return NotImplemented
elif self is other:
return True
else:
vvo = getattr(other.value, "value", getattr(other.value, "address", None))
vvs = getattr(self.value, "value", getattr(self.value, "address", None))
return (other.datatype, other.name, other.value.name, other.value.datatype, other.value.constant, vvo) ==\
(self.datatype, self.name, self.value.name, self.value.datatype, self.value.constant, vvs)
2017-12-23 00:53:48 +00:00
def assignable_from(self, other: 'ParseResult.Value') -> Tuple[bool, str]:
if self.constant:
return False, "cannot assign to a constant"
2017-12-23 13:36:23 +00:00
if self.datatype == DataType.BYTE:
if other.datatype == DataType.BYTE:
return True, ""
if self.datatype == DataType.WORD:
if other.datatype in {DataType.BYTE, DataType.WORD} | STRING_DATATYPES:
return True, ""
if self.datatype == DataType.FLOAT:
if other.datatype in {DataType.BYTE, DataType.WORD, DataType.FLOAT}:
return True, ""
if isinstance(other, (ParseResult.IntegerValue, ParseResult.FloatValue, ParseResult.StringValue)):
rangefault = check_value_in_range(self.datatype, "", 1, other.value)
if rangefault:
return False, rangefault
2017-12-23 00:53:48 +00:00
return True, ""
return False, "incompatible value for indirect assignment (need byte, word, float or string)"
2017-12-21 13:52:30 +00:00
class IntegerValue(Value):
def __init__(self, value: Optional[int], *, datatype: DataType=None, name: str=None) -> None:
if type(value) is int:
if datatype is None:
if 0 <= value < 0x100:
datatype = DataType.BYTE
elif value < 0x10000:
datatype = DataType.WORD
else:
raise OverflowError("value too big: ${:x}".format(value))
else:
faultreason = check_value_in_range(datatype, "", 1, value)
if faultreason:
raise OverflowError(faultreason)
super().__init__(datatype, name, True)
self.value = value
elif value is None:
if not name:
raise ValueError("when integer value is not given, the name symbol should be speicified")
super().__init__(datatype, name, True)
self.value = None
else:
raise TypeError("invalid data type")
def __hash__(self):
return hash((self.datatype, self.value, self.name))
def __eq__(self, other: Any) -> bool:
if not isinstance(other, ParseResult.IntegerValue):
return NotImplemented
elif self is other:
return True
else:
2017-12-24 23:15:04 +00:00
return (other.datatype, other.value, other.name) == (self.datatype, self.value, self.name)
2017-12-21 13:52:30 +00:00
def __str__(self):
return "<IntegerValue {} name={}>".format(self.value, self.name)
class FloatValue(Value):
def __init__(self, value: float, name: str=None) -> None:
if type(value) is float:
super().__init__(DataType.FLOAT, name, True)
self.value = value
else:
raise TypeError("invalid data type")
def __hash__(self):
return hash((self.datatype, self.value, self.name))
def __eq__(self, other: Any) -> bool:
if not isinstance(other, ParseResult.FloatValue):
return NotImplemented
elif self is other:
return True
else:
2017-12-24 23:15:04 +00:00
return (other.datatype, other.value, other.name) == (self.datatype, self.value, self.name)
2017-12-21 13:52:30 +00:00
def __str__(self):
return "<FloatValue {} name={}>".format(self.value, self.name)
class StringValue(Value):
def __init__(self, value: str, name: str=None, constant: bool=False) -> None:
super().__init__(DataType.STRING, name, constant)
self.value = value
def __hash__(self):
2017-12-24 23:15:04 +00:00
return hash((self.datatype, self.value, self.name, self.constant))
2017-12-21 13:52:30 +00:00
def __eq__(self, other: Any) -> bool:
if not isinstance(other, ParseResult.StringValue):
return NotImplemented
elif self is other:
return True
else:
2017-12-24 23:15:04 +00:00
return (other.datatype, other.value, other.name, other.constant) == (self.datatype, self.value, self.name, self.constant)
2017-12-21 13:52:30 +00:00
def __str__(self):
return "<StringValue {!r:s} name={} constant={}>".format(self.value, self.name, self.constant)
class RegisterValue(Value):
def __init__(self, register: str, datatype: DataType, name: str=None) -> None:
assert datatype in (DataType.BYTE, DataType.WORD)
assert register in REGISTER_SYMBOLS
super().__init__(datatype, name, False)
self.register = register
def __hash__(self):
return hash((self.datatype, self.register, self.name))
def __eq__(self, other: Any) -> bool:
if not isinstance(other, ParseResult.RegisterValue):
return NotImplemented
elif self is other:
return True
else:
2017-12-24 23:15:04 +00:00
return (other.datatype, other.register, other.name) == (self.datatype, self.register, self.name)
2017-12-21 13:52:30 +00:00
def __str__(self):
return "<RegisterValue {:s} type {:s} name={}>".format(self.register, self.datatype, self.name)
def assignable_from(self, other: 'ParseResult.Value') -> Tuple[bool, str]:
2017-12-23 00:53:48 +00:00
if isinstance(other, ParseResult.IndirectValue):
2017-12-23 13:36:23 +00:00
if self.datatype == DataType.BYTE:
if other.datatype == DataType.BYTE:
return True, ""
return False, "(unsigned) byte required"
if self.datatype == DataType.WORD:
if other.datatype in (DataType.BYTE, DataType.WORD):
return True, ""
return False, "(unsigned) byte required"
return False, "incompatible indirect value for register assignment"
2017-12-25 20:43:06 +00:00
if self.register in ("SC", "SI"):
2017-12-21 13:52:30 +00:00
if isinstance(other, ParseResult.IntegerValue) and other.value in (0, 1):
return True, ""
2017-12-25 20:43:06 +00:00
return False, "can only assign an integer constant value of 0 or 1 to SC and SI"
2017-12-21 13:52:30 +00:00
if self.constant:
return False, "cannot assign to a constant"
2017-12-27 18:01:14 +00:00
if isinstance(other, ParseResult.RegisterValue):
if other.register in {"SI", "SC", "SZ"}:
return False, "cannot explicitly assign from a status bit register alias"
if len(self.register) < len(other.register):
return False, "register size mismatch"
2017-12-28 03:20:59 +00:00
if isinstance(other, ParseResult.StringValue) and self.register in REGISTER_BYTES | REGISTER_SBITS:
2017-12-21 13:52:30 +00:00
return False, "string address requires 16 bits combined register"
2017-12-23 13:36:23 +00:00
if isinstance(other, ParseResult.IntegerValue):
if other.value is not None:
range_error = check_value_in_range(self.datatype, self.register, 1, other.value)
if range_error:
return False, range_error
return True, ""
if self.datatype == DataType.WORD:
return True, ""
return False, "cannot assign address to single register"
if isinstance(other, ParseResult.FloatValue):
2017-12-21 13:52:30 +00:00
range_error = check_value_in_range(self.datatype, self.register, 1, other.value)
if range_error:
return False, range_error
return True, ""
if self.datatype == DataType.BYTE:
if other.datatype != DataType.BYTE:
return False, "(unsigned) byte required"
return True, ""
if self.datatype == DataType.WORD:
if other.datatype in (DataType.BYTE, DataType.WORD) or other.datatype in STRING_DATATYPES:
return True, ""
return False, "(unsigned) byte, word or string required"
2017-12-23 00:53:48 +00:00
return False, "incompatible value for register assignment"
2017-12-21 13:52:30 +00:00
class MemMappedValue(Value):
def __init__(self, address: Optional[int], datatype: DataType, length: int, name: str=None, constant: bool=False) -> None:
super().__init__(datatype, name, constant)
self.address = address
self.length = length
2017-12-23 00:53:48 +00:00
assert address is None or type(address) is int
2017-12-21 13:52:30 +00:00
def __hash__(self):
2017-12-24 23:15:04 +00:00
return hash((self.datatype, self.address, self.length, self.name, self.constant))
2017-12-21 13:52:30 +00:00
def __eq__(self, other: Any) -> bool:
if not isinstance(other, ParseResult.MemMappedValue):
return NotImplemented
elif self is other:
return True
else:
2017-12-24 23:15:04 +00:00
return (other.datatype, other.address, other.length, other.name, other.constant) ==\
(self.datatype, self.address, self.length, self.name, self.constant)
2017-12-21 13:52:30 +00:00
def __str__(self):
addr = "" if self.address is None else "${:04x}".format(self.address)
return "<MemMappedValue {:s} type={:s} #={:d} name={} constant={}>"\
.format(addr, self.datatype, self.length, self.name, self.constant)
def assignable_from(self, other: 'ParseResult.Value') -> Tuple[bool, str]:
if self.constant:
return False, "cannot assign to a constant"
2017-12-23 00:53:48 +00:00
if isinstance(other, ParseResult.IndirectValue):
return False, "can not yet assign memory mapped value from indirect value" # @todo indirect v assign
2017-12-21 13:52:30 +00:00
if self.datatype == DataType.BYTE:
if isinstance(other, (ParseResult.IntegerValue, ParseResult.RegisterValue, ParseResult.MemMappedValue)):
if other.datatype == DataType.BYTE:
return True, ""
return False, "(unsigned) byte required"
elif isinstance(other, ParseResult.FloatValue):
range_error = check_value_in_range(self.datatype, "", 1, other.value)
if range_error:
return False, range_error
return True, ""
else:
return False, "(unsigned) byte required"
elif self.datatype in (DataType.WORD, DataType.FLOAT):
if isinstance(other, (ParseResult.IntegerValue, ParseResult.FloatValue)):
range_error = check_value_in_range(self.datatype, "", 1, other.value)
if range_error:
return False, range_error
return True, ""
elif isinstance(other, (ParseResult.RegisterValue, ParseResult.MemMappedValue)):
if other.datatype in (DataType.BYTE, DataType.WORD, DataType.FLOAT):
return True, ""
else:
return False, "byte or word or float required"
elif isinstance(other, ParseResult.StringValue):
if self.datatype == DataType.WORD:
return True, ""
return False, "string address requires 16 bits (a word)"
if self.datatype == DataType.BYTE:
return False, "(unsigned) byte required"
if self.datatype == DataType.WORD:
return False, "(unsigned) word required"
return False, "incompatible value for assignment"
class _AstNode: # @todo merge Value with this?
2017-12-27 18:01:14 +00:00
def __init__(self, lineno: int) -> None:
self.lineno = lineno
2017-12-21 13:52:30 +00:00
2017-12-21 22:05:35 +00:00
class Comment(_AstNode):
2017-12-27 18:01:14 +00:00
def __init__(self, text: str, lineno: int) -> None:
super().__init__(lineno)
2017-12-21 22:05:35 +00:00
self.text = text
class Label(_AstNode):
2017-12-21 13:52:30 +00:00
def __init__(self, name: str, lineno: int) -> None:
2017-12-27 18:01:14 +00:00
super().__init__(lineno)
2017-12-21 13:52:30 +00:00
self.name = name
class AssignmentStmt(_AstNode):
2017-12-21 13:52:30 +00:00
def __init__(self, leftvalues: List['ParseResult.Value'], right: 'ParseResult.Value', lineno: int) -> None:
2017-12-27 18:01:14 +00:00
super().__init__(lineno)
2017-12-21 13:52:30 +00:00
self.leftvalues = leftvalues
self.right = right
def __str__(self):
return "<Assign {:s} to {:s}>".format(str(self.right), ",".join(str(lv) for lv in self.leftvalues))
_immediate_string_vars = {} # type: Dict[str, Tuple[str, str]]
def desugar_immediate_string(self, parser: 'Parser') -> None:
if self.right.name or not isinstance(self.right, ParseResult.StringValue):
return
if self.right.value in self._immediate_string_vars:
blockname, stringvar_name = self._immediate_string_vars[self.right.value]
if blockname:
2017-12-23 00:53:48 +00:00
self.right.name = blockname + '.' + stringvar_name
2017-12-21 13:52:30 +00:00
else:
self.right.name = stringvar_name
else:
cur_block = parser.cur_block
stringvar_name = "il65_str_{:d}".format(id(self))
value = self.right.value
cur_block.symbols.define_variable(stringvar_name, cur_block.sourceref, DataType.STRING, value=value)
2017-12-21 13:52:30 +00:00
self.right.name = stringvar_name
self._immediate_string_vars[self.right.value] = (cur_block.name, stringvar_name)
2017-12-25 12:20:23 +00:00
def remove_identity_lvalues(self, filename: str, lineno: int) -> None:
2017-12-25 02:42:20 +00:00
for lv in self.leftvalues:
if lv == self.right:
2017-12-25 12:20:23 +00:00
print("{:s}:{:d}: removed identity assignment".format(filename, lineno))
2017-12-24 23:15:04 +00:00
remaining_leftvalues = [lv for lv in self.leftvalues if lv != self.right]
self.leftvalues = remaining_leftvalues
2017-12-25 12:20:23 +00:00
def is_identity(self) -> bool:
return all(lv == self.right for lv in self.leftvalues)
2017-12-28 03:20:59 +00:00
class AugmentedAssignmentStmt(AssignmentStmt):
SUPPORTED_OPERATORS = {"+=", "-=", "&=", "|=", "^=", ">>=", "<<="}
# full set: {"+=", "-=", "*=", "/=", "%=", "//=", "**=", "&=", "|=", "^=", ">>=", "<<="}
def __init__(self, left: 'ParseResult.Value', operator: str, right: 'ParseResult.Value', lineno: int) -> None:
assert operator in self.SUPPORTED_OPERATORS
super().__init__([left], right, lineno)
self.operator = operator
def __str__(self):
return "<AugAssign {:s} {:s} {:s}>".format(str(self.leftvalues[0]), self.operator, str(self.right))
class ReturnStmt(_AstNode):
2017-12-27 18:01:14 +00:00
def __init__(self, lineno: int, a: Optional['ParseResult.Value']=None,
2017-12-21 13:52:30 +00:00
x: Optional['ParseResult.Value']=None,
y: Optional['ParseResult.Value']=None) -> None:
2017-12-27 18:01:14 +00:00
super().__init__(lineno)
2017-12-21 13:52:30 +00:00
self.a = a
self.x = x
self.y = y
2017-12-28 03:20:59 +00:00
class InplaceIncrStmt(_AstNode):
def __init__(self, what: 'ParseResult.Value', howmuch: Union[int, float], lineno: int) -> None:
super().__init__(lineno)
assert howmuch > 0
self.what = what
self.howmuch = howmuch
class InplaceDecrStmt(_AstNode):
def __init__(self, what: 'ParseResult.Value', howmuch: Union[int, float], lineno: int) -> None:
2017-12-27 18:01:14 +00:00
super().__init__(lineno)
2017-12-28 03:20:59 +00:00
assert howmuch > 0
2017-12-21 13:52:30 +00:00
self.what = what
self.howmuch = howmuch
class CallStmt(_AstNode):
2017-12-23 00:53:48 +00:00
def __init__(self, lineno: int, target: Optional['ParseResult.Value']=None, *,
address: Optional[int]=None, arguments: List[Tuple[str, Any]]=None,
2017-12-27 18:01:14 +00:00
outputs: List[Tuple[str, 'ParseResult.Value']]=None, is_goto: bool=False,
preserve_regs: bool=True, condition: 'ParseResult.IfCondition'=None) -> None:
if not is_goto:
assert condition is None
super().__init__(lineno)
2017-12-23 00:53:48 +00:00
self.target = target
2017-12-21 13:52:30 +00:00
self.address = address
self.arguments = arguments
2017-12-26 00:30:22 +00:00
self.outputvars = outputs
self.is_goto = is_goto
2017-12-27 18:01:14 +00:00
self.condition = condition
self.preserve_regs = preserve_regs
self.desugared_call_arguments = [] # type: List[ParseResult.AssignmentStmt]
2017-12-26 00:30:22 +00:00
self.desugared_output_assignments = [] # type: List[ParseResult.AssignmentStmt]
def desugar_call_arguments_and_outputs(self, parser: 'Parser') -> None:
self.desugared_call_arguments.clear()
self.desugared_output_assignments.clear()
for name, value in self.arguments or []:
assert name is not None, "all call arguments should have a name or be matched on a named parameter"
2017-12-28 03:20:59 +00:00
assignment = parser.parse_assignment(name, value)
2017-12-29 02:52:26 +00:00
if assignment.leftvalues[0].datatype != DataType.BYTE:
if isinstance(assignment.right, ParseResult.IntegerValue) and assignment.right.constant:
# a call that doesn't expect a BYTE argument but gets one, converted from a 1-byte string most likely
if value.startswith("'") and value.endswith("'"):
parser.print_warning("possible problematic string to byte conversion (use a .text var instead?)")
2017-12-26 00:30:22 +00:00
if not assignment.is_identity():
assignment.lineno = self.lineno
self.desugared_call_arguments.append(assignment)
2017-12-30 12:34:52 +00:00
if all(not isinstance(v, ParseResult.RegisterValue) for r, v in self.outputvars or []):
# if none of the output variables are registers, we can simply generate the assignments without issues
for register, value in self.outputvars or []:
rvalue = parser.parse_expression(register)
assignment = ParseResult.AssignmentStmt([value], rvalue, self.lineno)
assignment.lineno = self.lineno
self.desugared_output_assignments.append(assignment)
else:
result_reg_mapping = [(register, value.register, value) for register, value in self.outputvars or []
if isinstance(value, ParseResult.RegisterValue)]
if any(r[0] != r[1] for r in result_reg_mapping):
# not all result parameter registers line up with the correct order of registers in the statement,
# reshuffling call results is not supported yet.
raise parser.PError("result registers and/or their ordering is not the same as in the "
"subroutine definition, this isn't supported yet")
else:
# no register alignment issues, just generate the assignments
# note: do not remove the identity assignment here or the output register handling generates buggy code
for register, value in self.outputvars or []:
rvalue = parser.parse_expression(register)
assignment = ParseResult.AssignmentStmt([value], rvalue, self.lineno)
assignment.lineno = self.lineno
self.desugared_output_assignments.append(assignment)
2017-12-21 13:52:30 +00:00
class InlineAsm(_AstNode):
2017-12-27 18:01:14 +00:00
def __init__(self, asmlines: List[str], lineno: int) -> None:
super().__init__(lineno)
2017-12-21 13:52:30 +00:00
self.asmlines = asmlines
2017-12-27 18:01:14 +00:00
class IfCondition(_AstNode):
SWAPPED_OPERATOR = {"==": "==",
"!=": "!=",
"<=": ">=",
">=": "<=",
"<": ">",
">": "<"}
2017-12-28 18:08:33 +00:00
IF_STATUSES = {"cc", "cs", "vc", "vs", "eq", "ne", "true", "not", "zero", "lt", "gt", "le", "ge"}
2017-12-27 18:01:14 +00:00
def __init__(self, ifstatus: str, leftvalue: Optional['ParseResult.Value'],
operator: str, rightvalue: Optional['ParseResult.Value'], lineno: int) -> None:
2017-12-28 18:08:33 +00:00
assert ifstatus in self.IF_STATUSES
2017-12-27 18:01:14 +00:00
assert operator in (None, "") or operator in self.SWAPPED_OPERATOR
if operator:
2017-12-28 18:08:33 +00:00
assert ifstatus in ("true", "not", "zero")
2017-12-27 18:01:14 +00:00
super().__init__(lineno)
self.ifstatus = ifstatus
self.lvalue = leftvalue
self.comparison_op = operator
self.rvalue = rightvalue
def __str__(self):
2017-12-28 18:08:33 +00:00
return "<IfCondition if_{:s} {} {:s} {}>".format(self.ifstatus, self.lvalue, self.comparison_op, self.rvalue)
def make_if_true(self) -> bool:
# makes a condition of the form if_not a < b into: if a > b (gets rid of the not)
# returns whether the change was made or not
if self.ifstatus == "not" and self.comparison_op:
self.ifstatus = "true"
self.comparison_op = self.SWAPPED_OPERATOR[self.comparison_op]
return True
return False
2017-12-27 18:01:14 +00:00
def swap(self) -> Tuple['ParseResult.Value', str, 'ParseResult.Value']:
self.lvalue, self.comparison_op, self.rvalue = self.rvalue, self.SWAPPED_OPERATOR[self.comparison_op], self.lvalue
return self.lvalue, self.comparison_op, self.rvalue
2017-12-27 22:45:22 +00:00
class BreakpointStmt(_AstNode):
def __init__(self, lineno: int) -> None:
super().__init__(lineno)
2017-12-21 13:52:30 +00:00
def add_block(self, block: 'ParseResult.Block', position: Optional[int]=None) -> None:
if position is not None:
self.blocks.insert(position, block)
else:
self.blocks.append(block)
def merge(self, parsed: 'ParseResult') -> None:
2017-12-21 22:05:35 +00:00
existing_blocknames = set(block.name for block in self.blocks)
other_blocknames = set(block.name for block in parsed.blocks)
overlap = existing_blocknames & other_blocknames
if overlap != {"<header>"}:
raise SymbolError("double block names: {}".format(overlap))
for block in parsed.blocks:
if block.name != "<header>":
self.blocks.append(block)
2017-12-21 13:52:30 +00:00
2017-12-25 18:09:10 +00:00
def find_block(self, name: str) -> Block:
for block in self.blocks:
if block.name == name:
return block
raise KeyError("block not found: " + name)
def sub_used_by(self, sub: SubroutineDef, sourceref: SourceRef) -> None:
self.subroutine_usage[(sub.blockname, sub.name)].add(str(sourceref))
2017-12-21 13:52:30 +00:00
class Parser:
2017-12-30 12:34:52 +00:00
def __init__(self, filename: str, outputdir: str, existing_imports: Set[str], parsing_import: bool = False,
sourcelines: List[Tuple[int, str]] = None, ppsymbols: SymbolTable = None, sub_usage: Dict=None) -> None:
2017-12-21 13:52:30 +00:00
self.result = ParseResult(filename)
2017-12-25 18:09:10 +00:00
if sub_usage is not None:
# re-use the (global) subroutine usage tracking
self.result.subroutine_usage = sub_usage
2017-12-21 13:52:30 +00:00
self.sourceref = SourceRef(filename, -1, 0)
if sourcelines:
self.lines = sourcelines
else:
self.lines = self.load_source(filename)
self.outputdir = outputdir
self.parsing_import = parsing_import # are we parsing a import file?
2017-12-25 12:20:23 +00:00
self._cur_lineidx = -1 # used to efficiently go to next/previous line in source
2017-12-21 13:52:30 +00:00
self.cur_block = None # type: ParseResult.Block
self.root_scope = SymbolTable("<root>", None, None)
2017-12-29 02:52:26 +00:00
self.root_scope.set_zeropage(self.result.zeropage)
self.ppsymbols = ppsymbols # symboltable from preprocess phase
self.print_block_parsing = True
2017-12-30 12:34:52 +00:00
self.existing_imports = existing_imports
2017-12-21 13:52:30 +00:00
def load_source(self, filename: str) -> List[Tuple[int, str]]:
with open(filename, "rU") as source:
sourcelines = source.readlines()
2017-12-21 22:05:35 +00:00
# store all lines that aren't empty
2017-12-23 13:36:23 +00:00
# comments are kept (end-of-line comments are stripped though)
2017-12-21 13:52:30 +00:00
lines = []
for num, line in enumerate(sourcelines, start=1):
2017-12-23 13:36:23 +00:00
line = line.rstrip()
if line.lstrip().startswith(';'):
lines.append((num, line.lstrip()))
else:
line2, sep, comment = line.rpartition(';')
if sep:
line = line2.rstrip()
if line:
lines.append((num, line))
2017-12-21 13:52:30 +00:00
return lines
def parse(self) -> Optional[ParseResult]:
# start the parsing
try:
return self.parse_file()
except ParseError as x:
2017-12-26 00:30:22 +00:00
if sys.stderr.isatty():
print("\x1b[1m", file=sys.stderr)
print("", file=sys.stderr)
2017-12-21 22:05:35 +00:00
if x.sourcetext:
2017-12-26 00:30:22 +00:00
print("\tsource text: '{:s}'".format(x.sourcetext), file=sys.stderr)
2017-12-21 13:52:30 +00:00
if x.sourceref.column:
2017-12-26 00:30:22 +00:00
print("\t" + ' '*x.sourceref.column + ' ^', file=sys.stderr)
2017-12-21 13:52:30 +00:00
if self.parsing_import:
2017-12-26 00:30:22 +00:00
print("Error (in imported file):", str(x), file=sys.stderr)
2017-12-21 13:52:30 +00:00
else:
2017-12-26 00:30:22 +00:00
print("Error:", str(x), file=sys.stderr)
if sys.stderr.isatty():
print("\x1b[0m", file=sys.stderr)
2017-12-21 13:52:30 +00:00
raise # XXX temporary solution to get stack trace info in the event of parse errors
except Exception as x:
2017-12-26 00:30:22 +00:00
if sys.stderr.isatty():
print("\x1b[1m", file=sys.stderr)
print("\nERROR: internal parser error: ", x, file=sys.stderr)
2017-12-28 22:03:59 +00:00
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)
2017-12-26 00:30:22 +00:00
if sys.stderr.isatty():
print("\x1b[0m", file=sys.stderr)
2017-12-21 13:52:30 +00:00
raise # XXX temporary solution to get stack trace info in the event of parse errors
def parse_file(self) -> ParseResult:
print("\nparsing", self.sourceref.file)
2017-12-21 13:52:30 +00:00
self._parse_1()
2017-12-30 12:34:52 +00:00
self._parse_import_file("il65lib") # compiler support library is always imported.
2017-12-21 13:52:30 +00:00
self._parse_2()
return self.result
2017-12-28 18:08:33 +00:00
def print_warning(self, text: str, sourceref: SourceRef=None) -> None:
self.print_bold("warning: {}: {:s}".format(sourceref or self.sourceref, text))
def print_bold(self, text: str) -> None:
2017-12-26 00:30:22 +00:00
if sys.stdout.isatty():
print("\x1b[1m" + text + "\x1b[0m")
else:
print(text)
2017-12-21 22:05:35 +00:00
def _parse_comments(self) -> None:
while True:
line = self.next_line().lstrip()
if line.startswith(';'):
2017-12-27 18:01:14 +00:00
self.cur_block.statements.append(ParseResult.Comment(line, self.sourceref.line))
2017-12-21 22:05:35 +00:00
continue
self.prev_line()
break
2017-12-21 13:52:30 +00:00
def _parse_1(self) -> None:
2017-12-21 22:05:35 +00:00
self.cur_block = ParseResult.Block("<header>", self.sourceref, self.root_scope)
self.result.add_block(self.cur_block)
2017-12-21 13:52:30 +00:00
self.parse_header()
2017-12-29 02:52:26 +00:00
if not self.parsing_import:
self.result.zeropage.configure(self.result.clobberzp)
2017-12-21 13:52:30 +00:00
while True:
2017-12-21 22:05:35 +00:00
self._parse_comments()
next_line = self.peek_next_line().lstrip()
if next_line.startswith("~"):
2017-12-21 13:52:30 +00:00
block = self.parse_block()
if block:
self.result.add_block(block)
2017-12-30 12:34:52 +00:00
elif next_line.startswith(("import ", "import\t")):
2017-12-21 13:52:30 +00:00
self.parse_import()
else:
break
line = self.next_line()
if line:
raise self.PError("invalid statement or characters, block expected")
if not self.parsing_import:
# check if we have a proper main block to contain the program's entry point
2017-12-25 12:20:23 +00:00
main_found = False
2017-12-21 13:52:30 +00:00
for block in self.result.blocks:
if block.name == "main":
2017-12-25 12:20:23 +00:00
main_found = True
2017-12-21 13:52:30 +00:00
if "start" not in block.label_names:
self.sourceref.line = block.sourceref.line
self.sourceref.column = 0
2017-12-28 18:08:33 +00:00
raise self.PError("block 'main' should contain the program entry point 'start'")
2017-12-25 12:20:23 +00:00
self._check_return_statement(block, "'main' block")
for sub in block.symbols.iter_subroutines(True):
2017-12-28 18:08:33 +00:00
self._check_return_statement(sub.sub_block, "subroutine '{:s}'".format(sub.name))
2017-12-25 12:20:23 +00:00
if not main_found:
2017-12-28 18:08:33 +00:00
raise self.PError("a block 'main' should be defined and contain the program's entry point label 'start'")
2017-12-21 13:52:30 +00:00
2017-12-25 12:20:23 +00:00
def _check_return_statement(self, block: ParseResult.Block, message: str) -> None:
# find last statement that isn't a comment
for stmt in reversed(block.statements):
if isinstance(stmt, ParseResult.Comment):
continue
if isinstance(stmt, ParseResult.ReturnStmt) or isinstance(stmt, ParseResult.CallStmt) and stmt.is_goto:
return
if isinstance(stmt, ParseResult.InlineAsm):
# check that the last asm line is a jmp or a rts
for asmline in reversed(stmt.asmlines):
if asmline.lstrip().startswith(';'):
continue
if " rts" in asmline or "\trts" in asmline or " jmp" in asmline or "\tjmp" in asmline:
return
if asmline.strip():
if asmline.split()[0].isidentifier():
continue
break
break
2017-12-28 18:08:33 +00:00
self.print_warning("{:s} doesn't end with a return statement".format(message), block.sourceref)
2017-12-25 12:20:23 +00:00
2017-12-21 13:52:30 +00:00
def _parse_2(self) -> None:
2017-12-24 23:15:04 +00:00
# parsing pass 2 (not done during preprocessing!)
2017-12-21 13:52:30 +00:00
self.cur_block = None
self.sourceref.line = -1
self.sourceref.column = 0
2017-12-25 12:20:23 +00:00
2017-12-28 22:03:59 +00:00
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)
2017-12-21 13:52:30 +00:00
for block in self.result.blocks:
self.cur_block = block
2017-12-28 22:03:59 +00:00
self.sourceref = block.sourceref.copy()
self.sourceref.column = 0
for block, sub, stmt in block.all_statements():
2017-12-21 13:52:30 +00:00
if isinstance(stmt, ParseResult.CallStmt):
self.sourceref.line = stmt.lineno
2017-12-26 00:30:22 +00:00
stmt.desugar_call_arguments_and_outputs(self)
2017-12-28 22:03:59 +00:00
desugar_immediate_strings(stmt)
2017-12-21 13:52:30 +00:00
def next_line(self) -> str:
2017-12-25 12:20:23 +00:00
self._cur_lineidx += 1
2017-12-21 13:52:30 +00:00
try:
2017-12-25 12:20:23 +00:00
self.sourceref.line, line = self.lines[self._cur_lineidx]
2017-12-21 13:52:30 +00:00
self.sourceref.column = 0
return line
except IndexError:
return ""
def prev_line(self) -> str:
2017-12-25 12:20:23 +00:00
self._cur_lineidx -= 1
self.sourceref.line, line = self.lines[self._cur_lineidx]
2017-12-21 13:52:30 +00:00
return line
def peek_next_line(self) -> str:
2017-12-25 12:20:23 +00:00
if (self._cur_lineidx + 1) < len(self.lines):
return self.lines[self._cur_lineidx + 1][1]
2017-12-21 13:52:30 +00:00
return ""
def PError(self, message: str, lineno: int=0, column: int=0) -> ParseError:
2017-12-21 13:52:30 +00:00
sourceline = ""
2017-12-25 12:20:23 +00:00
lineno = lineno or self.sourceref.line
column = column or self.sourceref.column
for num, text in self.lines:
if num == lineno:
sourceline = text.strip()
break
return ParseError(message, sourceline, SourceRef(self.sourceref.file, lineno, column))
def get_datatype(self, typestr: str) -> Tuple[DataType, int, Optional[Tuple[int, int]]]:
if typestr == ".byte":
return DataType.BYTE, 1, None
elif typestr == ".word":
return DataType.WORD, 1, None
elif typestr == ".float":
return DataType.FLOAT, 1, None
elif typestr.endswith("text"):
if typestr == ".text":
return DataType.STRING, 0, None
elif typestr == ".ptext":
return DataType.STRING_P, 0, None
elif typestr == ".stext":
return DataType.STRING_S, 0, None
elif typestr == ".pstext":
return DataType.STRING_PS, 0, None
elif typestr.startswith(".array(") and typestr.endswith(")"):
return DataType.BYTEARRAY, self._size_from_arraydecl(typestr), None
elif typestr.startswith(".wordarray(") and typestr.endswith(")"):
return DataType.WORDARRAY, self._size_from_arraydecl(typestr), None
elif typestr.startswith(".matrix(") and typestr.endswith(")"):
dimensions = self._size_from_matrixdecl(typestr)
return DataType.MATRIX, dimensions[0] * dimensions[1], dimensions
raise self.PError("invalid data type: " + typestr)
2017-12-21 13:52:30 +00:00
def parse_header(self) -> None:
self.result.with_sys = False
self.result.format = ProgramFormat.RAW
output_specified = False
2017-12-28 22:03:59 +00:00
zp_specified = False
2017-12-21 13:52:30 +00:00
while True:
2017-12-21 22:05:35 +00:00
self._parse_comments()
2017-12-21 13:52:30 +00:00
line = self.next_line()
2017-12-28 22:03:59 +00:00
if line.startswith(("output ", "output\t")):
2017-12-21 13:52:30 +00:00
if output_specified:
2017-12-28 22:03:59 +00:00
raise self.PError("can only specify output options once")
2017-12-21 13:52:30 +00:00
output_specified = True
2017-12-28 22:03:59 +00:00
_, _, optionstr = line.partition(" ")
options = set(optionstr.replace(' ', '').split(','))
2017-12-21 13:52:30 +00:00
self.result.with_sys = False
self.result.format = ProgramFormat.RAW
2017-12-28 22:03:59 +00:00
if "raw" in options:
options.remove("raw")
if "prg" in options:
options.remove("prg")
2017-12-21 13:52:30 +00:00
self.result.format = ProgramFormat.PRG
2017-12-28 22:03:59 +00:00
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))
2017-12-21 13:52:30 +00:00
elif line.startswith("address"):
if self.result.start_address:
raise self.PError("multiple occurrences of 'address'")
_, _, arg = line.partition(" ")
try:
self.result.start_address = parse_expr_as_int(arg, None, None, self.sourceref)
2017-12-21 13:52:30 +00:00
except ParseError:
raise self.PError("invalid address")
if self.result.format == ProgramFormat.PRG and self.result.with_sys and self.result.start_address != 0x0801:
raise self.PError("cannot use non-default 'address' when output format includes basic SYS program")
else:
# header parsing finished!
self.prev_line()
if not self.result.start_address:
# set the proper default start address
if self.result.format == ProgramFormat.PRG:
self.result.start_address = 0x0801 # normal C-64 basic program start address
elif self.result.format == ProgramFormat.RAW:
self.result.start_address = 0xc000 # default start for raw assembly
if self.result.format == ProgramFormat.PRG and self.result.with_sys and self.result.start_address != 0x0801:
raise self.PError("cannot use non-default 'address' when output format includes basic SYS program")
return
def parse_import(self) -> None:
line = self.next_line()
line = line.lstrip()
2017-12-30 12:34:52 +00:00
if not line.startswith(("import ", "import\t")):
2017-12-21 13:52:30 +00:00
raise self.PError("expected import")
try:
_, arg = line.split(maxsplit=1)
except ValueError:
raise self.PError("invalid import statement")
if not arg.startswith('"') or not arg.endswith('"'):
raise self.PError("filename must be between quotes")
filename = arg[1:-1]
if not filename:
raise self.PError("invalid filename")
2017-12-30 12:34:52 +00:00
self._parse_import_file(filename)
def _parse_import_file(self, filename: str) -> None:
2017-12-21 13:52:30 +00:00
filename_at_source_location = os.path.join(os.path.split(self.sourceref.file)[0], filename)
filename_at_libs_location = os.path.join(os.getcwd(), "lib", filename)
candidates = [filename,
filename_at_source_location,
filename_at_libs_location,
filename+".ill",
filename_at_source_location+".ill",
filename_at_libs_location+".ill"]
for filename in candidates:
if os.path.isfile(filename):
2017-12-30 12:34:52 +00:00
if not self.check_import_okay(filename):
return
self.print_import_progress("importing", filename)
2017-12-21 13:52:30 +00:00
parser = self.create_import_parser(filename, self.outputdir)
result = parser.parse()
2017-12-30 12:34:52 +00:00
self.print_import_progress("\ncontinuing", self.sourceref.file)
2017-12-21 13:52:30 +00:00
if result:
# merge the symbol table of the imported file into our own
2017-12-21 22:05:35 +00:00
try:
self.root_scope.merge_roots(parser.root_scope)
self.result.merge(result)
except SymbolError as x:
raise self.PError(str(x))
2017-12-21 13:52:30 +00:00
return
else:
raise self.PError("Error while parsing imported file")
raise self.PError("imported file not found")
2017-12-30 12:34:52 +00:00
def print_import_progress(self, message: str, *args: str) -> None:
print(message, *args)
2017-12-21 13:52:30 +00:00
def create_import_parser(self, filename: str, outputdir: str) -> 'Parser':
2017-12-30 12:34:52 +00:00
return Parser(filename, outputdir, self.existing_imports, True, ppsymbols=self.ppsymbols, sub_usage=self.result.subroutine_usage)
2017-12-21 13:52:30 +00:00
2017-12-28 18:08:33 +00:00
def parse_block(self) -> Optional[ParseResult.Block]:
2017-12-21 13:52:30 +00:00
# first line contains block header "~ [name] [addr]" followed by a '{'
2017-12-21 22:05:35 +00:00
self._parse_comments()
2017-12-21 13:52:30 +00:00
line = self.next_line()
line = line.lstrip()
if not line.startswith("~"):
raise self.PError("expected '~' (block)")
block_args = line[1:].split()
arg = ""
2017-12-21 22:05:35 +00:00
self.cur_block = ParseResult.Block("", self.sourceref.copy(), self.root_scope)
2017-12-21 13:52:30 +00:00
is_zp_block = False
while block_args:
arg = block_args.pop(0)
if arg.isidentifier():
if arg.lower() == "zeropage" or arg in ("zp", "zP", "Zp"):
raise self.PError("zero page block should be named 'ZP'")
is_zp_block = arg == "ZP"
if arg in set(b.name for b in self.result.blocks):
orig = [b for b in self.result.blocks if b.name == arg][0]
if not is_zp_block:
raise self.PError("duplicate block name '{:s}', original definition at {}".format(arg, orig.sourceref))
self.cur_block = orig # zero page block occurrences are merged
else:
2017-12-21 22:05:35 +00:00
self.cur_block = ParseResult.Block(arg, self.sourceref.copy(), self.root_scope)
2017-12-21 13:52:30 +00:00
try:
2017-12-21 22:05:35 +00:00
self.root_scope.define_scope(self.cur_block.symbols, self.cur_block.sourceref)
2017-12-21 13:52:30 +00:00
except SymbolError as x:
raise self.PError(str(x))
elif arg == "{":
break
elif arg.endswith("{"):
# when there is no whitespace before the {
block_args.insert(0, "{")
block_args.insert(0, arg[:-1])
continue
else:
try:
block_address = parse_expr_as_int(arg, self.cur_block.symbols, self.ppsymbols, self.sourceref)
2017-12-21 13:52:30 +00:00
except ParseError:
raise self.PError("Invalid block address")
if block_address == 0 or (block_address < 0x0200 and not is_zp_block):
raise self.PError("block address must be >= $0200 (or omitted)")
if is_zp_block:
if block_address not in (0, 0x04):
raise self.PError("zero page block address must be $04 (or omittted)")
block_address = 0x04
self.cur_block.address = block_address
if arg != "{":
line = self.peek_next_line()
if line != "{":
raise self.PError("expected '{' after block")
else:
self.next_line()
if self.print_block_parsing:
if self.cur_block.address:
print(" parsing block '{:s}' at ${:04x}".format(self.cur_block.name, self.cur_block.address))
else:
print(" parsing block '{:s}'".format(self.cur_block.name))
2017-12-28 18:08:33 +00:00
if self.cur_block.ignore:
# just skip the lines until we hit a '}' that closes the block
nesting_level = 1
while True:
line = self.next_line().strip()
if line.endswith("{"):
nesting_level += 1
elif line == "}":
nesting_level -= 1
if nesting_level == 0:
self.print_warning("ignoring block without name and address", self.cur_block.sourceref)
return None
else:
raise self.PError("invalid statement in block")
2017-12-21 13:52:30 +00:00
while True:
2017-12-21 22:05:35 +00:00
self._parse_comments()
2017-12-21 13:52:30 +00:00
line = self.next_line()
unstripped_line = line
line = line.strip()
if line == "}":
if is_zp_block and any(b.name == "ZP" for b in self.result.blocks):
return None # we already have the ZP block
2017-12-28 18:08:33 +00:00
if self.cur_block.ignore:
self.print_warning("ignoring block without name and address", self.cur_block.sourceref)
2017-12-21 13:52:30 +00:00
return None
return self.cur_block
2017-12-23 13:36:23 +00:00
if line.startswith(("var ", "var\t")):
2017-12-21 13:52:30 +00:00
self.parse_var_def(line)
2017-12-23 13:36:23 +00:00
elif line.startswith(("const ", "const\t")):
2017-12-21 13:52:30 +00:00
self.parse_const_def(line)
2017-12-23 13:36:23 +00:00
elif line.startswith(("memory ", "memory\t")):
2017-12-21 13:52:30 +00:00
self.parse_memory_def(line, is_zp_block)
2017-12-24 23:15:04 +00:00
elif line.startswith(("sub ", "sub\t")):
2017-12-21 13:52:30 +00:00
if is_zp_block:
raise self.PError("ZP block cannot contain subroutines")
2017-12-25 18:09:10 +00:00
self.parse_subroutine_def(line)
2017-12-23 13:36:23 +00:00
elif line.startswith(("asminclude ", "asminclude\t", "asmbinary ", "asmbinary\t")):
2017-12-21 13:52:30 +00:00
if is_zp_block:
raise self.PError("ZP block cannot contain assembler directives")
self.cur_block.statements.append(self.parse_asminclude(line))
2017-12-23 13:36:23 +00:00
elif line.startswith(("asm ", "asm\t")):
2017-12-21 13:52:30 +00:00
if is_zp_block:
raise self.PError("ZP block cannot contain code statements")
self.prev_line()
self.cur_block.statements.append(self.parse_asm())
2017-12-27 22:45:22 +00:00
elif line == "breakpoint":
self.cur_block.statements.append(ParseResult.BreakpointStmt(self.sourceref.line))
2017-12-28 18:08:33 +00:00
self.print_warning("breakpoint defined")
2017-12-21 13:52:30 +00:00
elif unstripped_line.startswith((" ", "\t")):
if is_zp_block:
raise self.PError("ZP block cannot contain code statements")
self.cur_block.statements.append(self.parse_statement(line))
elif line:
if is_zp_block:
raise self.PError("ZP block cannot contain code labels")
self.parse_label(line)
else:
2017-12-24 23:15:04 +00:00
raise self.PError("invalid statement in block")
2017-12-21 13:52:30 +00:00
def parse_label(self, line: str) -> None:
label_line = line.split(maxsplit=1)
if str.isidentifier(label_line[0]):
labelname = label_line[0]
if labelname in self.cur_block.label_names:
raise self.PError("label already defined")
if labelname in self.cur_block.symbols:
raise self.PError("symbol already defined")
self.cur_block.symbols.define_label(labelname, self.sourceref)
self.cur_block.statements.append(ParseResult.Label(labelname, self.sourceref.line))
if len(label_line) > 1:
rest = label_line[1]
self.cur_block.statements.append(self.parse_statement(rest))
else:
raise self.PError("invalid label name")
def parse_memory_def(self, line: str, is_zeropage: bool=False) -> None:
varname, datatype, length, dimensions, valuetext = self.parse_def_common(line, "memory")
memaddress = parse_expr_as_int(valuetext, self.cur_block.symbols, self.ppsymbols, self.sourceref)
2017-12-21 13:52:30 +00:00
if is_zeropage and memaddress > 0xff:
2017-12-29 02:52:26 +00:00
raise self.PError("address must be in zeropage $00-$ff")
2017-12-21 13:52:30 +00:00
try:
self.cur_block.symbols.define_variable(varname, self.sourceref, datatype,
length=length, address=memaddress, matrixsize=dimensions)
except SymbolError as x:
raise self.PError(str(x)) from x
def parse_const_def(self, line: str) -> None:
varname, datatype, length, dimensions, valuetext = self.parse_def_common(line, "const")
if dimensions:
raise self.PError("cannot declare a constant matrix")
value = parse_expr_as_primitive(valuetext, self.cur_block.symbols, self.ppsymbols, self.sourceref)
_, value = self.coerce_value(self.sourceref, datatype, value)
2017-12-21 13:52:30 +00:00
try:
self.cur_block.symbols.define_constant(varname, self.sourceref, datatype, length=length, value=value)
except (ValueError, SymbolError) as x:
raise self.PError(str(x)) from x
2017-12-25 18:09:10 +00:00
def parse_subroutine_def(self, line: str) -> None:
2017-12-27 11:39:19 +00:00
match = re.fullmatch(r"sub\s+(?P<name>\w+)\s+"
r"\((?P<parameters>[\w\s:,]*)\)"
r"\s*->\s*"
r"\((?P<results>[\w\s?,]*)\)\s*"
r"(?P<decltype>\s+=\s+(?P<address>\S*)|{)\s*", line)
2017-12-21 13:52:30 +00:00
if not match:
2017-12-24 23:15:04 +00:00
raise self.PError("invalid sub declaration")
2017-12-27 11:39:19 +00:00
groups = match.groupdict()
code_decl = groups["decltype"] == "{"
name, parameterlist, resultlist, address_str = groups["name"], groups["parameters"], groups["results"], groups["address"]
parameters = [(m.group("name"), m.group("target"))
for m in re.finditer(r"(?:(?:(?P<name>[\w]+)\s*:\s*)?(?P<target>[\w]+))(?:,|$)", parameterlist)]
2017-12-21 13:52:30 +00:00
for _, regs in parameters:
if regs not in REGISTER_SYMBOLS:
raise self.PError("invalid register(s) in parameter or return values")
all_paramnames = [p[0] for p in parameters if p[0]]
if len(all_paramnames) != len(set(all_paramnames)):
raise self.PError("duplicates in parameter names")
2017-12-27 11:39:19 +00:00
results = [m.group("name") for m in re.finditer(r"\s*(?P<name>(?:\w+)\??)\s*(?:,|$)", resultlist)]
2017-12-30 20:36:42 +00:00
if not results:
if resultlist == "?":
# a single '?' in the result spec means: all 3 registers clobbered
results = ['A?', 'X?', 'Y?']
elif resultlist:
raise self.PError("invalid return values spec")
2017-12-23 23:58:55 +00:00
subroutine_block = None
if code_decl:
address = None
# parse the subroutine code lines (until the closing '}')
2017-12-25 18:09:10 +00:00
subroutine_block = ParseResult.Block(self.cur_block.name + "." + name, self.sourceref, self.cur_block.symbols)
2017-12-23 23:58:55 +00:00
current_block = self.cur_block
self.cur_block = subroutine_block
while True:
self._parse_comments()
line = self.next_line()
unstripped_line = line
line = line.strip()
if line == "}":
# subroutine end
break
2017-12-24 23:15:04 +00:00
if line.startswith(("sub ", "sub\t")):
2017-12-23 23:58:55 +00:00
raise self.PError("cannot nest subroutines")
elif line.startswith(("asm ", "asm\t")):
self.prev_line()
subroutine_block.statements.append(self.parse_asm())
elif unstripped_line.startswith((" ", "\t")):
subroutine_block.statements.append(self.parse_statement(line))
elif line:
self.parse_label(line)
else:
2017-12-24 23:15:04 +00:00
raise self.PError("invalid statement in subroutine")
2017-12-23 23:58:55 +00:00
self.cur_block = current_block
self.cur_block.sourceref = subroutine_block.sourceref
else:
try:
address = parse_expr_as_int(address_str, self.cur_block.symbols, self.ppsymbols, self.sourceref)
except ParseError:
raise self.PError("invalid subroutine address")
2017-12-21 13:52:30 +00:00
try:
2017-12-23 23:58:55 +00:00
self.cur_block.symbols.define_sub(name, self.sourceref, parameters, results, address, subroutine_block)
2017-12-21 13:52:30 +00:00
except SymbolError as x:
raise self.PError(str(x)) from x
def parse_var_def(self, line: str) -> None:
varname, datatype, length, dimensions, valuetext = self.parse_def_common(line, "var", False)
value = parse_expr_as_primitive(valuetext, self.cur_block.symbols, self.ppsymbols, self.sourceref)
_, value = self.coerce_value(self.sourceref, datatype, value)
2017-12-21 13:52:30 +00:00
try:
self.cur_block.symbols.define_variable(varname, self.sourceref, datatype,
length=length, value=value, matrixsize=dimensions)
except (ValueError, SymbolError) as x:
raise self.PError(str(x)) from x
def parse_def_common(self, line: str, what: str, value_required: bool=True) -> \
Tuple[str, DataType, int, Optional[Tuple[int, int]], str]:
try:
vartext, valuetext = line.split("=", maxsplit=1)
except ValueError:
if '=' not in line:
if value_required:
raise self.PError("missing value assignment")
vartext, valuetext = line, "0" # unspecified value is '0'
else:
raise self.PError("invalid {:s} decl, '=' missing?".format(what))
args = self.psplit(vartext)
if args[0] != what or len(args) < 2:
raise self.PError("invalid {:s} decl".format(what))
if len(args) > 3 or valuetext.startswith('='):
raise self.PError("invalid {:s} decl, '=' missing?".format(what))
if len(args) == 2:
args.insert(1, ".byte") # unspecified data type is ".byte"
if not args[1].startswith("."):
raise self.PError("invalid {:s} decl, type is missing".format(what))
varname = args[2]
if not varname.isidentifier():
raise self.PError("invalid {:s} name".format(what))
if varname in RESERVED_NAMES:
raise self.PError("can't use a reserved name as {:s} name".format(what))
datatype, length, matrix_dimensions = self.get_datatype(args[1])
return varname, datatype, length, matrix_dimensions, valuetext
def parse_statement(self, line: str) -> ParseResult._AstNode:
2017-12-27 18:01:14 +00:00
match = re.fullmatch(r"(?P<if>if(_[a-z]+)?)\s+(?P<cond>.+)?goto\s+(?P<subname>[\S]+?)\s*(\((?P<arguments>.*)\))?\s*", line)
if match:
# conditional goto
groups = match.groupdict()
subname = groups["subname"]
if '!' in subname:
raise self.PError("goto is always without register preservation, should not have exclamation mark")
if groups["if"] == "if" and not groups["cond"]:
raise self.PError("need explicit if status when a condition is not present")
condition = self.parse_if_condition(groups["if"], groups["cond"])
return self.parse_call_or_goto(subname, groups["arguments"], None, False, True, condition=condition)
2017-12-27 11:39:19 +00:00
match = re.fullmatch(r"goto\s+(?P<subname>[\S]+?)\s*(\((?P<arguments>.*)\))?\s*", line)
2017-12-26 00:30:22 +00:00
if match:
2017-12-27 11:39:19 +00:00
# goto
groups = match.groupdict()
subname = groups["subname"]
if '!' in subname:
raise self.PError("goto is always without register preservation, should not have exclamation mark")
2017-12-27 18:01:14 +00:00
return self.parse_call_or_goto(subname, groups["arguments"], None, False, True)
2017-12-27 11:39:19 +00:00
match = re.fullmatch(r"(?P<outputs>[^\(]*\s*=)?\s*(?P<subname>[\S]+?)\s*(?P<fcall>[!]?)\s*(\((?P<arguments>.*)\))?\s*", line)
if match:
# subroutine call (not a goto) with possible output param assignment
groups = match.groupdict()
preserve = not bool(groups["fcall"])
subname = groups["subname"]
arguments = groups["arguments"]
outputs = groups["outputs"] or ""
2017-12-26 00:30:22 +00:00
if outputs.strip() == "=":
raise self.PError("missing assignment target variables")
outputs = outputs.rstrip("=")
2017-12-27 11:39:19 +00:00
if arguments or match.group(4): # group 4 = (possibly empty) parenthesis
2017-12-26 00:30:22 +00:00
return self.parse_call_or_goto(subname, arguments, outputs, preserve, False)
# apparently it is not a call (no arguments), fall through
2017-12-23 13:36:23 +00:00
if line == "return" or line.startswith(("return ", "return\t")):
2017-12-21 13:52:30 +00:00
return self.parse_return(line)
elif line.endswith(("++", "--")):
incr = line.endswith("++")
what = self.parse_expression(line[:-2].rstrip())
if isinstance(what, ParseResult.IntegerValue):
raise self.PError("cannot in/decrement a constant value")
2017-12-28 03:20:59 +00:00
if incr:
return ParseResult.InplaceIncrStmt(what, 1, self.sourceref.line)
return ParseResult.InplaceDecrStmt(what, 1, self.sourceref.line)
2017-12-21 13:52:30 +00:00
else:
2017-12-28 03:20:59 +00:00
# perhaps it is an augmented assignment statement
match = re.fullmatch(r"(?P<left>\S+)\s*(?P<assignment>\+=|-=|\*=|/=|%=|//=|\*\*=|&=|\|=|\^=|>>=|<<=)\s*(?P<right>\S.*)", line)
if match:
return self.parse_augmented_assignment(match.group("left"), match.group("assignment"), match.group("right"))
# a normal assignment perhaps?
splits = [s.strip() for s in line.split('=')]
2017-12-28 18:08:33 +00:00
if len(splits) > 1 and all(splits):
2017-12-28 03:20:59 +00:00
return self.parse_assignment(*splits)
2017-12-21 13:52:30 +00:00
raise self.PError("invalid statement")
2017-12-26 00:30:22 +00:00
def parse_call_or_goto(self, targetstr: str, argumentstr: str, outputstr: str,
2017-12-27 18:01:14 +00:00
preserve_regs=True, is_goto=False, condition: ParseResult.IfCondition=None) -> ParseResult.CallStmt:
if not is_goto:
assert condition is None
2017-12-23 13:36:23 +00:00
argumentstr = argumentstr.strip() if argumentstr else ""
2017-12-26 00:30:22 +00:00
outputstr = outputstr.strip() if outputstr else ""
2017-12-23 00:53:48 +00:00
arguments = None
2017-12-26 00:30:22 +00:00
outputvars = None
2017-12-23 13:36:23 +00:00
if argumentstr:
2017-12-25 21:22:19 +00:00
arguments = parse_arguments(argumentstr, self.sourceref)
2017-12-23 00:53:48 +00:00
target = None # type: ParseResult.Value
if targetstr[0] == '[' and targetstr[-1] == ']':
2017-12-21 13:52:30 +00:00
# indirect call to address in register pair or memory location
2017-12-24 23:15:04 +00:00
targetstr, target = self.parse_indirect_value(targetstr, True)
2017-12-23 00:53:48 +00:00
if target.datatype != DataType.WORD:
raise self.PError("invalid call target (should contain 16-bit)")
2017-12-21 13:52:30 +00:00
else:
2017-12-23 00:53:48 +00:00
target = self.parse_expression(targetstr)
2017-12-30 20:36:42 +00:00
if not isinstance(target, (ParseResult.IntegerValue, ParseResult.MemMappedValue, ParseResult.IndirectValue)):
2017-12-23 00:53:48 +00:00
raise self.PError("cannot call that type of symbol")
if isinstance(target, ParseResult.IndirectValue) \
and not isinstance(target.value, (ParseResult.IntegerValue, ParseResult.RegisterValue, ParseResult.MemMappedValue)):
raise self.PError("cannot call that type of indirect symbol")
address = target.address if isinstance(target, ParseResult.MemMappedValue) else None
try:
2017-12-25 21:22:19 +00:00
_, symbol = self.lookup_with_ppsymbols(targetstr)
except ParseError:
symbol = None # it's probably a number or a register then
2017-12-23 00:53:48 +00:00
if isinstance(symbol, SubroutineDef):
2017-12-27 18:01:14 +00:00
if condition and symbol.parameters:
raise self.PError("cannot use a subroutine that requires parameters as a target for conditional goto")
2017-12-23 00:53:48 +00:00
# verify subroutine arguments
2017-12-23 13:36:23 +00:00
if len(arguments or []) != len(symbol.parameters):
2017-12-23 00:53:48 +00:00
raise self.PError("invalid number of arguments ({:d}, expected {:d})"
2017-12-23 13:36:23 +00:00
.format(len(arguments or []), len(symbol.parameters)))
2017-12-23 00:53:48 +00:00
args_with_pnames = []
for i, (argname, value) in enumerate(arguments or []):
pname, preg = symbol.parameters[i]
2017-12-25 21:22:19 +00:00
required_name = pname or preg
if argname and argname != required_name:
raise self.PError("parameter mismatch ('{:s}', expected '{:s}')".format(argname, required_name))
argname = preg
2017-12-23 00:53:48 +00:00
args_with_pnames.append((argname, value))
arguments = args_with_pnames
2017-12-26 00:30:22 +00:00
# verify output parameters
if symbol.return_registers:
if outputstr:
outputs = [r.strip() for r in outputstr.split(",")]
if len(outputs) != len(symbol.return_registers):
raise self.PError("invalid number of output parameters consumed ({:d}, expected {:d})"
.format(len(outputs), len(symbol.return_registers)))
outputvars = list(zip(symbol.return_registers, (self.parse_expression(out) for out in outputs)))
else:
2017-12-28 18:08:33 +00:00
self.print_warning("return values discarded")
2017-12-26 00:30:22 +00:00
else:
if outputstr:
raise self.PError("this subroutine doesn't have output parameters")
self.result.sub_used_by(symbol, self.sourceref) # sub usage tracking
2017-12-23 13:36:23 +00:00
else:
2017-12-26 00:30:22 +00:00
if outputstr:
raise self.PError("call cannot use output parameter assignment here, a subroutine is required for that")
2017-12-23 13:36:23 +00:00
if arguments:
2017-12-26 00:30:22 +00:00
raise self.PError("call cannot take any arguments here, a subroutine is required for that")
# verify that all arguments have gotten a name
if any(not a[0] for a in arguments or []):
raise self.PError("all call arguments should have a name or be matched on a named parameter")
2017-12-23 00:53:48 +00:00
if isinstance(target, (type(None), ParseResult.Value)):
2017-12-29 02:52:26 +00:00
# special case for the C-64 lib's print function, to be able to use it with a single character argument
2017-12-30 12:34:52 +00:00
if target.name == "c64util.print_string" and len(arguments) == 1 and isinstance(arguments[0], str):
2017-12-29 02:52:26 +00:00
if arguments[0][1].startswith("'") and arguments[0][1].endswith("'"):
target = self.parse_expression("c64.CHROUT")
address = target.address
outputvars = None
_, newsymbol = self.lookup_with_ppsymbols("c64.CHROUT")
assert len(newsymbol.parameters) == 1
arguments = [(newsymbol.parameters[0][1], arguments[0][1])]
2017-12-23 13:36:23 +00:00
if is_goto:
2017-12-26 00:30:22 +00:00
return ParseResult.CallStmt(self.sourceref.line, target, address=address,
2017-12-27 18:01:14 +00:00
arguments=arguments, outputs=outputvars, is_goto=True, condition=condition)
2017-12-21 13:52:30 +00:00
else:
2017-12-26 00:30:22 +00:00
return ParseResult.CallStmt(self.sourceref.line, target, address=address,
arguments=arguments, outputs=outputvars, preserve_regs=preserve_regs)
else:
2017-12-23 00:53:48 +00:00
raise TypeError("target should be a Value", target)
2017-12-21 13:52:30 +00:00
def parse_integer(self, text: str) -> int:
text = text.strip()
if text.startswith('$'):
return int(text[1:], 16)
if text.startswith('%'):
return int(text[1:], 2)
return int(text)
2017-12-28 03:20:59 +00:00
def parse_assignment(self, *parts) -> ParseResult.AssignmentStmt:
# parses the assignment of one rvalue to one or more lvalues
l_values = [self.parse_expression(p) for p in parts[:-1]]
r_value = self.parse_expression(parts[-1])
if any(lv.constant for lv in l_values):
2017-12-24 23:15:04 +00:00
raise self.PError("can't have a constant as assignment target, perhaps you wanted indirection [...] instead?")
2017-12-21 13:52:30 +00:00
for lv in l_values:
assignable, reason = lv.assignable_from(r_value)
if not assignable:
raise self.PError("cannot assign {0} to {1}; {2}".format(r_value, lv, reason))
if lv.datatype in (DataType.BYTE, DataType.WORD, DataType.MATRIX):
2017-12-28 03:20:59 +00:00
# truncate the rvalue if needed
2017-12-21 13:52:30 +00:00
if isinstance(r_value, ParseResult.FloatValue):
truncated, value = self.coerce_value(self.sourceref, lv.datatype, r_value.value)
2017-12-21 13:52:30 +00:00
if truncated:
r_value = ParseResult.IntegerValue(int(value), datatype=lv.datatype, name=r_value.name)
return ParseResult.AssignmentStmt(l_values, r_value, self.sourceref.line)
2017-12-28 03:20:59 +00:00
def parse_augmented_assignment(self, leftstr: str, operator: str, rightstr: str) \
-> Union[ParseResult.AssignmentStmt, ParseResult.InplaceDecrStmt, ParseResult.InplaceIncrStmt]:
# parses an augmented assignment (for instance: value += 3)
if operator not in ParseResult.AugmentedAssignmentStmt.SUPPORTED_OPERATORS:
raise self.PError("augmented assignment operator '{:s}' not supported".format(operator))
l_value = self.parse_expression(leftstr)
r_value = self.parse_expression(rightstr)
if l_value.constant:
raise self.PError("can't have a constant as assignment target, perhaps you wanted indirection [...] instead?")
if l_value.datatype in (DataType.BYTE, DataType.WORD, DataType.MATRIX):
# truncate the rvalue if needed
if isinstance(r_value, ParseResult.FloatValue):
truncated, value = self.coerce_value(self.sourceref, l_value.datatype, r_value.value)
if truncated:
r_value = ParseResult.IntegerValue(int(value), datatype=l_value.datatype, name=r_value.name)
if r_value.constant and operator in ("+=", "-="):
if operator == "+=":
if r_value.value > 0: # type: ignore
return ParseResult.InplaceIncrStmt(l_value, r_value.value, self.sourceref.line) # type: ignore
elif r_value.value < 0: # type: ignore
return ParseResult.InplaceDecrStmt(l_value, -r_value.value, self.sourceref.line) # type: ignore
else:
2017-12-28 18:08:33 +00:00
self.print_warning("incr with zero, ignored")
2017-12-28 03:20:59 +00:00
else:
if r_value.value > 0: # type: ignore
return ParseResult.InplaceDecrStmt(l_value, r_value.value, self.sourceref.line) # type: ignore
elif r_value.value < 0: # type: ignore
return ParseResult.InplaceIncrStmt(l_value, -r_value.value, self.sourceref.line) # type: ignore
else:
2017-12-28 18:08:33 +00:00
self.print_warning("decr with zero, ignored")
2017-12-28 03:20:59 +00:00
return ParseResult.AugmentedAssignmentStmt(l_value, operator, r_value, self.sourceref.line)
2017-12-21 13:52:30 +00:00
def parse_return(self, line: str) -> ParseResult.ReturnStmt:
parts = line.split(maxsplit=1)
if parts[0] != "return":
raise self.PError("invalid statement, return expected")
a = x = y = None
values = [] # type: List[str]
if len(parts) > 1:
values = parts[1].split(",")
if len(values) == 0:
2017-12-27 18:01:14 +00:00
return ParseResult.ReturnStmt(self.sourceref.line)
2017-12-21 13:52:30 +00:00
else:
a = self.parse_expression(values[0]) if values[0] else None
if len(values) > 1:
x = self.parse_expression(values[1]) if values[1] else None
if len(values) > 2:
y = self.parse_expression(values[2]) if values[2] else None
if len(values) > 3:
raise self.PError("too many returnvalues")
2017-12-27 18:01:14 +00:00
return ParseResult.ReturnStmt(self.sourceref.line, a, x, y)
2017-12-21 13:52:30 +00:00
def parse_asm(self) -> ParseResult.InlineAsm:
line = self.next_line()
2017-12-21 22:05:35 +00:00
lineno = self.sourceref.line
2017-12-21 13:52:30 +00:00
aline = line.split()
if not len(aline) == 2 or aline[0] != "asm" or aline[1] != "{":
raise self.PError("invalid asm start")
asmlines = [] # type: List[str]
while True:
line = self.next_line()
if line.strip() == "}":
2017-12-27 18:01:14 +00:00
return ParseResult.InlineAsm(asmlines, lineno)
2017-12-25 18:09:10 +00:00
# asm can refer to other symbols as well, track subroutine usage
2017-12-25 21:22:19 +00:00
splits = line.split(maxsplit=1)
if len(splits) == 2:
for match in re.finditer(r"(?P<symbol>[a-zA-Z_$][a-zA-Z0-9_\.]+)", splits[1]):
name = match.group("symbol")
if name[0] == '$':
continue
try:
if '.' not in name:
name = self.cur_block.symbols.parent.name + '.' + name
_, symbol = self.lookup_with_ppsymbols(name)
except ParseError:
pass
else:
if isinstance(symbol, SubroutineDef):
self.result.sub_used_by(symbol, self.sourceref)
2017-12-21 13:52:30 +00:00
asmlines.append(line)
def parse_asminclude(self, line: str) -> ParseResult.InlineAsm:
aline = line.split()
if len(aline) < 2:
raise self.PError("invalid asminclude or asmbinary statement")
filename = aline[1]
if not filename.startswith('"') or not filename.endswith('"'):
raise self.PError("filename must be between quotes")
filename = filename[1:-1]
if not filename:
raise self.PError("invalid filename")
filename_in_sourcedir = os.path.join(os.path.split(self.sourceref.file)[0], filename)
filename_in_output_location = os.path.join(self.outputdir, filename)
if not os.path.isfile(filename_in_sourcedir):
raise self.PError("included file not found")
print("copying included file to output location:", filename)
shutil.copy(filename_in_sourcedir, filename_in_output_location)
if aline[0] == "asminclude":
if len(aline) == 3:
scopename = aline[2]
lines = ['{:s}\t.binclude "{:s}"'.format(scopename, filename)]
else:
raise self.PError("invalid asminclude statement")
2017-12-27 18:01:14 +00:00
return ParseResult.InlineAsm(lines, self.sourceref.line)
2017-12-21 13:52:30 +00:00
elif aline[0] == "asmbinary":
if len(aline) == 4:
offset = parse_expr_as_int(aline[2], None, None, self.sourceref)
length = parse_expr_as_int(aline[3], None, None, self.sourceref)
2017-12-21 13:52:30 +00:00
lines = ['\t.binary "{:s}", ${:04x}, ${:04x}'.format(filename, offset, length)]
elif len(aline) == 3:
offset = parse_expr_as_int(aline[2], None, None, self.sourceref)
2017-12-21 13:52:30 +00:00
lines = ['\t.binary "{:s}", ${:04x}'.format(filename, offset)]
elif len(aline) == 2:
lines = ['\t.binary "{:s}"'.format(filename)]
else:
raise self.PError("invalid asmbinary statement")
2017-12-27 18:01:14 +00:00
return ParseResult.InlineAsm(lines, self.sourceref.line)
2017-12-21 13:52:30 +00:00
else:
raise self.PError("invalid statement")
2017-12-23 13:36:23 +00:00
def parse_expression(self, text: str, is_indirect=False) -> ParseResult.Value:
2017-12-21 13:52:30 +00:00
# parse an expression into whatever it is (primitive value, register, memory, register, etc)
text = text.strip()
if not text:
raise self.PError("value expected")
if text[0] == '#':
2017-12-23 13:36:23 +00:00
if is_indirect:
raise self.PError("using the address-of something in an indirect value makes no sense")
2017-12-21 13:52:30 +00:00
# take the pointer (memory address) from the thing that follows this
expression = self.parse_expression(text[1:])
2017-12-21 13:52:30 +00:00
if isinstance(expression, ParseResult.StringValue):
return expression
elif isinstance(expression, ParseResult.MemMappedValue):
return ParseResult.IntegerValue(expression.address, datatype=DataType.WORD, name=expression.name)
else:
raise self.PError("cannot take the address of this type")
2017-12-27 23:44:17 +00:00
elif text[0] in "-.0123456789$%~":
number = parse_expr_as_number(text, self.cur_block.symbols, self.ppsymbols, self.sourceref)
2017-12-21 13:52:30 +00:00
try:
if type(number) is int:
return ParseResult.IntegerValue(int(number))
elif type(number) is float:
return ParseResult.FloatValue(number)
else:
raise TypeError("invalid number type")
except (ValueError, OverflowError) as ex:
raise self.PError(str(ex))
elif text in REGISTER_WORDS:
return ParseResult.RegisterValue(text, DataType.WORD)
2017-12-28 03:20:59 +00:00
elif text in REGISTER_BYTES | REGISTER_SBITS:
2017-12-21 13:52:30 +00:00
return ParseResult.RegisterValue(text, DataType.BYTE)
elif (text.startswith("'") and text.endswith("'")) or (text.startswith('"') and text.endswith('"')):
strvalue = parse_expr_as_string(text, self.cur_block.symbols, self.ppsymbols, self.sourceref)
2017-12-21 13:52:30 +00:00
if len(strvalue) == 1:
petscii_code = char_to_bytevalue(strvalue)
return ParseResult.IntegerValue(petscii_code)
return ParseResult.StringValue(strvalue)
elif text == "true":
return ParseResult.IntegerValue(1)
elif text == "false":
return ParseResult.IntegerValue(0)
elif self.is_identifier(text):
2017-12-25 21:22:19 +00:00
symblock, sym = self.lookup_with_ppsymbols(text)
if isinstance(sym, (VariableDef, ConstantDef)):
2017-12-21 13:52:30 +00:00
constant = isinstance(sym, ConstantDef)
if self.cur_block is symblock:
2017-12-21 13:52:30 +00:00
symbolname = sym.name
else:
symbolname = "{:s}.{:s}".format(sym.blockname, sym.name)
if isinstance(sym, VariableDef) and sym.register:
return ParseResult.RegisterValue(sym.register, sym.type, name=symbolname)
elif sym.type in (DataType.BYTE, DataType.WORD, DataType.FLOAT):
if isinstance(sym, ConstantDef):
2017-12-23 00:53:48 +00:00
if sym.type == DataType.FLOAT:
return ParseResult.FloatValue(sym.value, sym.name) # type: ignore
elif sym.type in (DataType.BYTE, DataType.WORD):
return ParseResult.IntegerValue(sym.value, datatype=sym.type, name=sym.name) # type: ignore
elif sym.type in STRING_DATATYPES:
return ParseResult.StringValue(sym.value, sym.name, True) # type: ignore
else:
raise TypeError("invalid const type", sym.type)
2017-12-21 13:52:30 +00:00
else:
2017-12-23 00:53:48 +00:00
return ParseResult.MemMappedValue(sym.address, sym.type, sym.length, name=symbolname, constant=constant)
2017-12-21 13:52:30 +00:00
elif sym.type in STRING_DATATYPES:
return ParseResult.StringValue(sym.value, name=symbolname, constant=constant) # type: ignore
elif sym.type == DataType.MATRIX:
raise self.PError("cannot manipulate matrix directly, use one of the matrix procedures")
elif sym.type == DataType.BYTEARRAY or sym.type == DataType.WORDARRAY:
raise self.PError("cannot manipulate array directly, use one of the array procedures")
else:
2017-12-23 00:53:48 +00:00
raise self.PError("invalid symbol type")
elif isinstance(sym, LabelDef):
name = sym.name if symblock is self.cur_block else sym.blockname + '.' + sym.name
return ParseResult.MemMappedValue(None, DataType.WORD, 1, name, True)
elif isinstance(sym, SubroutineDef):
2017-12-25 18:09:10 +00:00
self.result.sub_used_by(sym, self.sourceref)
2017-12-23 00:53:48 +00:00
name = sym.name if symblock is self.cur_block else sym.blockname + '.' + sym.name
return ParseResult.MemMappedValue(sym.address, DataType.WORD, 1, name, True)
2017-12-21 13:52:30 +00:00
else:
2017-12-23 00:53:48 +00:00
raise self.PError("invalid symbol type")
2017-12-21 13:52:30 +00:00
elif text.startswith('[') and text.endswith(']'):
2017-12-23 00:53:48 +00:00
return self.parse_indirect_value(text)[1]
2017-12-21 13:52:30 +00:00
else:
2017-12-28 03:20:59 +00:00
raise self.PError("invalid single value '" + text + "'") # @todo understand complex expressions
2017-12-21 13:52:30 +00:00
2017-12-24 23:15:04 +00:00
def parse_indirect_value(self, text: str, allow_mmapped_for_call: bool=False) -> Tuple[str, ParseResult.IndirectValue]:
2017-12-23 00:53:48 +00:00
indirect = text[1:-1].strip()
indirect2, sep, typestr = indirect.rpartition('.')
type_modifier = None
if sep:
if typestr in ("byte", "word", "float"):
type_modifier, type_len, _ = self.get_datatype(sep + typestr)
indirect = indirect2
2017-12-23 13:36:23 +00:00
expr = self.parse_expression(indirect, True)
2017-12-23 00:53:48 +00:00
if not isinstance(expr, (ParseResult.IntegerValue, ParseResult.MemMappedValue, ParseResult.RegisterValue)):
raise self.PError("only integers, memmapped vars, and registers can be used in an indirect value")
2017-12-23 13:36:23 +00:00
if type_modifier is None:
if isinstance(expr, (ParseResult.RegisterValue, ParseResult.MemMappedValue)):
type_modifier = expr.datatype
else:
type_modifier = DataType.BYTE
2017-12-23 00:53:48 +00:00
if isinstance(expr, ParseResult.IntegerValue):
2017-12-23 13:36:23 +00:00
if type_modifier not in (DataType.BYTE, DataType.WORD, DataType.FLOAT):
2017-12-23 00:53:48 +00:00
raise self.PError("invalid type modifier for the value's datatype")
elif isinstance(expr, ParseResult.MemMappedValue):
2017-12-24 23:15:04 +00:00
if allow_mmapped_for_call:
if type_modifier and expr.datatype != type_modifier:
raise self.PError("invalid type modifier for the value's datatype, must be " + expr.datatype.name)
else:
raise self.PError("use variable directly instead of using indirect addressing")
2017-12-23 00:53:48 +00:00
return indirect, ParseResult.IndirectValue(expr, type_modifier)
2017-12-21 13:52:30 +00:00
def is_identifier(self, name: str) -> bool:
if name.isidentifier():
return True
blockname, sep, name = name.partition(".")
if sep:
return blockname.isidentifier() and name.isidentifier()
return False
2017-12-25 21:22:19 +00:00
def lookup_with_ppsymbols(self, dottedname: str) -> Tuple[ParseResult.Block, Union[SymbolDefinition, SymbolTable]]:
# Tries to find a symbol, if it cannot be located, the symbol table from the preprocess parse phase is consulted as well
symblock, sym = self.cur_block.lookup(dottedname)
2017-12-25 21:22:19 +00:00
if sym is None and self.ppsymbols:
# symbol is not (yet) known, see if the symbols from the preprocess parse phase know about it
if '.' not in dottedname:
dottedname = self.cur_block.name + '.' + dottedname
try:
2017-12-25 21:22:19 +00:00
symtable, sym = self.ppsymbols.lookup(dottedname)
assert dottedname.startswith(symtable.name)
symblock = None # the block might not have been parsed yet, so just return this instead
except (LookupError, SymbolError) as x:
raise self.PError(str(x))
return symblock, sym
2017-12-21 13:52:30 +00:00
def _size_from_arraydecl(self, decl: str) -> int:
return parse_expr_as_int(decl[:-1].split("(")[-1], self.cur_block.symbols, self.ppsymbols, self.sourceref)
2017-12-21 13:52:30 +00:00
def _size_from_matrixdecl(self, decl: str) -> Tuple[int, int]:
dimensions = decl[:-1].split("(")[-1]
try:
xs, ys = dimensions.split(",")
except ValueError:
raise self.PError("invalid matrix dimensions")
return (parse_expr_as_int(xs, self.cur_block.symbols, self.ppsymbols, self.sourceref),
parse_expr_as_int(ys, self.cur_block.symbols, self.ppsymbols, self.sourceref))
2017-12-21 13:52:30 +00:00
def coerce_value(self, sourceref: SourceRef, datatype: DataType, value: PrimitiveType) -> Tuple[bool, PrimitiveType]:
# if we're a BYTE type, and the value is a single character, convert it to the numeric value
if datatype in (DataType.BYTE, DataType.BYTEARRAY, DataType.MATRIX) and isinstance(value, str):
if len(value) == 1:
return True, char_to_bytevalue(value)
# if we're an integer value and the passed value is float, truncate it (and give a warning)
if datatype in (DataType.BYTE, DataType.WORD, DataType.MATRIX) and type(value) is float:
frac = math.modf(value) # type:ignore
if frac != 0:
2017-12-28 18:08:33 +00:00
self.print_warning("float value truncated")
return True, int(value)
return False, value
@staticmethod
def to_hex(number: int) -> str:
# 0..255 -> "$00".."$ff"
# 256..65536 -> "$0100".."$ffff"
if 0 <= number < 0x100:
return "${:02x}".format(number)
if number < 0x10000:
return "${:04x}".format(number)
raise OverflowError(number)
2017-12-21 13:52:30 +00:00
def psplit(self, sentence: str, separators: str=" \t", lparen: str="(", rparen: str=")") -> List[str]:
"""split a sentence but not on separators within parenthesis"""
nb_brackets = 0
sentence = sentence.strip(separators) # get rid of leading/trailing seps
indices = [0]
for i, c in enumerate(sentence):
if c == lparen:
nb_brackets += 1
elif c == rparen:
nb_brackets -= 1
elif c in separators and nb_brackets == 0:
indices.append(i)
# handle malformed string
if nb_brackets < 0:
raise self.PError("syntax error")
indices.append(len(sentence))
# handle missing closing parentheses
if nb_brackets > 0:
raise self.PError("syntax error")
result = [sentence[i:j].strip(separators) for i, j in zip(indices, indices[1:])]
return list(filter(None, result)) # remove empty strings
2017-12-27 18:01:14 +00:00
def parse_if_condition(self, ifpart: str, conditionpart: str) -> ParseResult.IfCondition:
if ifpart == "if":
ifstatus = "true"
else:
ifstatus = ifpart[3:]
2017-12-28 18:08:33 +00:00
if ifstatus not in ParseResult.IfCondition.IF_STATUSES:
2017-12-27 18:01:14 +00:00
raise self.PError("invalid if form")
if conditionpart:
2017-12-28 18:08:33 +00:00
if ifstatus not in ("true", "not", "zero"):
raise self.PError("can only have if[_true], if_not or if_zero when using a comparison expression")
2017-12-27 18:01:14 +00:00
left, operator, right = parse_expr_as_comparison(conditionpart, self.sourceref)
leftv = self.parse_expression(left)
if not operator and isinstance(leftv, (ParseResult.IntegerValue, ParseResult.FloatValue, ParseResult.StringValue)):
raise self.PError("condition is a constant value")
if isinstance(leftv, ParseResult.RegisterValue):
if leftv.register in {"SC", "SZ", "SI"}:
raise self.PError("cannot use a status bit register explicitly in a condition")
if operator:
rightv = self.parse_expression(right)
else:
rightv = None
if leftv == rightv:
raise self.PError("left and right values in comparison are identical")
2017-12-28 18:08:33 +00:00
result = ParseResult.IfCondition(ifstatus, leftv, operator, rightv, self.sourceref.line)
2017-12-27 18:01:14 +00:00
else:
2017-12-28 18:08:33 +00:00
result = ParseResult.IfCondition(ifstatus, None, "", None, self.sourceref.line)
if result.make_if_true():
self.print_warning("if_not condition inverted to if")
return result
2017-12-27 18:01:14 +00:00
2017-12-30 12:34:52 +00:00
def check_import_okay(self, filename: str) -> bool:
if filename == self.sourceref.file and not filename.endswith("il65lib.ill"):
raise self.PError("can't import itself")
if filename in self.existing_imports:
return False
self.existing_imports.add(filename)
return True
2017-12-21 13:52:30 +00:00
class Optimizer:
def __init__(self, parseresult: ParseResult) -> None:
self.parsed = parseresult
def optimize(self) -> ParseResult:
print("\noptimizing parse tree")
2017-12-28 22:03:59 +00:00
for block in self.parsed.all_blocks():
2017-12-25 12:20:23 +00:00
self.remove_identity_assigns(block)
2017-12-21 13:52:30 +00:00
self.combine_assignments_into_multi(block)
self.optimize_multiassigns(block)
2017-12-25 18:09:10 +00:00
self.remove_unused_subroutines(block)
2017-12-27 18:01:14 +00:00
self.optimize_compare_with_zero(block)
2017-12-21 13:52:30 +00:00
return self.parsed
2017-12-27 18:01:14 +00:00
def optimize_compare_with_zero(self, block: ParseResult.Block) -> None:
# a conditional goto that compares a value to zero will be simplified
# the comparison operator and rvalue (0) will be removed and the if-status changed accordingly
for stmt in block.statements:
if isinstance(stmt, ParseResult.CallStmt):
2017-12-27 23:44:17 +00:00
cond = stmt.condition
if cond and isinstance(cond.rvalue, (ParseResult.IntegerValue, ParseResult.FloatValue)) and cond.rvalue.value == 0:
simplified = False
if cond.ifstatus in ("true", "ne"):
if cond.comparison_op == "==":
# if_true something == 0 -> if_not something
cond.ifstatus = "not"
cond.comparison_op, cond.rvalue = "", None
simplified = True
elif cond.comparison_op == "!=":
# if_true something != 0 -> if_true something
cond.comparison_op, cond.rvalue = "", None
simplified = True
elif cond.ifstatus in ("not", "eq"):
if cond.comparison_op == "==":
# if_not something == 0 -> if_true something
cond.ifstatus = "true"
cond.comparison_op, cond.rvalue = "", None
simplified = True
elif cond.comparison_op == "!=":
# if_not something != 0 -> if_not something
cond.comparison_op, cond.rvalue = "", None
simplified = True
if simplified:
print("{:s}:{:d}: simplified comparison with zero".format(block.sourceref.file, stmt.lineno))
2017-12-21 13:52:30 +00:00
def combine_assignments_into_multi(self, block: ParseResult.Block) -> None:
# fold multiple consecutive assignments with the same rvalue into one multi-assignment
statements = [] # type: List[ParseResult._AstNode]
2017-12-21 13:52:30 +00:00
multi_assign_statement = None
for stmt in block.statements:
2017-12-28 03:20:59 +00:00
if isinstance(stmt, ParseResult.AssignmentStmt) and not isinstance(stmt, ParseResult.AugmentedAssignmentStmt):
2017-12-21 13:52:30 +00:00
if multi_assign_statement and multi_assign_statement.right == stmt.right:
multi_assign_statement.leftvalues.extend(stmt.leftvalues)
print("{:s}:{:d}: joined with previous line into multi-assign statement".format(block.sourceref.file, stmt.lineno))
else:
if multi_assign_statement:
statements.append(multi_assign_statement)
multi_assign_statement = stmt
else:
if multi_assign_statement:
statements.append(multi_assign_statement)
multi_assign_statement = None
statements.append(stmt)
if multi_assign_statement:
statements.append(multi_assign_statement)
block.statements = statements
def optimize_multiassigns(self, block: ParseResult.Block) -> None:
# optimize multi-assign statements.
for stmt in block.statements:
if isinstance(stmt, ParseResult.AssignmentStmt) and len(stmt.leftvalues) > 1:
# remove duplicates
lvalues = list(set(stmt.leftvalues))
if len(lvalues) != len(stmt.leftvalues):
print("{:s}:{:d}: removed duplicate assignment targets".format(block.sourceref.file, stmt.lineno))
# change order: first registers, then zp addresses, then non-zp addresses, then the rest (if any)
2017-12-24 23:15:04 +00:00
stmt.leftvalues = list(sorted(lvalues, key=_value_sortkey))
2017-12-25 12:20:23 +00:00
def remove_identity_assigns(self, block: ParseResult.Block) -> None:
have_removed_stmts = False
for index, stmt in enumerate(list(block.statements)):
if isinstance(stmt, ParseResult.AssignmentStmt):
stmt.remove_identity_lvalues(block.sourceref.file, stmt.lineno)
if not stmt.leftvalues:
print("{:s}:{:d}: removed identity assignment statement".format(block.sourceref.file, stmt.lineno))
have_removed_stmts = True
block.statements[index] = None
if have_removed_stmts:
# remove the Nones
block.statements = [s for s in block.statements if s is not None]
2017-12-25 18:09:10 +00:00
def remove_unused_subroutines(self, block: ParseResult.Block) -> None:
# some symbols are used by the emitted assembly code from the code generator,
# and should never be removed or the assembler will fail
never_remove = {"c64util.GIVUAYF", "c64.FREADUY", "c64.FTOMEMXY"}
discarded = []
for sub in list(block.symbols.iter_subroutines()):
usages = self.parsed.subroutine_usage[(sub.blockname, sub.name)]
if not usages and sub.blockname + '.' + sub.name not in never_remove:
block.symbols.discard_sub(sub.name)
discarded.append(sub.name)
if discarded:
2017-12-30 12:34:52 +00:00
print("{}: discarded {:d} unused subroutines from block '{:s}'".format(block.sourceref, len(discarded), block.name))
2017-12-25 18:09:10 +00:00
2017-12-21 13:52:30 +00:00
2017-12-24 23:15:04 +00:00
def _value_sortkey(value: ParseResult.Value) -> int:
2017-12-21 13:52:30 +00:00
if isinstance(value, ParseResult.RegisterValue):
num = 0
for char in value.register:
num *= 100
num += ord(char)
return num
elif isinstance(value, ParseResult.MemMappedValue):
2017-12-24 23:15:04 +00:00
if value.address is None:
return 99999999
2017-12-21 13:52:30 +00:00
if value.address < 0x100:
return 10000 + value.address
else:
return 20000 + value.address
else:
return 99999999