509 lines
19 KiB
Python
509 lines
19 KiB
Python
# Copyright (c) 2014-2024, Chris Pressey, Cat's Eye Technologies.
|
|
# This file is distributed under a 2-clause BSD license. See LICENSES/ dir.
|
|
# SPDX-License-Identifier: LicenseRef-BSD-2-Clause-X-SixtyPical
|
|
|
|
# encoding: UTF-8
|
|
|
|
from sixtypical.ast import (
|
|
Program, Defn, Routine, Block, SingleOp, Reset, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
|
|
)
|
|
from sixtypical.model import (
|
|
TYPE_BIT, TYPE_BYTE, TYPE_WORD,
|
|
RoutineType, VectorType, TableType, PointerType,
|
|
ConstantRef, IndirectRef, IndexedRef,
|
|
)
|
|
from sixtypical.scanner import Scanner
|
|
from sixtypical.symtab import SymEntry
|
|
|
|
|
|
class ForwardReference(object):
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def __repr__(self):
|
|
return "%s(%r)" % (self.__class__.__name__, self.name)
|
|
|
|
|
|
class Parser(object):
|
|
def __init__(self, symtab, text, filename, include_path):
|
|
self.symtab = symtab
|
|
self.include_path = include_path
|
|
self.scanner = Scanner(text, filename)
|
|
self.current_routine_name = None
|
|
|
|
def syntax_error(self, msg):
|
|
self.scanner.syntax_error(msg)
|
|
|
|
def lookup(self, name, allow_forward=False, routine_name=None):
|
|
model = self.symtab.fetch_global_ref(name)
|
|
if model is None and routine_name:
|
|
model = self.symtab.fetch_local_ref(routine_name, name)
|
|
if model is None and allow_forward:
|
|
return ForwardReference(name)
|
|
if model is None:
|
|
self.syntax_error('Undefined symbol "{}"'.format(name))
|
|
return model
|
|
|
|
def declare(self, name, ast_node, type_):
|
|
if self.symtab.fetch_global_ref(name):
|
|
self.syntax_error('Symbol "%s" already declared' % name)
|
|
self.symtab.symbols[name] = SymEntry(ast_node, type_)
|
|
|
|
def declare_local(self, routine_name, name, ast_node, type_):
|
|
if self.symtab.fetch_local_ref(routine_name, name):
|
|
self.syntax_error('Symbol "%s" already declared locally' % name)
|
|
if self.symtab.fetch_global_ref(name):
|
|
self.syntax_error('Symbol "%s" already declared globally' % name)
|
|
self.symtab.locals.setdefault(routine_name, {})[name] = SymEntry(ast_node, type_)
|
|
|
|
# ---- symbol resolution
|
|
|
|
def resolve_symbols(self, program):
|
|
# This could stand to be better unified.
|
|
|
|
def resolve(w):
|
|
return self.lookup(w.name) if isinstance(w, ForwardReference) else w
|
|
|
|
def backpatched_type(type_):
|
|
if isinstance(type_, TableType):
|
|
return TableType(backpatched_type(type_.of_type), type_.size)
|
|
elif isinstance(type_, VectorType):
|
|
return VectorType(backpatched_type(type_.of_type))
|
|
elif isinstance(type_, RoutineType):
|
|
return RoutineType(
|
|
frozenset([resolve(w) for w in type_.inputs]),
|
|
frozenset([resolve(w) for w in type_.outputs]),
|
|
frozenset([resolve(w) for w in type_.trashes]),
|
|
)
|
|
else:
|
|
return type_
|
|
|
|
for name, symentry in self.symtab.symbols.items():
|
|
symentry.type_ = backpatched_type(symentry.type_)
|
|
|
|
def resolve_fwd_reference(obj, field):
|
|
field_value = getattr(obj, field, None)
|
|
if isinstance(field_value, ForwardReference):
|
|
setattr(obj, field, self.lookup(field_value.name))
|
|
elif isinstance(field_value, IndexedRef):
|
|
if isinstance(field_value.ref, ForwardReference):
|
|
field_value.ref = self.lookup(field_value.ref.name)
|
|
|
|
for node in program.all_children():
|
|
if isinstance(node, SingleOp):
|
|
resolve_fwd_reference(node, 'src')
|
|
resolve_fwd_reference(node, 'dest')
|
|
if isinstance(node, (Call, GoTo)):
|
|
resolve_fwd_reference(node, 'location')
|
|
|
|
# --- grammar productions
|
|
|
|
def program(self):
|
|
defns = []
|
|
routines = []
|
|
includes = []
|
|
while self.scanner.consume('include'):
|
|
filename = self.scanner.token
|
|
self.scanner.scan()
|
|
program = load_program(filename, self.symtab, self.include_path, include_file=True)
|
|
includes.append(program)
|
|
while self.scanner.on('typedef', 'const'):
|
|
if self.scanner.on('typedef'):
|
|
self.typedef()
|
|
if self.scanner.on('const'):
|
|
self.defn_const()
|
|
typenames = ['byte', 'word', 'table', 'vector', 'pointer'] # 'routine',
|
|
typenames.extend(self.symtab.typedefs.keys())
|
|
while self.scanner.on(*typenames):
|
|
type_, defn = self.defn()
|
|
self.declare(defn.name, defn, type_)
|
|
defns.append(defn)
|
|
while self.scanner.consume('define'):
|
|
name = self.scanner.token
|
|
self.scanner.scan()
|
|
self.current_routine_name = name
|
|
preserved = False
|
|
if self.scanner.consume('preserved'):
|
|
preserved = True
|
|
type_, routine = self.routine(name)
|
|
self.declare(name, routine, type_)
|
|
routine.preserved = preserved
|
|
routines.append(routine)
|
|
self.current_routine_name = None
|
|
self.scanner.check_type('EOF')
|
|
|
|
program = Program(self.scanner.line_number, defns=defns, routines=routines)
|
|
programs = includes + [program]
|
|
program = merge_programs(programs)
|
|
|
|
self.resolve_symbols(program)
|
|
return program
|
|
|
|
def typedef(self):
|
|
self.scanner.expect('typedef')
|
|
type_ = self.defn_type()
|
|
name = self.defn_name()
|
|
if name in self.symtab.typedefs:
|
|
self.syntax_error('Type "%s" already declared' % name)
|
|
self.symtab.typedefs[name] = type_
|
|
return type_
|
|
|
|
def defn_const(self):
|
|
self.scanner.expect('const')
|
|
name = self.defn_name()
|
|
if name in self.symtab.consts:
|
|
self.syntax_error('Const "%s" already declared' % name)
|
|
loc = self.const()
|
|
self.symtab.consts[name] = loc
|
|
return loc
|
|
|
|
def defn(self):
|
|
type_ = self.defn_type()
|
|
name = self.defn_name()
|
|
|
|
initial = None
|
|
if self.scanner.consume(':'):
|
|
if isinstance(type_, TableType):
|
|
if self.scanner.on_type('string literal'):
|
|
initial = self.scanner.token
|
|
self.scanner.scan()
|
|
else:
|
|
initial = []
|
|
initial.append(self.const().value)
|
|
while self.scanner.consume(','):
|
|
initial.append(self.const().value)
|
|
else:
|
|
initial = self.const().value
|
|
|
|
addr = None
|
|
if self.scanner.consume('@'):
|
|
self.scanner.check_type('integer literal')
|
|
addr = int(self.scanner.token)
|
|
self.scanner.scan()
|
|
|
|
if initial is not None and addr is not None:
|
|
self.syntax_error("Definition cannot have both initial value and explicit address")
|
|
|
|
return type_, Defn(self.scanner.line_number, name=name, addr=addr, initial=initial)
|
|
|
|
def const(self):
|
|
if self.scanner.token in ('on', 'off'):
|
|
loc = ConstantRef(TYPE_BIT, 1 if self.scanner.token == 'on' else 0)
|
|
self.scanner.scan()
|
|
return loc
|
|
elif self.scanner.on_type('integer literal'):
|
|
value = int(self.scanner.token)
|
|
self.scanner.scan()
|
|
type_ = TYPE_WORD if value > 255 else TYPE_BYTE
|
|
loc = ConstantRef(type_, value)
|
|
return loc
|
|
elif self.scanner.consume('word'):
|
|
loc = ConstantRef(TYPE_WORD, int(self.scanner.token))
|
|
self.scanner.scan()
|
|
return loc
|
|
elif self.scanner.token in self.symtab.consts:
|
|
loc = self.symtab.consts[self.scanner.token]
|
|
self.scanner.scan()
|
|
return loc
|
|
else:
|
|
self.syntax_error('bad constant "%s"' % self.scanner.token)
|
|
|
|
def defn_size(self):
|
|
self.scanner.expect('[')
|
|
size = self.const().value
|
|
self.scanner.expect(']')
|
|
return size
|
|
|
|
def defn_type(self):
|
|
type_ = self.defn_type_term()
|
|
|
|
if self.scanner.consume('table'):
|
|
size = self.defn_size()
|
|
if size <= 0 or size > 65536:
|
|
self.syntax_error("Table size must be > 0 and <= 65536")
|
|
type_ = TableType(type_, size)
|
|
|
|
return type_
|
|
|
|
def defn_type_term(self):
|
|
type_ = None
|
|
|
|
if self.scanner.consume('('):
|
|
type_ = self.defn_type()
|
|
self.scanner.expect(')')
|
|
return type_
|
|
|
|
if self.scanner.consume('byte'):
|
|
type_ = TYPE_BYTE
|
|
elif self.scanner.consume('word'):
|
|
type_ = TYPE_WORD
|
|
elif self.scanner.consume('vector'):
|
|
type_ = self.defn_type_term()
|
|
if not isinstance(type_, RoutineType):
|
|
self.syntax_error("Vectors can only be of a routine, not %r" % type_)
|
|
type_ = VectorType(type_)
|
|
elif self.scanner.consume('routine'):
|
|
(inputs, outputs, trashes) = self.constraints()
|
|
type_ = RoutineType(frozenset(inputs), frozenset(outputs), frozenset(trashes))
|
|
elif self.scanner.consume('pointer'):
|
|
type_ = PointerType()
|
|
else:
|
|
type_name = self.scanner.token
|
|
self.scanner.scan()
|
|
if type_name not in self.symtab.typedefs:
|
|
self.syntax_error("Undefined type '%s'" % type_name)
|
|
type_ = self.symtab.typedefs[type_name]
|
|
|
|
return type_
|
|
|
|
def defn_name(self):
|
|
self.scanner.check_type('identifier')
|
|
name = self.scanner.token
|
|
self.scanner.scan()
|
|
return name
|
|
|
|
def constraints(self):
|
|
inputs = set()
|
|
outputs = set()
|
|
trashes = set()
|
|
if self.scanner.consume('inputs'):
|
|
inputs = set(self.labels())
|
|
if self.scanner.consume('outputs'):
|
|
outputs = set(self.labels())
|
|
if self.scanner.consume('trashes'):
|
|
trashes = set(self.labels())
|
|
return (
|
|
set([ForwardReference(n) for n in inputs]),
|
|
set([ForwardReference(n) for n in outputs]),
|
|
set([ForwardReference(n) for n in trashes])
|
|
)
|
|
|
|
def routine(self, name):
|
|
type_ = self.defn_type()
|
|
if not isinstance(type_, RoutineType):
|
|
self.syntax_error("Can only define a routine, not {}".format(repr(type_)))
|
|
locals_ = []
|
|
if self.scanner.consume('@'):
|
|
self.scanner.check_type('integer literal')
|
|
block = None
|
|
addr = int(self.scanner.token)
|
|
self.scanner.scan()
|
|
else:
|
|
locals_ = self.locals()
|
|
block = self.block()
|
|
addr = None
|
|
return type_, Routine(self.scanner.line_number, name=name, block=block, addr=addr, locals=locals_)
|
|
|
|
def labels(self):
|
|
accum = []
|
|
accum.append(self.label())
|
|
while self.scanner.consume(','):
|
|
accum.append(self.label())
|
|
return accum
|
|
|
|
def label(self):
|
|
"""Like a locexpr, but does not allow literal values, and the labels do not
|
|
need to be defined yet. They will be resolved at the end of parsing."""
|
|
loc = self.scanner.token
|
|
self.scanner.scan()
|
|
return loc
|
|
|
|
def locexprs(self):
|
|
accum = []
|
|
accum.append(self.locexpr())
|
|
while self.scanner.consume(','):
|
|
accum.append(self.locexpr())
|
|
return accum
|
|
|
|
def locexpr(self):
|
|
if self.scanner.token in ('on', 'off', 'word') or self.scanner.token in self.symtab.consts or self.scanner.on_type('integer literal'):
|
|
return self.const()
|
|
else:
|
|
name = self.scanner.token
|
|
self.scanner.scan()
|
|
return self.lookup(name, allow_forward=True, routine_name=self.current_routine_name)
|
|
|
|
def indlocexpr(self):
|
|
if self.scanner.consume('['):
|
|
loc = self.locexpr()
|
|
self.scanner.expect(']')
|
|
self.scanner.expect('+')
|
|
self.scanner.expect('y')
|
|
return IndirectRef(loc)
|
|
else:
|
|
return self.indexed_locexpr()
|
|
|
|
def indexed_locexpr(self):
|
|
loc = self.locexpr()
|
|
if not isinstance(loc, str):
|
|
index = None
|
|
offset = ConstantRef(TYPE_BYTE, 0)
|
|
if self.scanner.consume('+'):
|
|
if self.scanner.token in self.symtab.consts or self.scanner.on_type('integer literal'):
|
|
offset = self.const()
|
|
self.scanner.expect('+')
|
|
index = self.locexpr()
|
|
loc = IndexedRef(loc, offset, index)
|
|
return loc
|
|
|
|
def locals(self):
|
|
defns = []
|
|
while self.scanner.consume('static'):
|
|
type_, defn = self.defn()
|
|
if defn.initial is None:
|
|
self.syntax_error("Static definition {} must have initial value".format(defn))
|
|
self.declare_local(self.current_routine_name, defn.name, defn, type_)
|
|
defns.append(defn)
|
|
while self.scanner.consume('local'):
|
|
type_, defn = self.defn()
|
|
if defn.initial is not None:
|
|
self.syntax_error("Local definition {} may not have initial value".format(defn))
|
|
self.declare_local(self.current_routine_name, defn.name, defn, type_)
|
|
defns.append(defn)
|
|
return defns
|
|
|
|
def block(self):
|
|
instrs = []
|
|
self.scanner.expect('{')
|
|
while not self.scanner.on('}'):
|
|
instrs.append(self.instr())
|
|
if isinstance(instrs[-1], GoTo):
|
|
break
|
|
self.scanner.expect('}')
|
|
return Block(self.scanner.line_number, instrs=instrs)
|
|
|
|
def instr(self):
|
|
if self.scanner.consume('if'):
|
|
inverted = False
|
|
if self.scanner.consume('not'):
|
|
inverted = True
|
|
src = self.locexpr()
|
|
block1 = self.block()
|
|
block2 = None
|
|
if self.scanner.consume('else'):
|
|
block2 = self.block()
|
|
return If(self.scanner.line_number, src=src, block1=block1, block2=block2, inverted=inverted)
|
|
elif self.scanner.consume('repeat'):
|
|
inverted = False
|
|
src = None
|
|
block = self.block()
|
|
if self.scanner.consume('until'):
|
|
if self.scanner.consume('not'):
|
|
inverted = True
|
|
src = self.locexpr()
|
|
else:
|
|
self.scanner.expect('forever')
|
|
return Repeat(self.scanner.line_number, src=src, block=block, inverted=inverted)
|
|
elif self.scanner.consume('for'):
|
|
dest = self.locexpr()
|
|
if self.scanner.consume('down'):
|
|
direction = -1
|
|
elif self.scanner.consume('up'):
|
|
direction = 1
|
|
else:
|
|
self.syntax_error('expected "up" or "down", found "%s"' % self.scanner.token)
|
|
self.scanner.expect('to')
|
|
final = self.const()
|
|
block = self.block()
|
|
return For(self.scanner.line_number, dest=dest, direction=direction, final=final, block=block)
|
|
elif self.scanner.consume('reset'):
|
|
pointer = self.locexpr()
|
|
offset = self.const()
|
|
return Reset(self.scanner.line_number, pointer=pointer, offset=offset)
|
|
elif self.scanner.token in ("ld",):
|
|
# the same as add, sub, cmp etc below, except supports an indlocexpr for the src
|
|
opcode = self.scanner.token
|
|
self.scanner.scan()
|
|
dest = self.locexpr()
|
|
self.scanner.expect(',')
|
|
src = self.indlocexpr()
|
|
return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src)
|
|
elif self.scanner.token in ("add", "sub", "cmp", "and", "or", "xor"):
|
|
opcode = self.scanner.token
|
|
self.scanner.scan()
|
|
dest = self.locexpr()
|
|
self.scanner.expect(',')
|
|
src = self.indexed_locexpr()
|
|
return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src)
|
|
elif self.scanner.token in ("st",):
|
|
opcode = self.scanner.token
|
|
self.scanner.scan()
|
|
src = self.locexpr()
|
|
self.scanner.expect(',')
|
|
dest = self.indlocexpr()
|
|
return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src)
|
|
elif self.scanner.token in ("shl", "shr", "inc", "dec"):
|
|
opcode = self.scanner.token
|
|
self.scanner.scan()
|
|
dest = self.indexed_locexpr()
|
|
return SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=None)
|
|
elif self.scanner.token in ("nop",):
|
|
opcode = self.scanner.token
|
|
self.scanner.scan()
|
|
return SingleOp(self.scanner.line_number, opcode=opcode, dest=None, src=None)
|
|
elif self.scanner.consume("call"):
|
|
name = self.scanner.token
|
|
self.scanner.scan()
|
|
instr = Call(self.scanner.line_number, location=ForwardReference(name))
|
|
return instr
|
|
elif self.scanner.consume("goto"):
|
|
name = self.scanner.token
|
|
self.scanner.scan()
|
|
instr = GoTo(self.scanner.line_number, location=ForwardReference(name))
|
|
return instr
|
|
elif self.scanner.token in ("copy",):
|
|
opcode = self.scanner.token
|
|
self.scanner.scan()
|
|
src = self.indlocexpr()
|
|
self.scanner.expect(',')
|
|
dest = self.indlocexpr()
|
|
instr = SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src)
|
|
return instr
|
|
elif self.scanner.consume("with"):
|
|
self.scanner.expect("interrupts")
|
|
self.scanner.expect("off")
|
|
block = self.block()
|
|
return WithInterruptsOff(self.scanner.line_number, block=block)
|
|
elif self.scanner.consume("save"):
|
|
locations = self.locexprs()
|
|
block = self.block()
|
|
return Save(self.scanner.line_number, locations=locations, block=block)
|
|
elif self.scanner.consume("point"):
|
|
pointer = self.locexpr()
|
|
self.scanner.expect("into")
|
|
table = self.locexpr()
|
|
block = self.block()
|
|
return PointInto(self.scanner.line_number, pointer=pointer, table=table, block=block)
|
|
elif self.scanner.consume("trash"):
|
|
dest = self.locexpr()
|
|
return SingleOp(self.scanner.line_number, opcode='trash', src=None, dest=dest)
|
|
else:
|
|
self.syntax_error('bad opcode "%s"' % self.scanner.token)
|
|
|
|
|
|
# - - - -
|
|
|
|
|
|
def load_program(filename, symtab, include_path, include_file=False):
|
|
import os
|
|
if include_file:
|
|
for include_dir in include_path:
|
|
if os.path.exists(os.path.join(include_dir, filename)):
|
|
filename = os.path.join(include_dir, filename)
|
|
break
|
|
text = open(filename).read()
|
|
parser = Parser(symtab, text, filename, include_path)
|
|
program = parser.program()
|
|
return program
|
|
|
|
|
|
def merge_programs(programs):
|
|
"""Assumes that the programs do not have any conflicts."""
|
|
|
|
full = Program(1, defns=[], routines=[])
|
|
for p in programs:
|
|
full.defns.extend(p.defns)
|
|
full.routines.extend(p.routines)
|
|
|
|
return full
|