diff --git a/HISTORY.md b/HISTORY.md index 7d7b22f..e77b673 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,6 +5,11 @@ History of SixtyPical ---- * `save X, Y, Z { }` now allowed as a shortcut for nested `save`s. +* If the name in a location expression isn't found in the symbol + table, a forward reference will _always_ be generated; and forward + references in _all_ operations will be resolved after parsing. +* As a consequence, trying to call or goto a non-routine-typed symbol + is now an analysis error, not a syntax error. * Split TODO off into own file. * `sixtypical` no longer writes the compiled binary to standard output. The `--output` command-line argument should be given diff --git a/TODO.md b/TODO.md index ebdff52..908f893 100644 --- a/TODO.md +++ b/TODO.md @@ -1,20 +1,16 @@ TODO for SixtyPical =================== -### `low` and `high` address operators +### 16-bit `cmp` -To turn `word` type into `byte`. +This is because we don't actually want `low` and `high` address operators +that turn `word` type into `byte`. -Trying to remember if we have a compelling case for this or now. The best I can think -of is for implementing 16-bit `cmp` in an efficient way. Maybe we should see if we -can get by with 16-bit `cmp` instead though. +This is because this immediately makes things harder (that is, effectively +impossible) to analyze. -The problem is that once a byte is extracted, putting it back into a word is awkward. -The address operators have to modify a destination in a special way. That is, when -you say `st a, >word`, you are updating `word` to be `word & $ff | a << 8`, somelike. -Is that consistent with `st`? Well, probably it is, but we have to explain it. -It might make more sense, then, for it to be "part of the operation" instead of "part of -the reference"; something like `st.hi x, word`; `st.lo y, word`. Dunno. +16-bit `cmp` also benefits from some special differences between `cmp` +and `sub` on 6502, so it would be nice to capture them. ### Save values to other-than-the-stack @@ -27,17 +23,12 @@ Allow Which uses some other storage location instead of the stack. A local static would be a good candidate for such. -### Make all symbols forward-referencable - -Basically, don't do symbol-table lookups when parsing, but do have a more formal -"symbol resolution" (linking) phase right after parsing. - ### Associate each pointer with the buffer it points into Check that the buffer being read or written to through pointer, appears in appropriate inputs or outputs set. -In the analysis, when we obtain a pointer, we need to record, in contect, what buffer +In the analysis, when we obtain a pointer, we need to record, in context, what buffer that pointer came from. When we write through that pointer, we need to set that buffer as written. @@ -78,8 +69,8 @@ error. More generally, define a block as having zero or one `goto`s at the end. (and `goto`s cannot appear elsewhere.) -If a block ends in a `call` can that be converted to end in a `goto`? Why not? I think it can. -The constraints should iron out the same both ways. +If a block ends in a `call` can that be converted to end in a `goto`? Why not? I think it can, +if the block is in tail position. The constraints should iron out the same both ways. And - once we have this - why do we need `goto` to be in tail position, strictly? As long as the routine has consistent type context every place it exits, that should be fine. @@ -91,3 +82,8 @@ Search a searchlist of include paths. And use them to make libraries of routine One such library routine might be an `interrupt routine` type for various architectures. Since "the supervisor" has stored values on the stack, we should be able to trash them with impunity, in such a routine. + +### Line numbers in analysis error messages + +For analysis errors, there is a line number, but it's the line of the routine +after the routine in which the analysis error occurred. Fix this. diff --git a/eg/c64/demo-game/demo-game.60p b/eg/c64/demo-game/demo-game.60p index fd72065..a521e30 100644 --- a/eg/c64/demo-game/demo-game.60p +++ b/eg/c64/demo-game/demo-game.60p @@ -33,9 +33,11 @@ typedef routine inputs joy2, press_fire_msg, dispatch_game_state, actor_pos, actor_delta, actor_logic, + player_died, screen, screen1, screen2, screen3, screen4, colormap1, colormap2, colormap3, colormap4 outputs dispatch_game_state, actor_pos, actor_delta, actor_logic, + player_died, screen, screen1, screen2, screen3, screen4, colormap1, colormap2, colormap3, colormap4 trashes a, x, y, c, z, n, v, pos, new_pos, delta, ptr, dispatch_logic game_state_routine @@ -45,13 +47,13 @@ typedef routine // // Routines that conform to this type also follow this convention: // -// Set carry if the player perished. Carry clear otherwise. +// Set player_died to 1 if the player perished. Unchanged otherwise. // typedef routine - inputs pos, delta, joy2, screen - outputs pos, delta, new_pos, screen, c - trashes a, x, y, z, n, v, ptr + inputs pos, delta, joy2, screen, player_died + outputs pos, delta, new_pos, screen, player_died + trashes a, x, y, z, n, v, c, ptr logic_routine // ---------------------------------------------------------------- @@ -87,6 +89,8 @@ word new_pos word table[256] actor_delta word delta +byte player_died + vector logic_routine table[256] actor_logic vector logic_routine dispatch_logic @@ -241,7 +245,7 @@ define check_new_position_in_bounds routine routine init_game inputs actor_pos, actor_delta, actor_logic - outputs actor_pos, actor_delta, actor_logic + outputs actor_pos, actor_delta, actor_logic, player_died trashes pos, a, y, z, n, c, v { ld y, 0 @@ -259,9 +263,11 @@ routine init_game } until z ld y, 0 - copy word 0, actor_pos + y + copy word 40, actor_pos + y copy word 0, actor_delta + y copy player_logic, actor_logic + y + + st y, player_died } // ---------------------------------------------------------------- @@ -301,10 +307,9 @@ define player_logic logic_routine st off, c add ptr, pos copy 81, [ptr] + y - - st off, c } else { - st on, c + ld a, 1 + st a, player_died } // FIXME these trashes, strictly speaking, probably shouldn't be needed, @@ -314,8 +319,6 @@ define player_logic logic_routine trash ptr trash y trash v - } else { - st off, c } } @@ -351,10 +354,6 @@ define enemy_logic logic_routine st off, c add ptr, pos copy 82, [ptr] + y - - st off, c - } else { - st on, c } // FIXME these trashes, strictly speaking, probably shouldn't be needed, @@ -373,8 +372,6 @@ define enemy_logic logic_routine copy $ffd8, delta } } - - st off, c } // ---------------------------------------------------------------- @@ -408,6 +405,7 @@ define game_state_title_screen game_state_routine define game_state_play game_state_routine { ld x, 0 + st x, player_died for x up to 15 { copy actor_pos + x, pos copy actor_delta + x, delta @@ -420,18 +418,19 @@ define game_state_play game_state_routine save x { copy actor_logic + x, dispatch_logic call dispatch_logic - - if c { - // Player died! Want no dead! - call clear_screen - copy game_state_game_over, dispatch_game_state - } } copy pos, actor_pos + x copy delta, actor_delta + x } + ld a, player_died + if not z { + // Player died! Want no dead! + call clear_screen + copy game_state_game_over, dispatch_game_state + } + goto save_cinv } diff --git a/src/sixtypical/analyzer.py b/src/sixtypical/analyzer.py index 1e69965..98959e1 100644 --- a/src/sixtypical/analyzer.py +++ b/src/sixtypical/analyzer.py @@ -550,6 +550,8 @@ class Analyzer(object): context.invalidate_range(dest) elif opcode == 'call': type = instr.location.type + if not isinstance(type, (RoutineType, VectorType)): + raise TypeMismatchError(instr, instr.location) if isinstance(type, VectorType): type = type.of_type for ref in type.inputs: diff --git a/src/sixtypical/ast.py b/src/sixtypical/ast.py index 718636f..f7aad36 100644 --- a/src/sixtypical/ast.py +++ b/src/sixtypical/ast.py @@ -37,14 +37,16 @@ class AST(object): def all_children(self): for attr in self.children_attrs: for child in self.attrs[attr]: + if child is not None: + yield child + for subchild in child.all_children(): + yield subchild + for attr in self.child_attrs: + child = self.attrs[attr] + if child is not None: yield child for subchild in child.all_children(): yield subchild - for attr in self.child_attrs: - child = self.attrs[attr] - yield child - for subchild in child.all_children(): - yield subchild class Program(AST): diff --git a/src/sixtypical/model.py b/src/sixtypical/model.py index e2d7757..7204dde 100644 --- a/src/sixtypical/model.py +++ b/src/sixtypical/model.py @@ -18,20 +18,6 @@ class Type(object): def __hash__(self): return hash(self.name) - def backpatch_constraint_labels(self, resolver): - def resolve(w): - if not isinstance(w, str): - return w - return resolver(w) - if isinstance(self, TableType): - self.of_type.backpatch_constraint_labels(resolver) - elif isinstance(self, VectorType): - self.of_type.backpatch_constraint_labels(resolver) - elif isinstance(self, RoutineType): - self.inputs = set([resolve(w) for w in self.inputs]) - self.outputs = set([resolve(w) for w in self.outputs]) - self.trashes = set([resolve(w) for w in self.trashes]) - TYPE_BIT = Type('bit', max_range=(0, 1)) TYPE_BYTE = Type('byte', max_range=(0, 255)) @@ -41,11 +27,11 @@ TYPE_WORD = Type('word', max_range=(0, 65535)) class RoutineType(Type): """This memory location contains the code for a routine.""" - def __init__(self, inputs=None, outputs=None, trashes=None): + def __init__(self, inputs, outputs, trashes): self.name = 'routine' - self.inputs = inputs or set() - self.outputs = outputs or set() - self.trashes = trashes or set() + self.inputs = inputs + self.outputs = outputs + self.trashes = trashes def __repr__(self): return '%s(%r, inputs=%r, outputs=%r, trashes=%r)' % ( diff --git a/src/sixtypical/parser.py b/src/sixtypical/parser.py index bd68faa..07f6f1c 100644 --- a/src/sixtypical/parser.py +++ b/src/sixtypical/parser.py @@ -18,6 +18,14 @@ class SymEntry(object): return "%s(%r, %r)" % (self.__class__.__name__, self.ast_node, self.model) +class ForwardReference(object): + def __init__(self, name): + self.name = name + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self.name) + + class ParsingContext(object): def __init__(self): self.symbols = {} # token -> SymEntry @@ -45,7 +53,6 @@ class Parser(object): def __init__(self, context, text, filename): self.context = context self.scanner = Scanner(text, filename) - self.backpatch_instrs = [] def syntax_error(self, msg): self.scanner.syntax_error(msg) @@ -67,6 +74,44 @@ class Parser(object): def clear_statics(self): self.context.statics = {} + # ---- symbol resolution + + def resolve_symbols(self, program): + # This could stand to be better unified. + + def backpatch_constraint_labels(type_): + def resolve(w): + if not isinstance(w, ForwardReference): + return w + return self.lookup(w.name) + if isinstance(type_, TableType): + backpatch_constraint_labels(type_.of_type) + elif isinstance(type_, VectorType): + backpatch_constraint_labels(type_.of_type) + elif isinstance(type_, RoutineType): + type_.inputs = set([resolve(w) for w in type_.inputs]) + type_.outputs = set([resolve(w) for w in type_.outputs]) + type_.trashes = set([resolve(w) for w in type_.trashes]) + + for defn in program.defns: + backpatch_constraint_labels(defn.location.type) + for routine in program.routines: + backpatch_constraint_labels(routine.location.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, 'location') + resolve_fwd_reference(node, 'src') + resolve_fwd_reference(node, 'dest') + # --- grammar productions def program(self): @@ -91,28 +136,9 @@ class Parser(object): routines.append(routine) self.scanner.check_type('EOF') - # now backpatch the executable types. - #for type_name, type_ in self.context.typedefs.items(): - # type_.backpatch_constraint_labels(lambda w: self.lookup(w)) - for defn in defns: - defn.location.type.backpatch_constraint_labels(lambda w: self.lookup(w)) - for routine in routines: - routine.location.type.backpatch_constraint_labels(lambda w: self.lookup(w)) - for instr in self.backpatch_instrs: - if instr.opcode in ('call', 'goto'): - name = instr.location - model = self.lookup(name) - if not isinstance(model.type, (RoutineType, VectorType)): - self.syntax_error('Illegal call of non-executable "%s"' % name) - instr.location = model - if instr.opcode in ('copy',) and isinstance(instr.src, str): - name = instr.src - model = self.lookup(name) - if not isinstance(model.type, (RoutineType, VectorType)): - self.syntax_error('Illegal copy of non-executable "%s"' % name) - instr.src = model - - return Program(self.scanner.line_number, defns=defns, routines=routines) + program = Program(self.scanner.line_number, defns=defns, routines=routines) + self.resolve_symbols(program) + return program def typedef(self): self.scanner.expect('typedef') @@ -252,7 +278,11 @@ class Parser(object): outputs = set(self.labels()) if self.scanner.consume('trashes'): trashes = set(self.labels()) - return (inputs, outputs, trashes) + 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() @@ -302,23 +332,19 @@ class Parser(object): accum.append(self.locexpr()) return accum - def locexpr(self, forward=False): + def locexpr(self): if self.scanner.token in ('on', 'off', 'word') or self.scanner.token in self.context.consts or self.scanner.on_type('integer literal'): return self.const() - elif forward: + else: name = self.scanner.token self.scanner.scan() loc = self.context.fetch(name) - if loc is not None: + if loc: return loc else: - return name - else: - loc = self.lookup(self.scanner.token) - self.scanner.scan() - return loc + return ForwardReference(name) - def indlocexpr(self, forward=False): + def indlocexpr(self): if self.scanner.consume('['): loc = self.locexpr() self.scanner.expect(']') @@ -329,10 +355,10 @@ class Parser(object): loc = self.locexpr() return AddressRef(loc) else: - return self.indexed_locexpr(forward=forward) + return self.indexed_locexpr() - def indexed_locexpr(self, forward=False): - loc = self.locexpr(forward=forward) + def indexed_locexpr(self): + loc = self.locexpr() if not isinstance(loc, str): index = None if self.scanner.consume('+'): @@ -427,17 +453,15 @@ class Parser(object): self.scanner.scan() name = self.scanner.token self.scanner.scan() - instr = SingleOp(self.scanner.line_number, opcode=opcode, location=name, dest=None, src=None) - self.backpatch_instrs.append(instr) + instr = SingleOp(self.scanner.line_number, opcode=opcode, location=ForwardReference(name), dest=None, src=None) return instr elif self.scanner.token in ("copy",): opcode = self.scanner.token self.scanner.scan() - src = self.indlocexpr(forward=True) + src = self.indlocexpr() self.scanner.expect(',') dest = self.indlocexpr() instr = SingleOp(self.scanner.line_number, opcode=opcode, dest=dest, src=src) - self.backpatch_instrs.append(instr) return instr elif self.scanner.consume("with"): self.scanner.expect("interrupts") diff --git a/tests/SixtyPical Analysis.md b/tests/SixtyPical Analysis.md index 0caecc8..2e06530 100644 --- a/tests/SixtyPical Analysis.md +++ b/tests/SixtyPical Analysis.md @@ -196,6 +196,35 @@ If a routine reads or writes a user-define memory location, it needs to declare | } = ok +### call ### + +You can't call a non-routine. + + | byte up + | + | routine main outputs x, y trashes z, n { + | ld x, 0 + | ld y, 1 + | call up + | } + ? TypeMismatchError: up + + | routine main outputs x, y trashes z, n { + | ld x, 0 + | ld y, 1 + | call x + | } + ? TypeMismatchError: x + +Nor can you goto a non-routine. + + | byte foo + | + | routine main { + | goto foo + | } + ? TypeMismatchError: foo + ### ld ### Can't `ld` from a memory location that isn't initialized. diff --git a/tests/SixtyPical Syntax.md b/tests/SixtyPical Syntax.md index f8e71f8..3409e63 100644 --- a/tests/SixtyPical Syntax.md +++ b/tests/SixtyPical Syntax.md @@ -404,24 +404,6 @@ Can't call routine that hasn't been defined. | } ? SyntaxError -And you can't call a non-routine. - - | byte up - | - | define main routine { - | ld x, 0 - | ld y, 1 - | call up - | } - ? SyntaxError - - | define main routine { - | ld x, 0 - | ld y, 1 - | call x - | } - ? SyntaxError - But you can call a routine that is yet to be defined, further on. | define main routine { @@ -589,13 +571,6 @@ goto. | } ? SyntaxError - | byte foo - | - | define main routine { - | goto foo - | } - ? SyntaxError - Buffers and pointers. | buffer[2048] buf