547 lines
20 KiB
Python
547 lines
20 KiB
Python
|
|
# XXX Backquoted words should have precedence over grouping
|
|
# XXX Give warning when starting new block after calls were made
|
|
# XXX Give warning when def-block contains 'call' put no 'push'
|
|
# XXX Give warning when def-block contains code but no 'ret'
|
|
# XXX Give warning when a variable is not both written and read
|
|
|
|
from asm import *
|
|
import string
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
class Program:
|
|
def __init__(self, name, forRom=True):
|
|
self.name = name # For defining unique labels in global symbol table
|
|
self.forRom = forRom # Inject trampolines if compiling for ROM XXX why not do that outside?
|
|
self.comments = [] # Stack of line numbers
|
|
self.lineNumber = 0
|
|
self.lastWord = None
|
|
self.filename = None
|
|
self.openBlocks = [0] # Outside first block is 0
|
|
self.nextBlockId = 1
|
|
self.loops = {} # blockId -> address after `do'
|
|
self.elses = {} # blockId -> count of `else'
|
|
self.defs = {} # blockId -> address of last `def'
|
|
self.vars = {} # name -> address (GCL variables)
|
|
self.segStart = None
|
|
self.vPC = None
|
|
self.segId = 0
|
|
self.version = None # Must be first word 'gcl<N>'
|
|
self.execute = None
|
|
self.needPatch = False
|
|
self.lengths = {} # block -> length, or var -> length
|
|
# XXX Provisional method to load mnemonics
|
|
try:
|
|
loadBindings(Path('Core') / 'v6502.json')
|
|
except FileNotFoundError:
|
|
loadBindings(Path(__file__).parent / 'v6502.json')
|
|
|
|
def org(self, address):
|
|
"""Set start address"""
|
|
self.closeSegment()
|
|
# Don't open new segment before the first byte comes
|
|
self.segStart = address
|
|
self.vPC = address
|
|
page = address & ~255
|
|
self.segEnd = page + (250 if 0x100 <= page <= 0x400 else 256)
|
|
|
|
def line(self, line):
|
|
"""Process a line by tokenizing and processing the words"""
|
|
|
|
self.lineNumber += 1
|
|
nextWord = ''
|
|
|
|
for nextChar in line:
|
|
if len(self.comments) > 0:
|
|
# Inside comments anything goes
|
|
if nextChar == '{': self.comments.append(self.lineNumber)
|
|
if nextChar == '}': self.comments.pop()
|
|
elif nextChar not in '{}[]':
|
|
if nextChar.isspace():
|
|
self.word(nextWord)
|
|
nextWord = ''
|
|
else:
|
|
nextWord += nextChar
|
|
else:
|
|
self.word(nextWord)
|
|
nextWord = ''
|
|
if nextChar == '{': self.comments.append(self.lineNumber)
|
|
elif nextChar == '}': self.error('Spurious %s' % repr(nextChar))
|
|
elif nextChar == '[':
|
|
self.openBlocks.append(self.nextBlockId)
|
|
self.elses[self.nextBlockId] = 0
|
|
self.nextBlockId += 1
|
|
elif nextChar == ']':
|
|
if len(self.openBlocks) <= 1:
|
|
self.error('Block close without open')
|
|
b = self.openBlocks.pop()
|
|
define('__%s_%d_cond%d__' % (self.name, b, self.elses[b]), prev(self.vPC))
|
|
del self.elses[b]
|
|
if b in self.defs:
|
|
self.lengths[self.thisBlock()] = self.vPC - self.defs[b] + 2
|
|
define('__%s_%#04x_def__' % (self.name, self.defs[b]), prev(self.vPC))
|
|
del self.defs[b]
|
|
elif nextChar == '(': pass
|
|
elif nextChar == ')': pass
|
|
self.word(nextWord)
|
|
|
|
def end(self):
|
|
"""Signal end of program"""
|
|
if len(self.comments) > 0:
|
|
self.lineNumber = self.comments[-1]
|
|
self.error('Unterminated comment')
|
|
self.closeSegment()
|
|
if len(self.openBlocks) > 1:
|
|
self.error('Unterminated block')
|
|
self.putInRomTable(0) # Zero marks the end of stream
|
|
if self.lineNumber > 0:
|
|
self.dumpVars()
|
|
|
|
def dumpVars(self):
|
|
print(' Variables count %d bytes %d end $%04x' % (len(self.vars), 2*len(self.vars), zpByte(0)))
|
|
line = ' :'
|
|
for var in sorted(self.vars.keys()):
|
|
if var in self.lengths and self.lengths[var]:
|
|
var += ' [%s]' % self.lengths[var]
|
|
if len(line + var) + 1 > 72:
|
|
print(line)
|
|
line = ' :'
|
|
line += ' ' + var
|
|
print(line)
|
|
|
|
def word(self, word):
|
|
# Process a GCL word and emit its corresponding vCPU code
|
|
if len(word) == 0:
|
|
return
|
|
self.lastWord = word
|
|
|
|
# Simple keywords
|
|
if not has(self.version):
|
|
if word in ['gcl0x']:
|
|
self.version = word
|
|
else:
|
|
self.error('Invalid GCL version')
|
|
elif word == 'def': self.emitDef()
|
|
elif word == 'do': self.loops[self.thisBlock()] = self.vPC
|
|
elif word == 'loop': self.emitLoop()
|
|
elif word == 'if<>0': self.emitIf('EQ')
|
|
elif word == 'if=0': self.emitIf('NE')
|
|
elif word == 'if>=0': self.emitIf('LT')
|
|
elif word == 'if<=0': self.emitIf('GT')
|
|
elif word == 'if>0': self.emitIf('LE')
|
|
elif word == 'if<0': self.emitIf('GE')
|
|
elif word == 'if<>0loop': self.emitIfLoop('NE')
|
|
elif word == 'if=0loop': self.emitIfLoop('EQ')
|
|
elif word == 'if>0loop': self.emitIfLoop('GT')
|
|
elif word == 'if<0loop': self.emitIfLoop('LT')
|
|
elif word == 'if>=0loop': self.emitIfLoop('GE')
|
|
elif word == 'if<=0loop': self.emitIfLoop('LE')
|
|
elif word == 'else': self.emitElse()
|
|
elif word == 'call': self.emitOp('CALL').emit(symbol('vAC'), '%04x vAC' % prev(self.vPC, 1))
|
|
elif word == 'push': self.emitOp('PUSH')
|
|
elif word == 'pop': self.emitOp('POP')
|
|
elif word == 'ret': self.emitOp('RET'); self.needPatch = self.needPatch or len(self.openBlocks) == 1 # Top-level use of 'ret' --> apply patch
|
|
elif word == 'peek': self.emitOp('PEEK')
|
|
elif word == 'deek': self.emitOp('DEEK')
|
|
else:
|
|
var, con, op = self.parseWord(word)
|
|
|
|
# Label definitions
|
|
if has(var) and has(con):
|
|
if op == '=' and var == 'zpReset': zpReset(con)
|
|
elif op == '=' and var == 'execute': self.execute = con
|
|
elif op == '=': self.defSymbol(var, con)
|
|
else: self.error("Invalid operator '%s' with name and constant" % op)
|
|
|
|
# Words with constant value as operand
|
|
elif has(con):
|
|
if not has(op):
|
|
if isinstance(con, int) and 0 <= con < 256:
|
|
self.emitOp('LDI')
|
|
else:
|
|
self.emitOp('LDWI').emit(lo(con)); con = hi(con)
|
|
elif op == '*= ': self.org(con); con = None
|
|
elif op == ';': self.emitOp('LDW')
|
|
elif op == '=': self.emitOp('STW'); self.depr('i=', 'i:')
|
|
elif op == ':' and con < 256: self.emitOp('STW')
|
|
elif op == ',': self.emitOp('LD')
|
|
elif op == '.': self.emitOp('ST')
|
|
elif op == '&': self.emitOp('ANDI')
|
|
elif op == '|': self.emitOp('ORI')
|
|
elif op == '^': self.emitOp('XORI')
|
|
elif op == '+': self.emitOp('ADDI')
|
|
elif op == '-': self.emitOp('SUBI')
|
|
elif op == '% =': self.emitOp('STLW')
|
|
elif op == '% ': self.emitOp('LDLW')
|
|
elif op == '--': self.emitOp('ALLOC'); con = 256-con if con else 0
|
|
elif op == '++': self.emitOp('ALLOC')
|
|
elif op == '< ++': self.emitOp('INC')
|
|
elif op == '> ++': self.emitOp('INC'); con += 1
|
|
elif op == '!!': self.emitOp('SYS'); con = self.sysTicks(con)
|
|
elif op == '!':
|
|
if isinstance(con, int) and 0 <= con < 256:
|
|
# XXX Deprecate in gcl1, replace with i!!
|
|
self.emitOp('SYS'); con = self.sysTicks(con);self.depr('i!', 'i!!')
|
|
else:
|
|
self.emitOp('CALLI_v5').emit(lo(con)); con = hi(con)
|
|
elif op == '?': self.emitOp('LUP'); #self.depr('i?', 'i??')
|
|
elif op == '??': self.emitOp('LUP')
|
|
elif op == '# ': self.emitOp(con); con = None # Silent truncation
|
|
elif op == '#< ': self.emitOp(con); con = None
|
|
elif op == '#> ': con = hi(con); assert self.segStart != self.vPC # XXX Conflict
|
|
elif op == '## ': self.emit(lo(con)).emit(hi(con)); con = None
|
|
elif op == '<<':
|
|
for i in range(con):
|
|
self.emitOp('LSLW')
|
|
con = None
|
|
# Deprecated syntax
|
|
elif op == ':': self.org(con); con = None; #self.depr('ii:', '*=ii')
|
|
elif op == '#': con &= 255; #self.depr('i#', '#i')
|
|
elif op == '<++': self.emitOp('INC'); #self.depr('i<++', '<i++')
|
|
elif op == '>++': self.emitOp('INC'); con += 1 #self.depr('i>++', '>i++')
|
|
elif op == '%=': self.emitOp('STLW'); #self.depr('i%=', '%i=')
|
|
elif op == '%': self.emitOp('LDLW'); #self.depr('i%', %i')
|
|
else:
|
|
self.error("Invalid operator '%s' with constant" % op)
|
|
if has(con):
|
|
self.emit(con)
|
|
|
|
# Words with variable or symbol name as operand
|
|
elif has(var):
|
|
offset = 0
|
|
if not has(op): self.emitOp('LDW')
|
|
elif op == '=': self.emitOp('STW'); self.updateDefInfo(var)
|
|
elif op == ',': self.emitOp('LDW').emitVar(var).emitOp('PEEK'); var = None
|
|
elif op == ';': self.emitOp('LDW').emitVar(var).emitOp('DEEK'); var = None
|
|
elif op == '.': self.emitOp('POKE')
|
|
elif op == ':': self.emitOp('DOKE')
|
|
elif op == '< ,': self.emitOp('LD')
|
|
elif op == '> ,': self.emitOp('LD'); offset = 1
|
|
elif op == '< .': self.emitOp('ST')
|
|
elif op == '> .': self.emitOp('ST'); offset = 1
|
|
elif op == '&': self.emitOp('ANDW')
|
|
elif op == '|': self.emitOp('ORW')
|
|
elif op == '^': self.emitOp('XORW')
|
|
elif op == '+': self.emitOp('ADDW')
|
|
elif op == '-': self.emitOp('SUBW')
|
|
elif op == '< ++': self.emitOp('INC')
|
|
elif op == '> ++': self.emitOp('INC'); offset = 1
|
|
elif op == '!': self.emitOp('CALL')
|
|
elif op == '`': self.emitQuote(var); var = None
|
|
elif op == '=*': self.defSymbol(var, self.vPC); var = None
|
|
elif op == '# ': self.emitImm(var); var = None
|
|
elif op == '#< ': self.emitImm(var); var = None
|
|
elif op == '#> ': self.emitImm(var, half=hi); var = None
|
|
elif op == '## ': self.emitImm(var).emit(hi(var[1:])); var = None
|
|
elif op == '#@ ': offset = -self.vPC-1 # PC relative, 6502 style
|
|
# Deprecated syntax
|
|
elif op == '<++': self.emitOp('INC'); #self.depr('X<++', '<X++')
|
|
elif op == '>++': self.emitOp('INC'); offset = 1; #self.depr('X>++', '>X++')
|
|
elif op == '<,': self.emitOp('LD'); #self.depr('X<,', '<X,')
|
|
elif op == '>,': self.emitOp('LD'); offset = 1; #self.depr('X>,', '>X,')
|
|
elif op == '<.': self.emitOp('ST'); #self.depr('X<.', '<X.')
|
|
elif op == '>.': self.emitOp('ST'); offset = 1; #self.depr('X>.', '>X.')
|
|
else:
|
|
self.error("Invalid operator '%s' with variable or symbol '%s'" % (op, var))
|
|
if has(var):
|
|
self.emitVar(var, offset)
|
|
|
|
else:
|
|
self.error('Invalid word')
|
|
|
|
def parseWord(self, word):
|
|
# Break word into pieces
|
|
|
|
word += '\0' # Avoid checking len() everywhere
|
|
sign = None
|
|
name, number, op = None, None, ''
|
|
|
|
if word[0] == '`':
|
|
# Quoted word
|
|
name, op = word[1:-1], word[0]
|
|
return name, number, op
|
|
|
|
ix = 0
|
|
prefixes = ['%', '#', '<', '>', '*', '=', '@']
|
|
if word[ix] in prefixes:
|
|
# Prefix operators
|
|
while word[ix] in prefixes:
|
|
op += word[ix]
|
|
ix += 1
|
|
op += ' ' # Space to demarcate prefix operators
|
|
|
|
if word[ix].isalpha() or word[ix] in ['&', '\\', '_']:
|
|
# Named variable or named constant
|
|
name = word[ix]
|
|
ix += 1
|
|
while word[ix].isalnum() or word[ix] == '_':
|
|
name += word[ix]
|
|
ix += 1
|
|
|
|
if word[ix] == '=':
|
|
# Infix symbol definition
|
|
op += word[ix]
|
|
# op += ' ' # Space to demarcate infix operator
|
|
ix += 1
|
|
|
|
if word[ix] in ['-', '+']:
|
|
# Number sign
|
|
sign = word[ix]
|
|
ix += 1
|
|
|
|
if word[ix] == '$' and word[ix+1] in string.hexdigits:
|
|
# Hexadecimal number
|
|
jx = ix+1
|
|
number = 0
|
|
while word[jx] in string.hexdigits:
|
|
o = string.hexdigits.index(word[jx])
|
|
number = 16*number + (o if o<16 else o-6)
|
|
jx += 1
|
|
ix = jx if jx-ix > 1 else 0
|
|
elif word[ix].isdigit():
|
|
# Decimal number
|
|
number = 0
|
|
while word[ix].isdigit():
|
|
number = 10*number + ord(word[ix]) - ord('0')
|
|
ix += 1
|
|
elif has(sign):
|
|
op += sign
|
|
sign = None
|
|
else:
|
|
pass
|
|
|
|
# Resolve '&_symbol' as the number it represents
|
|
if has(name) and name[0] == '&':
|
|
if name[1] == '_':
|
|
number = symbol(name[2:])
|
|
if not has(number):
|
|
number = name[2:] # Pass back as an unresolved reference without '_'
|
|
name = None
|
|
|
|
# Resolve '\symbol' as the number it represents
|
|
if has(name) and name[0] == '\\':
|
|
# Peeking into the assembler's symbol table (not GCL's)
|
|
# Substitute \symbol with its value, and keeping the operator
|
|
number = symbol(name[1:])
|
|
if not has(number):
|
|
number = name[1:] # Pass back as an unresolved reference
|
|
name = None
|
|
|
|
if sign == '-':
|
|
if has(number) and isinstance(number, int):
|
|
number = -number
|
|
else:
|
|
self.error('Unable to negate')
|
|
|
|
op += word[ix:-1] # Also strips sentinel '\0'
|
|
return (name, number, op if len(op)>0 else None)
|
|
|
|
def sysTicks(self, con):
|
|
# Convert maximum Gigatron cycles to the negative of excess ticks
|
|
if con & 1:
|
|
self.error('Invalid value (must be even, got %d)' % con)
|
|
extraTicks = con//2 - symbol('maxTicks')
|
|
return 256 - extraTicks if extraTicks > 0 else 0
|
|
|
|
def emitQuote(self, var):
|
|
if len(var) > 0:
|
|
d = '' # Replace backquotes with spaces
|
|
for c in var:
|
|
d += ' ' if c == '`' else c
|
|
else:
|
|
d = '`' # And symbol becomes a backquote
|
|
for c in d:
|
|
comment = '%04x %s' % (self.vPC, repr(c))
|
|
self.emit(ord(c), comment=comment)
|
|
|
|
def emitDef(self):
|
|
self.emitOp('DEF')
|
|
b = self.thisBlock()
|
|
if b in self.defs:
|
|
self.error('Second DEF in block')
|
|
self.defs[b] = self.vPC
|
|
self.emit(lo('__%s_%#04x_def__' % (self.name, self.vPC)))
|
|
|
|
def updateDefInfo(self, var):
|
|
# Heuristically track `def' lengths for reporting on stdout
|
|
if var not in self.lengths and self.thisBlock() in self.lengths:
|
|
self.lengths[var] = self.lengths[self.thisBlock()]
|
|
else:
|
|
self.lengths[var] = None # No def lengths can be associated
|
|
|
|
def emitLoop(self):
|
|
to = [b for b in self.openBlocks if b in self.loops]
|
|
if len(to) == 0:
|
|
self.error('Loop without do')
|
|
to = self.loops[to[-1]]
|
|
to = prev(to)
|
|
if self.vPC>>8 != to>>8:
|
|
self.error('Loop crosses page boundary')
|
|
self.emitOp('BRA')
|
|
self.emit(to&255)
|
|
|
|
def emitIf(self, cond):
|
|
self.emitOp('BCC')
|
|
self.emitOp(cond)
|
|
b = self.thisBlock()
|
|
self.emit(lo('__%s_%d_cond%d__' % (self.name, b, self.elses[b])))
|
|
|
|
def emitIfLoop(self, cond):
|
|
to = [blockId for blockId in self.openBlocks if blockId in self.loops]
|
|
if len(to) == 0:
|
|
self.error('Loop without do')
|
|
to = self.loops[to[-1]]
|
|
to = prev(to)
|
|
if self.vPC>>8 != to>>8:
|
|
self.error('Loop to different page')
|
|
self.emitOp('BCC')
|
|
self.emitOp(cond)
|
|
self.emit(to&255)
|
|
|
|
def emitElse(self):
|
|
self.emitOp('BRA')
|
|
b = self.thisBlock()
|
|
i = self.elses[b]
|
|
self.emit(lo('__%s_%d_cond%d__' % (self.name, b, i+1)))
|
|
define('__%s_%d_cond%d__' % (self.name, b, i), prev(self.vPC))
|
|
self.elses[b] = i+1
|
|
|
|
def emitOp(self, ins):
|
|
# Emit vCPU opcode
|
|
self.prepareSegment()
|
|
self.putInRomTable(lo(ins), '%04x %s' % (self.vPC, ins))
|
|
self.vPC += 1
|
|
return self
|
|
|
|
def emitVar(self, var, offset=0):
|
|
# Get or create address for GCL variable and emit it
|
|
# !!! Also safe at start of segment !!!
|
|
self.prepareSegment()
|
|
if var[0] == '_':
|
|
# _C notation for labels as variables
|
|
address, offset = lo(var[1:]), offset & 255
|
|
else:
|
|
# Regular GCL variable
|
|
if var not in self.vars:
|
|
self.vars[var] = zpByte(2)
|
|
address = self.vars[var]
|
|
comment = '%04x %s' % (prev(self.vPC, 1), repr(var))
|
|
comment += '%+d' % offset if offset else ''
|
|
byte = address + offset
|
|
if byte < -128 or byte >= 256:
|
|
self.error('Value %s out of range (must be -128..255)' % repr(byte))
|
|
self.putInRomTable(byte, comment)
|
|
self.vPC += 1
|
|
return self
|
|
|
|
def emitImm(self, var, half=lo):
|
|
# Emit low or high byte of symbol
|
|
# !!! Also safe at start of segment !!!
|
|
#
|
|
# Here we see the subtle differences between variables and named constants
|
|
# again. For named constants (preceeded by '_'), we want their value.
|
|
# For named variables, we want their address. This becomes evident with the
|
|
# '>' modifier: constant>>8 vs. address+1
|
|
self.prepareSegment()
|
|
if var[0] == '_':
|
|
address = half(var[1:])
|
|
else:
|
|
if var not in self.vars:
|
|
self.vars[var] = zpByte(2)
|
|
address = self.vars[var]
|
|
if half is hi:
|
|
address += 1
|
|
var = '>' + var
|
|
self.putInRomTable(address, '%04x %s' % (self.vPC, var))
|
|
self.vPC += 1
|
|
return self
|
|
|
|
def thisBlock(self):
|
|
return self.openBlocks[-1]
|
|
|
|
def prepareSegment(self):
|
|
# Check if there's space in the current segment
|
|
if self.vPC >= self.segEnd:
|
|
severity = self.warning if self.vPC & 255 > 0 else self.error
|
|
severity('Out of code space ($%04x)' % self.vPC)
|
|
|
|
# And write header bytes for a new segment
|
|
if self.segStart == self.vPC:
|
|
# This must come before any lo() or hi()
|
|
# Write header for GT1 segment
|
|
address = self.segStart
|
|
if not has(self.execute) and address >= 0x200:
|
|
self.execute = address
|
|
assert self.segId == 0 or address>>8 != 0 # Zero-page segment can only be first
|
|
self.putInRomTable(address>>8, '| RAM segment address (high byte first)')
|
|
self.putInRomTable(address&255, '|')
|
|
# Fill in the length through the symbol table
|
|
self.putInRomTable(lo('__%s_seg%d__' % (self.name, self.segId)), '| Length (1..256)')
|
|
|
|
def emit(self, byte, comment=None):
|
|
# Next program byte in RAM
|
|
self.prepareSegment()
|
|
if not isinstance(byte, (int, float)):
|
|
self.error('Invalid value (number expected, got %s)' % repr(byte))
|
|
if byte < -128 or byte >= 256:
|
|
self.error('Value %s out of range (must be -128..255)' % repr(byte))
|
|
self.putInRomTable(byte, comment)
|
|
self.vPC += 1
|
|
return self
|
|
|
|
def closeSegment(self):
|
|
# Register length of GT1 segment
|
|
if self.vPC != self.segStart:
|
|
print(' Segment at $%04x size %3d used %3d unused %3d' % (
|
|
self.segStart,
|
|
self.segEnd - self.segStart,
|
|
self.vPC - self.segStart,
|
|
self.segEnd - self.vPC))
|
|
length = self.vPC - self.segStart
|
|
assert 1 <= length <= 256
|
|
define('__%s_seg%d__' % (self.name, self.segId), length)
|
|
self.segId += 1
|
|
|
|
def putInRomTable(self, byte, comment=None):
|
|
if byte < -128 or byte >= 256:
|
|
self.error('Value %s out of range (must be -128..255)' % repr(byte))
|
|
ld(byte)
|
|
if comment:
|
|
C(comment)
|
|
if self.forRom and pc()&255 == 251:
|
|
trampoline()
|
|
|
|
def depr(self, old, new):
|
|
var, con, _op = self.parseWord(self.lastWord)
|
|
old = old.replace(' ', str(con) if has(con) else var)
|
|
new = new.replace(' ', str(con) if has(con) else var)
|
|
self.warning('%s is deprecated, please use %s' % (old, new))
|
|
|
|
def warning(self, message):
|
|
highlight(self.prefix('Warning'), message)
|
|
|
|
def error(self, message):
|
|
highlight(self.prefix('Error'), message)
|
|
sys.exit(1)
|
|
|
|
def prefix(self, prefix):
|
|
# Informative line prefix for warning and error messages
|
|
if has(self.filename):
|
|
prefix += ' file %s' % repr(self.filename)
|
|
if self.lineNumber != 0:
|
|
prefix += ':%s' % self.lineNumber
|
|
if has(self.lastWord):
|
|
prefix += ' (%s)' % self.lastWord
|
|
return prefix + ':'
|
|
|
|
def defSymbol(self, name, value):
|
|
# Define a label from GCL in the systems symbol table
|
|
if name[0] != '_':
|
|
self.error('Symbol \'%s\' must begin with underscore (\'_\')' % name)
|
|
define(name[1:], value)
|
|
|
|
def prev(address, step=2):
|
|
# Take vPC two bytes back, wrap around if needed to stay on page
|
|
return (address & ~255) | ((address-step) & 255)
|