1
0
mirror of https://github.com/catseye/SixtyPical.git synced 2024-11-25 07:32:16 +00:00

Merge pull request #21 from catseye/develop-0.20

Develop 0.20
This commit is contained in:
Chris Pressey 2019-05-14 16:25:27 +01:00 committed by GitHub
commit 8467cd947e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 4835 additions and 4002 deletions

View File

@ -1,6 +1,30 @@
History of SixtyPical
=====================
0.20
----
* A `point ... into` block no longer initializes the pointer
by default. A subequent `reset` instruction must be used
to initialize the pointer. The pointer may be reset to any
valid offset within the table (not only 0) and it may be
reset multiple times inside the block.
* Local locations need no longer be static. If they are not
static, they are considered uninitialized until assigned,
and they can be declared with an explicit fixed address.
* Along with `goto`, `call` and `with interrupts off` are
now forbidden inside a `with interrupts off` block.
* More tests to assure that using `call` inside a `point into`
block or inside a `for` block does not cause trouble,
particularly when the routine being called also uses the
variable named in that block.
* Fixed a bug where two local statics could be declared with
the same name.
* Split analysis context support off from analyzer, and
symbol table support from parse, and it their own modules.
* Split the SixtyPical Analysis tests across three files,
and placed test appliances for `sixtypical` in own file.
0.19
----

View File

@ -7,7 +7,7 @@ covered by the following BSD-compatible license, modelled after the
-----------------------------------------------------------------------------
Copyright (c)2014-2018 Chris Pressey, Cat's Eye Technologies.
Copyright (c)2014-2019 Chris Pressey, Cat's Eye Technologies.
The authors intend this Report to belong to the entire SixtyPical
community, and so we grant permission to copy and distribute it for
@ -24,7 +24,7 @@ The source code for the reference implementation and supporting tools (in the
-----------------------------------------------------------------------------
Copyright (c)2014-2018, Chris Pressey, Cat's Eye Technologies.
Copyright (c)2014-2019, Chris Pressey, Cat's Eye Technologies.
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@ -1,7 +1,7 @@
SixtyPical
==========
_Version 0.19. Work-in-progress, everything is subject to change._
_Version 0.20. Work-in-progress, everything is subject to change._
**SixtyPical** is a [low-level](#low-level) programming language
supporting a sophisticated [static analysis](#static-analysis).
@ -109,7 +109,9 @@ In order to run the tests for compilation, [dcc6502][] needs to be installed.
* [SixtyPical specification](doc/SixtyPical.md)
* [Literate test suite for SixtyPical syntax](tests/SixtyPical%20Syntax.md)
* [Literate test suite for SixtyPical analysis](tests/SixtyPical%20Analysis.md)
* [Literate test suite for SixtyPical analysis (operations)](tests/SixtyPical%20Analysis.md)
* [Literate test suite for SixtyPical analysis (storage)](tests/SixtyPical%20Storage.md)
* [Literate test suite for SixtyPical analysis (control flow)](tests/SixtyPical%20Control%20Flow.md)
* [Literate test suite for SixtyPical compilation](tests/SixtyPical%20Compilation.md)
* [Literate test suite for SixtyPical fallthru optimization](tests/SixtyPical%20Fallthru.md)

180
TODO.md
View File

@ -1,6 +1,9 @@
TODO for SixtyPical
===================
Language
--------
### Save values to other-than-the-stack
Allow
@ -9,35 +12,111 @@ Allow
...
}
Which uses some other storage location instead of the stack. A local static
would be a good candidate for such.
Which uses some other storage location instead of the stack. A local non-static
would be a good candidate for such. At any rate, the location must not
be writeable by anything that is called from within the block. So, probably
just restrict this to local non-statics.
### Analyze `call` within blocks?
### Copy byte to/from table
What happens if you call another routine from inside a `with interrupts off` block?
Do we want a `copy bytevar, table + x` instruction? We don't currently have one.
You have to `ld a`, `st a`. I think maybe we should have one.
What happens if you call another routine from inside a `save` block?
### Character literals
What happens if you call another routine from inside a `point into` block?
For goodness sake, let the programmer say `'A'` instead of `65`.
What happens if you call another routine from inside a `for` block?
### Character set mapping
Remember that any of these may have a `goto` ... and they may have a second
instance of the same block (e.g. `with interrupts off` nested within
`with interrupts off` shouldn't be allowed to turn them back on after the
inner block has finished -- even if there is no `call`.)
Not all computers think `'A'` should be `65`. Allow the character set to be
mapped. Probably copy what Ophis does.
These holes need to be plugged.
### "Include" directives
### Reset pointer in `point into` blocks
Search a searchlist of include paths. And use them to make libraries of routines.
We have `point into` blocks, but maybe the action when entering such a
block shouldn't always be to set the given pointer to the start of the given table.
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.
That is, sometimes we would like to start at some fixed offset. And
sometimes we want to (re)set the pointer, without closing and starting a new block.
### Pointers into non-byte tables
### Pointers associated globally with a table
Right now you cannot get a pointer into a non-byte (for instance, word or vector) table.
Word and vector tables are stored as two byte tables in memory. This is useful for
indexed access, but makes pointer access more difficult.
Laying them out for pointer access would make indexed access more difficult.
### Saving non-byte values
Right now you cannot save a word value.
There doesn't seem to be a hugely pressing reason why not.
Analysis
--------
### Forbid recursion
What happens if a routine calls itself, directly or indirectly? Many
constraints might be violated in this case. We should probably disallow
recursion by default. (Which means assembling the callgraph in all cases.)
### Analyze memory usage
If you define two variables that occupy the same address, an analysis error ought
to be raised. (But there should also be a way to annotate this as intentional.
Intentionally making two tables overlap could be valuable. However, the analysis
will probably completely miss this fact.)
Optimization
------------
### Space optimization of local non-statics
If there are two routines A and B, and A never calls B (even indirectly), and
B never calls A (even indirectly), then their non-static locals can
be allocated at the same space.
This is not just an impressive trick -- in the presence of local pointers, which
use up a word in zero-page, which we consider a precious resource, it allow those
zero-page locations to be re-used.
### Tail-call optimization
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.
As long as the routine has consistent type context every place it exits, that should be fine.
### Branch optimization in `if`
Currently the `if` generator is not smart enough to avoid generating silly
jump instructions. (See the Fallthru tests.) Improve it.
### Dead code removal
Once we have a call graph we can omit routines that we're sure aren't called.
This would let us use include-files and standard-libraries nicely: any
routines they define, but that you don't use, don't get included.
Analyzing the set of possible routines that a vector can take on would help
this immensely.
Implementation
--------------
### 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.
Blue-skying
-----------
### Pointers associated globally with a table(?)
We have `point into` blocks, but we would also like to sometimes pass a pointer
around to different routines, and have them all "know" what table it operates on.
@ -53,66 +132,5 @@ at different times.
These can co-exist with general, non-specific-table-linked `pointer` variables.
### Local non-statics
Somewhat related to the above, it should be possible to declare a local storage
location which is not static.
In this case, it would be considered uninitialized each time the routine was
entered.
So, you do not have a guarantee that it has a valid value. But you are guaranteed
that no other routine can read or modify it.
It also enables a trick: if there are two routines A and B, and A never calls B
(even indirectly), and B never calls A (even indirectly), then their locals can
be allocated at the same space.
A local could also be given an explicit address. In this case, two locals in
different routines could be given the same address, and as long as the condition
in the above paragraph holds, that's okay. (If it doesn't, the analyzer should
detect it.)
This would permit local pointers, which would be one way of addressing the
"same pointer to different tables" problem.
### Copy byte to/from table
Do we want a `copy bytevar, table + x` instruction? We don't currently have one.
You have to `ld a`, `st a`. I think maybe we should have one.
### Analyze memory usage
If you define two variables that occupy the same address, an analysis error ought
to be raised. (But there should also be a way to annotate this as intentional.
Intentionally making two tables overlap could be valuable. However, the analysis
will probably completely miss this fact.)
### Character literals
For goodness sake, let the programmer say `'A'` instead of `65`.
### Character set mapping
Not all computers think `'A'` should be `65`. Allow the character set to be
mapped. Probably copy what Ophis does.
### Tail-call optimization
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.
As long as the routine has consistent type context every place it exits, that should be fine.
### "Include" directives
Search a searchlist of include paths. And use them to make libraries of routines.
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.
If we have local pointers and space optimization for local non-statics, though,
these don't add as much.

View File

@ -16,7 +16,8 @@ import sys
from tempfile import NamedTemporaryFile
import traceback
from sixtypical.parser import Parser, SymbolTable, merge_programs
from sixtypical.symtab import SymbolTable
from sixtypical.parser import Parser, merge_programs
from sixtypical.analyzer import Analyzer
from sixtypical.outputter import outputter_class_for
from sixtypical.compiler import Compiler
@ -184,7 +185,7 @@ if __name__ == '__main__':
argparser.add_argument(
"--version",
action="version",
version="%(prog)s 0.19"
version="%(prog)s 0.20"
)
options, unknown = argparser.parse_known_args(sys.argv[1:])

View File

@ -1,7 +1,7 @@
SixtyPical
==========
This document describes the SixtyPical programming language version 0.19,
This document describes the SixtyPical programming language version 0.20,
both its static semantics (the capabilities and limits of the static
analyses it defines) and its runtime semantics (with reference to the
semantics of 6502 machine code.)
@ -196,7 +196,8 @@ table pointed to is implemented with "indirect indexed" addressing, as in
There are extended instruction modes for using these types of memory location.
See `copy` below, but here is some illustrative example code:
point ptr into buf { // this is the only way to initialize a pointer
point ptr into buf { // this associates this pointer with this table
reset ptr 0 // this is the only way to initialize a pointer
add ptr, 4 // note, this is unchecked against table's size!
ld y, 0 // you must set this to something yourself
copy [ptr] + y, byt // read memory through pointer, into byte
@ -658,4 +659,6 @@ Grammar
| "repeat" Block ("until" ["not"] LocExpr | "forever")
| "for" LocExpr ("up"|"down") "to" Const Block
| "with" "interrupts" LitBit Block
| "point" LocExpr "into" LocExpr Block
| "reset" LocExpr Const
.

View File

@ -265,37 +265,37 @@ define player_logic logic_routine
if c {
point ptr into screen {
reset ptr 0
st off, c
add ptr, new_pos
ld y, 0
// check collision.
ld a, [ptr] + y
}
// if "collision" is with your own self, treat it as if it's blank space!
cmp a, 81
if z {
ld a, 32
}
cmp a, 32
if z {
point ptr into screen {
// if "collision" is with your own self, treat it as if it's blank space!
cmp a, 81
if z {
ld a, 32
}
cmp a, 32
if z {
reset ptr 0
st off, c
add ptr, pos
copy 32, [ptr] + y
}
copy new_pos, pos
copy new_pos, pos
point ptr into screen {
reset ptr 0
st off, c
add ptr, pos
copy 81, [ptr] + y
}
} else {
ld a, 1
st a, player_died
}
} else {
ld a, 1
st a, player_died
}
}
@ -307,28 +307,29 @@ define enemy_logic logic_routine
if c {
point ptr into screen {
reset ptr 0
st off, c
add ptr, new_pos
ld y, 0
// check collision.
ld a, [ptr] + y
}
// if "collision" is with your own self, treat it as if it's blank space!
cmp a, 82
if z {
ld a, 32
}
cmp a, 32
if z {
point ptr into screen {
// if "collision" is with your own self, treat it as if it's blank space!
cmp a, 82
if z {
ld a, 32
}
cmp a, 32
if z {
reset ptr 0
st off, c
add ptr, pos
copy 32, [ptr] + y
}
copy new_pos, pos
copy new_pos, pos
point ptr into screen {
reset ptr 0
st off, c
add ptr, pos
copy 82, [ptr] + y

View File

@ -11,10 +11,12 @@ define main routine
trashes a, z, n, c, ptr
{
ld y, 0
copy ^buf, ptr
copy 123, [ptr] + y
copy [ptr] + y, foo
copy foo, [ptr] + y
point ptr into buf {
reset ptr 0
copy 123, [ptr] + y
copy [ptr] + y, foo
copy foo, [ptr] + y
}
// TODO: support saying `cmp foo, 123`, maybe
ld a, foo

View File

@ -0,0 +1,33 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print H (being ASCII 72 = 8 * 9)
// Increase y by 7, circuitously
//
define foo routine
inputs y
outputs y, n, z
trashes a, c
{
save x {
ld x, 0
for x up to 6 {
inc y
}
}
}
// Each iteration increases y by 8; there are 9 iterations
//
define main routine
outputs x, y, n, z
trashes a, c
{
ld x, 0
ld y, 0
for x up to 8 {
inc y
call foo
}
ld a, y
call chrout
}

View File

@ -1,8 +1,9 @@
# encoding: UTF-8
from sixtypical.ast import (
Program, Routine, Block, SingleOp, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
Program, Routine, Block, SingleOp, Reset, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
)
from sixtypical.context import AnalysisContext
from sixtypical.model import (
TYPE_BYTE, TYPE_WORD,
TableType, PointerType, VectorType, RoutineType,
@ -80,316 +81,6 @@ class IncompatibleConstraintsError(ConstraintsError):
pass
class AnalysisContext(object):
"""
A location is touched if it was changed (or even potentially
changed) during this routine, or some routine called by this routine.
A location is meaningful if it was an input to this routine,
or if it was set to a meaningful value by some operation in this
routine (or some routine called by this routine).
If a location is meaningful, it has a range. This range represents
the lowest and highest values that it might possibly be (i.e. we know
it cannot possibly be below the lowest or above the highest.) In the
absence of any usage information, the range of a byte, is 0..255 and
the range of a word is 0..65535.
A location is writeable if it was listed in the outputs and trashes
lists of this routine. A location can also be temporarily marked
unwriteable in certain contexts, such as `for` loops.
"""
def __init__(self, symtab, routine, inputs, outputs, trashes):
self.symtab = symtab
self.routine = routine # Routine (AST node)
self._touched = set() # {LocationRef}
self._range = dict() # LocationRef -> (Int, Int)
self._writeable = set() # {LocationRef}
self._terminated = False
self._gotos_encountered = set()
self._pointer_assoc = dict()
for ref in inputs:
if self.is_constant(ref):
raise ConstantConstraintError(self.routine, ref.name)
self._range[ref] = self.max_range(ref)
output_names = set()
for ref in outputs:
if self.is_constant(ref):
raise ConstantConstraintError(self.routine, ref.name)
output_names.add(ref.name)
self._writeable.add(ref)
for ref in trashes:
if self.is_constant(ref):
raise ConstantConstraintError(self.routine, ref.name)
if ref.name in output_names:
raise InconsistentConstraintsError(self.routine, ref.name)
self._writeable.add(ref)
def __str__(self):
return "{}(\n _touched={},\n _range={},\n _writeable={}\n)".format(
self.__class__.__name__,
LocationRef.format_set(self._touched), LocationRef.format_set(self._range), LocationRef.format_set(self._writeable)
)
def to_json_data(self):
type_ = self.symtab.fetch_global_type(self.routine.name)
return {
'routine_inputs': ','.join(sorted(loc.name for loc in type_.inputs)),
'routine_outputs': ','.join(sorted(loc.name for loc in type_.outputs)),
'routine_trashes': ','.join(sorted(loc.name for loc in type_.trashes)),
'touched': ','.join(sorted(loc.name for loc in self._touched)),
'range': dict((loc.name, '{}-{}'.format(rng[0], rng[1])) for (loc, rng) in self._range.items()),
'writeable': ','.join(sorted(loc.name for loc in self._writeable)),
'terminated': self._terminated,
'gotos_encountered': ','.join(sorted(loc.name for loc in self._gotos_encountered)),
}
def clone(self):
c = AnalysisContext(self.symtab, self.routine, [], [], [])
c._touched = set(self._touched)
c._range = dict(self._range)
c._writeable = set(self._writeable)
c._pointer_assoc = dict(self._pointer_assoc)
c._gotos_encountered = set(self._gotos_encountered)
return c
def update_from(self, other):
"""Replaces the information in this context, with the information from the other context.
This is an overwriting action - it does not attempt to merge the contexts.
We do not replace the gotos_encountered for technical reasons. (In `analyze_if`,
we merge those sets afterwards; at the end of `analyze_routine`, they are not distinct in the
set of contexts we are updating from, and we want to retain our own.)"""
self.routine = other.routine
self._touched = set(other._touched)
self._range = dict(other._range)
self._writeable = set(other._writeable)
self._terminated = other._terminated
self._pointer_assoc = dict(other._pointer_assoc)
def each_meaningful(self):
for ref in self._range.keys():
yield ref
def each_touched(self):
for ref in self._touched:
yield ref
def each_writeable(self):
for ref in self._writeable:
yield ref
def assert_meaningful(self, *refs, **kwargs):
exception_class = kwargs.get('exception_class', UnmeaningfulReadError)
for ref in refs:
# statics are always meaningful
if self.symtab.has_static(self.routine.name, ref.name):
continue
if self.is_constant(ref):
pass
elif isinstance(ref, LocationRef):
if ref not in self._range:
message = ref.name
if kwargs.get('message'):
message += ' (%s)' % kwargs['message']
raise exception_class(self.routine, message)
elif isinstance(ref, IndexedRef):
self.assert_meaningful(ref.ref, **kwargs)
self.assert_meaningful(ref.index, **kwargs)
else:
raise NotImplementedError(ref)
def assert_writeable(self, *refs, **kwargs):
exception_class = kwargs.get('exception_class', ForbiddenWriteError)
for ref in refs:
# statics are always writeable
if self.symtab.has_static(self.routine.name, ref.name):
continue
if ref not in self._writeable:
message = ref.name
if kwargs.get('message'):
message += ' (%s)' % kwargs['message']
raise exception_class(self.routine, message)
def assert_in_range(self, inside, outside, offset):
"""Given two locations, assert that the first location, offset by the given offset,
is contained 'inside' the second location."""
assert isinstance(inside, LocationRef)
assert isinstance(outside, LocationRef)
# inside should always be meaningful
inside_range = self._range[inside]
# outside might not be meaningful, so default to max range if necessary
if outside in self._range:
outside_range = self._range[outside]
else:
outside_range = self.max_range(outside)
if (inside_range[0] + offset.value) < outside_range[0] or (inside_range[1] + offset.value) > outside_range[1]:
raise RangeExceededError(self.routine,
"Possible range of {} {} (+{}) exceeds acceptable range of {} {}".format(
inside, inside_range, offset, outside, outside_range
)
)
def set_touched(self, *refs):
for ref in refs:
self._touched.add(ref)
# TODO: it might be possible to invalidate the range here
def set_meaningful(self, *refs):
for ref in refs:
if ref not in self._range:
self._range[ref] = self.max_range(ref)
def set_top_of_range(self, ref, top):
self.assert_meaningful(ref)
(bottom, _) = self._range[ref]
self._range[ref] = (bottom, top)
def set_bottom_of_range(self, ref, bottom):
self.assert_meaningful(ref)
(top, _) = self._range[ref]
self._range[ref] = (bottom, top)
def set_range(self, ref, bottom, top):
self.assert_meaningful(ref)
self._range[ref] = (bottom, top)
def get_top_of_range(self, ref):
if isinstance(ref, ConstantRef):
return ref.value
self.assert_meaningful(ref)
(_, top) = self._range[ref]
return top
def get_bottom_of_range(self, ref):
if isinstance(ref, ConstantRef):
return ref.value
self.assert_meaningful(ref)
(bottom, _) = self._range[ref]
return bottom
def get_range(self, ref):
if isinstance(ref, ConstantRef):
return (ref.value, ref.value)
self.assert_meaningful(ref)
(bottom, top) = self._range[ref]
return bottom, top
def copy_range(self, src, dest):
self.assert_meaningful(src)
if src in self._range:
src_range = self._range[src]
else:
src_range = self.max_range(src)
self._range[dest] = src_range
def invalidate_range(self, ref):
self.assert_meaningful(ref)
self._range[ref] = self.max_range(ref)
def set_unmeaningful(self, *refs):
for ref in refs:
if ref in self._range:
del self._range[ref]
def set_written(self, *refs):
"""A "helper" method which does the following common sequence for
the given refs: asserts they're all writable, and sets them all
as touched and meaningful."""
self.assert_writeable(*refs)
self.set_touched(*refs)
self.set_meaningful(*refs)
def set_unwriteable(self, *refs):
"""Intended to be used for implementing analyzing `for`."""
for ref in refs:
self._writeable.remove(ref)
def set_writeable(self, *refs):
"""Intended to be used for implementing analyzing `for`, but also used in `save`."""
for ref in refs:
self._writeable.add(ref)
def encounter_gotos(self, gotos):
self._gotos_encountered |= gotos
def encountered_gotos(self):
return self._gotos_encountered
def set_terminated(self):
# Having a terminated context and having encountered gotos is not the same thing.
self._terminated = True
def has_terminated(self):
return self._terminated
def extract(self, location):
"""Sets the given location as writeable in the context, and returns a 'baton' representing
the previous state of context for that location. This 'baton' can be used to later restore
this state of context."""
# Used in `save`.
baton = (
location,
location in self._touched,
self._range.get(location, None),
location in self._writeable,
)
self.set_writeable(location)
return baton
def re_introduce(self, baton):
"""Given a 'baton' produced by `extract()`, restores the context for that saved location
to what it was before `extract()` was called."""
# Used in `save`.
location, was_touched, was_range, was_writeable = baton
if was_touched:
self._touched.add(location)
elif location in self._touched:
self._touched.remove(location)
if was_range is not None:
self._range[location] = was_range
elif location in self._range:
del self._range[location]
if was_writeable:
self._writeable.add(location)
elif location in self._writeable:
self._writeable.remove(location)
def get_assoc(self, pointer):
return self._pointer_assoc.get(pointer)
def set_assoc(self, pointer, table):
self._pointer_assoc[pointer] = table
def is_constant(self, ref):
"""read-only means that the program cannot change the value
of a location. constant means that the value of the location
will not change during the lifetime of the program."""
if isinstance(ref, ConstantRef):
return True
if isinstance(ref, (IndirectRef, IndexedRef)):
return False
if isinstance(ref, LocationRef):
type_ = self.symtab.fetch_global_type(ref.name)
return isinstance(type_, RoutineType)
raise NotImplementedError
def max_range(self, ref):
if isinstance(ref, ConstantRef):
return (ref.value, ref.value)
elif self.symtab.has_static(self.routine.name, ref.name):
return self.symtab.fetch_static_type(self.routine.name, ref.name).max_range
else:
return self.symtab.fetch_global_type(ref.name).max_range
class Analyzer(object):
def __init__(self, symtab, debug=False):
@ -401,15 +92,15 @@ class Analyzer(object):
# - - - - helper methods - - - -
def get_type_for_name(self, name):
if self.current_routine and self.symtab.has_static(self.current_routine.name, name):
return self.symtab.fetch_static_type(self.current_routine.name, name)
if self.current_routine and self.symtab.has_local(self.current_routine.name, name):
return self.symtab.fetch_local_type(self.current_routine.name, name)
return self.symtab.fetch_global_type(name)
def get_type(self, ref):
if isinstance(ref, ConstantRef):
return ref.type
if not isinstance(ref, LocationRef):
raise NotImplementedError
raise NotImplementedError(str(ref))
return self.get_type_for_name(ref.name)
def assert_type(self, type_, *locations):
@ -462,6 +153,14 @@ class Analyzer(object):
self.current_routine = routine
type_ = self.get_type_for_name(routine.name)
context = AnalysisContext(self.symtab, routine, type_.inputs, type_.outputs, type_.trashes)
# register any local statics as already-initialized
for local_name, local_symentry in self.symtab.locals.get(routine.name, {}).items():
ref = self.symtab.fetch_local_ref(routine.name, local_name)
if local_symentry.ast_node.initial is not None:
context.set_meaningful(ref)
context.set_range(ref, local_symentry.ast_node.initial, local_symentry.ast_node.initial)
self.exit_contexts = []
self.analyze_block(routine.block, context)
@ -505,7 +204,7 @@ class Analyzer(object):
# if something was touched, then it should have been declared to be writable.
for ref in context.each_touched():
if ref not in type_.outputs and ref not in type_.trashes and not self.symtab.has_static(routine.name, ref.name):
if ref not in type_.outputs and ref not in type_.trashes and not self.symtab.has_local(routine.name, ref.name):
raise ForbiddenWriteError(routine, ref.name)
self.exit_contexts = None
@ -531,15 +230,15 @@ class Analyzer(object):
elif isinstance(instr, For):
self.analyze_for(instr, context)
elif isinstance(instr, WithInterruptsOff):
self.analyze_block(instr.block, context)
if context.encountered_gotos():
raise IllegalJumpError(instr, instr)
self.analyze_with_interrupts_off(instr, context)
elif isinstance(instr, Save):
self.analyze_save(instr, context)
elif isinstance(instr, PointInto):
self.analyze_point_into(instr, context)
elif isinstance(instr, Reset):
self.analyze_reset(instr, context)
else:
raise NotImplementedError
raise NotImplementedError(str(instr))
def analyze_single_op(self, instr, context):
@ -589,7 +288,6 @@ class Analyzer(object):
target = context.get_assoc(dest.ref)
if not target:
raise ForbiddenWriteError(instr, dest.ref)
context.set_touched(target)
context.set_written(target)
elif self.get_type(src) != self.get_type(dest):
@ -759,12 +457,11 @@ class Analyzer(object):
# 2. check that the context is meaningful
if isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndirectRef):
context.assert_meaningful(src, REG_Y)
context.assert_meaningful(src, dest.ref, REG_Y)
target = context.get_assoc(dest.ref)
if not target:
raise ForbiddenWriteError(instr, dest.ref)
context.set_touched(target)
context.set_written(target)
elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef):
@ -775,10 +472,10 @@ class Analyzer(object):
raise UnmeaningfulReadError(instr, src.ref)
context.assert_meaningful(origin)
context.set_touched(dest)
context.set_written(dest)
elif isinstance(src, IndirectRef) and isinstance(dest, IndirectRef):
context.assert_meaningful(src.ref, REG_Y)
context.assert_meaningful(src.ref, dest.ref, REG_Y)
origin = context.get_assoc(src.ref)
if not origin:
@ -788,7 +485,6 @@ class Analyzer(object):
target = context.get_assoc(dest.ref)
if not target:
raise ForbiddenWriteError(instr, dest.ref)
context.set_touched(target)
context.set_written(target)
elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef):
@ -799,7 +495,6 @@ class Analyzer(object):
context.set_written(dest.ref)
elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef):
context.assert_meaningful(src.ref, src.index)
context.set_touched(dest)
context.set_written(dest)
else:
context.assert_meaningful(src)
@ -865,7 +560,6 @@ class Analyzer(object):
exit_context = context.clone()
for ref in type_.outputs:
exit_context.set_touched(ref) # ?
exit_context.set_written(ref)
for ref in type_.trashes:
@ -973,6 +667,13 @@ class Analyzer(object):
context.set_range(instr.dest, instr.final, instr.final)
context.set_writeable(instr.dest)
def analyze_with_interrupts_off(self, instr, context):
block = instr.block
for instr in block.instrs:
if isinstance(instr, (Call, GoTo, WithInterruptsOff)):
raise IllegalJumpError(instr, instr)
self.analyze_instr(instr, context)
def analyze_save(self, instr, context):
batons = []
for location in instr.locations:
@ -1007,11 +708,11 @@ class Analyzer(object):
if context.get_assoc(instr.pointer):
raise ForbiddenWriteError(instr, instr.pointer)
# associate pointer with table, mark it as meaningful.
# associate pointer with table
# (do not mark it as meaningful yet - that's reset's job.)
context.set_assoc(instr.pointer, instr.table)
context.set_meaningful(instr.pointer)
context.set_touched(instr.pointer)
context.set_unmeaningful(instr.pointer)
self.analyze_block(instr.block, context)
if context.encountered_gotos():
@ -1021,3 +722,21 @@ class Analyzer(object):
context.set_assoc(instr.pointer, None)
context.set_unmeaningful(instr.pointer)
def analyze_reset(self, instr, context):
type = self.get_type(instr.pointer)
if not isinstance(type, (PointerType)):
raise TypeMismatchError(instr, instr.pointer.name)
table = context.get_assoc(instr.pointer)
if not table:
raise ForbiddenWriteError(instr, '{} is not associated with any table'.format(instr.pointer.name))
context.assert_meaningful(table)
low_limit, high_limit = context.get_range(table)
assert isinstance(instr.offset, ConstantRef)
if instr.offset.value < low_limit or instr.offset.value > high_limit:
raise RangeExceededError(instr, instr.pointer.name)
context.set_meaningful(instr.pointer)
context.set_touched(instr.pointer)

View File

@ -59,7 +59,7 @@ class Defn(AST):
class Routine(AST):
value_attrs = ('name', 'addr', 'initial',)
children_attrs = ('statics',)
children_attrs = ('locals',)
child_attrs = ('block',)
@ -75,6 +75,10 @@ class SingleOp(Instr):
value_attrs = ('opcode', 'dest', 'src',)
class Reset(Instr):
value_attrs = ('pointer', 'offset',)
class Call(Instr):
value_attrs = ('location',)

View File

@ -1,7 +1,7 @@
# encoding: UTF-8
from sixtypical.ast import (
Program, Routine, Block, SingleOp, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
Program, Routine, Block, SingleOp, Reset, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
)
from sixtypical.model import (
ConstantRef, LocationRef, IndexedRef, IndirectRef,
@ -34,16 +34,17 @@ class Compiler(object):
self.symtab = symtab
self.emitter = emitter
self.routines = {} # routine.name -> Routine
self.routine_statics = {} # routine.name -> { static.name -> Label }
self.routine_locals = {} # routine.name -> { local.name -> Label }
self.labels = {} # global.name -> Label ("global" includes routines)
self.trampolines = {} # Location -> Label
self.pointer_assoc = {} # pointer name -> table name (I'm not entirely happy about this)
self.current_routine = None
# - - - - helper methods - - - -
def get_type_for_name(self, name):
if self.current_routine and self.symtab.has_static(self.current_routine.name, name):
return self.symtab.fetch_static_type(self.current_routine.name, name)
if self.current_routine and self.symtab.has_local(self.current_routine.name, name):
return self.symtab.fetch_local_type(self.current_routine.name, name)
return self.symtab.fetch_global_type(name)
def get_type(self, ref):
@ -76,9 +77,9 @@ class Compiler(object):
def get_label(self, name):
if self.current_routine:
static_label = self.routine_statics.get(self.current_routine.name, {}).get(name)
if static_label:
return static_label
local_label = self.routine_locals.get(self.current_routine.name, {}).get(name)
if local_label:
return local_label
return self.labels[name]
def absolute_or_zero_page(self, label):
@ -107,16 +108,15 @@ class Compiler(object):
label.set_addr(routine.addr)
self.labels[routine.name] = label
if hasattr(routine, 'statics'):
self.current_routine = routine
static_labels = {}
for defn in routine.statics:
length = self.compute_length_of_defn(defn)
label = Label(defn.name, addr=defn.addr, length=length)
static_labels[defn.name] = label
declarations.append((defn, self.symtab.fetch_static_type(routine.name, defn.name), label))
self.routine_statics[routine.name] = static_labels
self.current_routine = None
self.current_routine = routine
local_labels = {}
for defn in routine.locals:
length = self.compute_length_of_defn(defn)
label = Label(defn.name, addr=defn.addr, length=length)
local_labels[defn.name] = label
declarations.append((defn, self.symtab.fetch_local_type(routine.name, defn.name), label))
self.routine_locals[routine.name] = local_labels
self.current_routine = None
if compilation_roster is None:
compilation_roster = [['main']] + [[routine.name] for routine in program.routines if routine.name != 'main']
@ -192,6 +192,8 @@ class Compiler(object):
return self.compile_save(instr)
elif isinstance(instr, PointInto):
return self.compile_point_into(instr)
elif isinstance(instr, Reset):
return self.compile_reset(instr)
else:
raise NotImplementedError
@ -742,12 +744,16 @@ class Compiler(object):
self.emitter.emit(STA(Absolute(src_label)))
def compile_point_into(self, instr):
src_label = self.get_label(instr.table.name)
self.pointer_assoc[instr.pointer.name] = instr.table.name
self.compile_block(instr.block)
del self.pointer_assoc[instr.pointer.name]
def compile_reset(self, instr):
table_name = self.pointer_assoc[instr.pointer.name]
src_label = Offset(self.get_label(table_name), instr.offset.value)
dest_label = self.get_label(instr.pointer.name)
self.emitter.emit(LDA(Immediate(HighAddressByte(src_label))))
self.emitter.emit(STA(ZeroPage(dest_label)))
self.emitter.emit(LDA(Immediate(LowAddressByte(src_label))))
self.emitter.emit(STA(ZeroPage(Offset(dest_label, 1))))
self.compile_block(instr.block)

328
src/sixtypical/context.py Normal file
View File

@ -0,0 +1,328 @@
# encoding: UTF-8
from sixtypical.model import (
RoutineType, ConstantRef, LocationRef, IndirectRef, IndexedRef,
)
class AnalysisContext(object):
"""
A location is touched if it was changed (or even potentially
changed) during this routine, or some routine called by this routine.
A location is meaningful if it was an input to this routine,
or if it was set to a meaningful value by some operation in this
routine (or some routine called by this routine).
If a location is meaningful, it has a range. This range represents
the lowest and highest values that it might possibly be (i.e. we know
it cannot possibly be below the lowest or above the highest.) In the
absence of any usage information, the range of a byte, is 0..255 and
the range of a word is 0..65535.
A location is writeable if it was listed in the outputs and trashes
lists of this routine. A location can also be temporarily marked
unwriteable in certain contexts, such as `for` loops.
"""
def __init__(self, symtab, routine, inputs, outputs, trashes):
from sixtypical.analyzer import ConstantConstraintError, InconsistentConstraintsError
self.symtab = symtab
self.routine = routine # Routine (AST node)
self._touched = set() # {LocationRef}
self._range = dict() # LocationRef -> (Int, Int)
self._writeable = set() # {LocationRef}
self._terminated = False
self._gotos_encountered = set()
self._pointer_assoc = dict()
for ref in inputs:
if self.is_constant(ref):
raise ConstantConstraintError(self.routine, ref.name)
self._range[ref] = self.max_range(ref)
output_names = set()
for ref in outputs:
if self.is_constant(ref):
raise ConstantConstraintError(self.routine, ref.name)
output_names.add(ref.name)
self._writeable.add(ref)
for ref in trashes:
if self.is_constant(ref):
raise ConstantConstraintError(self.routine, ref.name)
if ref.name in output_names:
raise InconsistentConstraintsError(self.routine, ref.name)
self._writeable.add(ref)
def __str__(self):
return "{}(\n _touched={},\n _range={},\n _writeable={}\n)".format(
self.__class__.__name__,
LocationRef.format_set(self._touched), LocationRef.format_set(self._range), LocationRef.format_set(self._writeable)
)
def to_json_data(self):
type_ = self.symtab.fetch_global_type(self.routine.name)
return {
'routine_inputs': ','.join(sorted(loc.name for loc in type_.inputs)),
'routine_outputs': ','.join(sorted(loc.name for loc in type_.outputs)),
'routine_trashes': ','.join(sorted(loc.name for loc in type_.trashes)),
'touched': ','.join(sorted(loc.name for loc in self._touched)),
'range': dict((loc.name, '{}-{}'.format(rng[0], rng[1])) for (loc, rng) in self._range.items()),
'writeable': ','.join(sorted(loc.name for loc in self._writeable)),
'terminated': self._terminated,
'gotos_encountered': ','.join(sorted(loc.name for loc in self._gotos_encountered)),
}
def clone(self):
c = AnalysisContext(self.symtab, self.routine, [], [], [])
c._touched = set(self._touched)
c._range = dict(self._range)
c._writeable = set(self._writeable)
c._pointer_assoc = dict(self._pointer_assoc)
c._gotos_encountered = set(self._gotos_encountered)
return c
def update_from(self, other):
"""Replaces the information in this context, with the information from the other context.
This is an overwriting action - it does not attempt to merge the contexts.
We do not replace the gotos_encountered for technical reasons. (In `analyze_if`,
we merge those sets afterwards; at the end of `analyze_routine`, they are not distinct in the
set of contexts we are updating from, and we want to retain our own.)"""
self.routine = other.routine
self._touched = set(other._touched)
self._range = dict(other._range)
self._writeable = set(other._writeable)
self._terminated = other._terminated
self._pointer_assoc = dict(other._pointer_assoc)
def each_meaningful(self):
for ref in self._range.keys():
yield ref
def each_touched(self):
for ref in self._touched:
yield ref
def each_writeable(self):
for ref in self._writeable:
yield ref
def assert_meaningful(self, *refs, **kwargs):
from sixtypical.analyzer import UnmeaningfulReadError
exception_class = kwargs.get('exception_class', UnmeaningfulReadError)
for ref in refs:
if self.symtab.has_local(self.routine.name, ref.name):
if ref not in self._range:
message = ref.name
if kwargs.get('message'):
message += ' (%s)' % kwargs['message']
raise exception_class(self.routine, message)
else:
continue
if self.is_constant(ref):
pass
elif isinstance(ref, LocationRef):
if ref not in self._range:
message = ref.name
if kwargs.get('message'):
message += ' (%s)' % kwargs['message']
raise exception_class(self.routine, message)
elif isinstance(ref, IndexedRef):
self.assert_meaningful(ref.ref, **kwargs)
self.assert_meaningful(ref.index, **kwargs)
else:
raise NotImplementedError(ref)
def assert_writeable(self, *refs, **kwargs):
from sixtypical.analyzer import ForbiddenWriteError
exception_class = kwargs.get('exception_class', ForbiddenWriteError)
for ref in refs:
# locals are always writeable
if self.symtab.has_local(self.routine.name, ref.name):
continue
if ref not in self._writeable:
message = ref.name
if kwargs.get('message'):
message += ' (%s)' % kwargs['message']
raise exception_class(self.routine, message)
def assert_in_range(self, inside, outside, offset):
"""Given two locations, assert that the first location, offset by the given offset,
is contained 'inside' the second location."""
from sixtypical.analyzer import RangeExceededError
assert isinstance(inside, LocationRef)
assert isinstance(outside, LocationRef)
# inside should always be meaningful
inside_range = self._range[inside]
# outside might not be meaningful, so default to max range if necessary
if outside in self._range:
outside_range = self._range[outside]
else:
outside_range = self.max_range(outside)
if (inside_range[0] + offset.value) < outside_range[0] or (inside_range[1] + offset.value) > outside_range[1]:
raise RangeExceededError(self.routine,
"Possible range of {} {} (+{}) exceeds acceptable range of {} {}".format(
inside, inside_range, offset, outside, outside_range
)
)
def set_touched(self, *refs):
for ref in refs:
self._touched.add(ref)
# TODO: it might be possible to invalidate the range here
def set_meaningful(self, *refs):
for ref in refs:
if ref not in self._range:
self._range[ref] = self.max_range(ref)
def set_top_of_range(self, ref, top):
self.assert_meaningful(ref)
(bottom, _) = self._range[ref]
self._range[ref] = (bottom, top)
def set_bottom_of_range(self, ref, bottom):
self.assert_meaningful(ref)
(top, _) = self._range[ref]
self._range[ref] = (bottom, top)
def set_range(self, ref, bottom, top):
self.assert_meaningful(ref)
self._range[ref] = (bottom, top)
def get_top_of_range(self, ref):
if isinstance(ref, ConstantRef):
return ref.value
self.assert_meaningful(ref)
(_, top) = self._range[ref]
return top
def get_bottom_of_range(self, ref):
if isinstance(ref, ConstantRef):
return ref.value
self.assert_meaningful(ref)
(bottom, _) = self._range[ref]
return bottom
def get_range(self, ref):
if isinstance(ref, ConstantRef):
return (ref.value, ref.value)
self.assert_meaningful(ref)
(bottom, top) = self._range[ref]
return bottom, top
def copy_range(self, src, dest):
self.assert_meaningful(src)
if src in self._range:
src_range = self._range[src]
else:
src_range = self.max_range(src)
self._range[dest] = src_range
def invalidate_range(self, ref):
self.assert_meaningful(ref)
self._range[ref] = self.max_range(ref)
def set_unmeaningful(self, *refs):
for ref in refs:
if ref in self._range:
del self._range[ref]
def set_written(self, *refs):
"""A "helper" method which does the following common sequence for
the given refs: asserts they're all writable, and sets them all
as touched and meaningful."""
self.assert_writeable(*refs)
self.set_touched(*refs)
self.set_meaningful(*refs)
def set_unwriteable(self, *refs):
"""Intended to be used for implementing analyzing `for`."""
for ref in refs:
self._writeable.remove(ref)
def set_writeable(self, *refs):
"""Intended to be used for implementing analyzing `for`, but also used in `save`."""
for ref in refs:
self._writeable.add(ref)
def encounter_gotos(self, gotos):
self._gotos_encountered |= gotos
def encountered_gotos(self):
return self._gotos_encountered
def set_terminated(self):
# Having a terminated context and having encountered gotos is not the same thing.
self._terminated = True
def has_terminated(self):
return self._terminated
def extract(self, location):
"""Sets the given location as writeable in the context, and returns a 'baton' representing
the previous state of context for that location. This 'baton' can be used to later restore
this state of context."""
# Used in `save`.
baton = (
location,
location in self._touched,
self._range.get(location, None),
location in self._writeable,
)
self.set_writeable(location)
return baton
def re_introduce(self, baton):
"""Given a 'baton' produced by `extract()`, restores the context for that saved location
to what it was before `extract()` was called."""
# Used in `save`.
location, was_touched, was_range, was_writeable = baton
if was_touched:
self._touched.add(location)
elif location in self._touched:
self._touched.remove(location)
if was_range is not None:
self._range[location] = was_range
elif location in self._range:
del self._range[location]
if was_writeable:
self._writeable.add(location)
elif location in self._writeable:
self._writeable.remove(location)
def get_assoc(self, pointer):
return self._pointer_assoc.get(pointer)
def set_assoc(self, pointer, table):
self._pointer_assoc[pointer] = table
def is_constant(self, ref):
"""read-only means that the program cannot change the value
of a location. constant means that the value of the location
will not change during the lifetime of the program."""
if isinstance(ref, ConstantRef):
return True
if isinstance(ref, (IndirectRef, IndexedRef)):
return False
if isinstance(ref, LocationRef):
type_ = self.symtab.fetch_global_type(ref.name)
return isinstance(type_, RoutineType)
raise NotImplementedError
def max_range(self, ref):
if isinstance(ref, ConstantRef):
return (ref.value, ref.value)
elif self.symtab.has_local(self.routine.name, ref.name):
return self.symtab.fetch_local_type(self.routine.name, ref.name).max_range
else:
return self.symtab.fetch_global_type(ref.name).max_range

View File

@ -128,7 +128,7 @@ class Offset(Emittable):
class HighAddressByte(Emittable):
def __init__(self, label):
assert isinstance(label, Label)
assert isinstance(label, (Label, Offset))
self.label = label
def size(self):
@ -143,7 +143,7 @@ class HighAddressByte(Emittable):
class LowAddressByte(Emittable):
def __init__(self, label):
assert isinstance(label, Label)
assert isinstance(label, (Label, Offset))
self.label = label
def size(self):

View File

@ -1,23 +1,15 @@
# encoding: UTF-8
from sixtypical.ast import (
Program, Defn, Routine, Block, SingleOp, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
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,
LocationRef, ConstantRef, IndirectRef, IndexedRef,
ConstantRef, IndirectRef, IndexedRef,
)
from sixtypical.scanner import Scanner
class SymEntry(object):
def __init__(self, ast_node, type_):
self.ast_node = ast_node
self.type_ = type_
def __repr__(self):
return "%s(%r, %r)" % (self.__class__.__name__, self.ast_node, self.type_)
from sixtypical.symtab import SymEntry
class ForwardReference(object):
@ -28,42 +20,6 @@ class ForwardReference(object):
return "%s(%r)" % (self.__class__.__name__, self.name)
class SymbolTable(object):
def __init__(self):
self.symbols = {} # symbol name -> SymEntry
self.statics = {} # routine name -> (symbol name -> SymEntry)
self.typedefs = {} # type name -> Type AST
self.consts = {} # const name -> ConstantRef
for name in ('a', 'x', 'y'):
self.symbols[name] = SymEntry(None, TYPE_BYTE)
for name in ('c', 'z', 'n', 'v'):
self.symbols[name] = SymEntry(None, TYPE_BIT)
def __str__(self):
return "Symbols: {}\nStatics: {}\nTypedefs: {}\nConsts: {}".format(self.symbols, self.statics, self.typedefs, self.consts)
def has_static(self, routine_name, name):
return name in self.statics.get(routine_name, {})
def fetch_global_type(self, name):
return self.symbols[name].type_
def fetch_static_type(self, routine_name, name):
return self.statics[routine_name][name].type_
def fetch_global_ref(self, name):
if name in self.symbols:
return LocationRef(name)
return None
def fetch_static_ref(self, routine_name, name):
routine_statics = self.statics.get(routine_name, {})
if name in routine_statics:
return LocationRef(name)
return None
class Parser(object):
def __init__(self, symtab, text, filename):
self.symtab = symtab
@ -76,7 +32,7 @@ class Parser(object):
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_static_ref(routine_name, name)
model = self.symtab.fetch_local_ref(routine_name, name)
if model is None and allow_forward:
return ForwardReference(name)
if model is None:
@ -88,10 +44,12 @@ class Parser(object):
self.syntax_error('Symbol "%s" already declared' % name)
self.symtab.symbols[name] = SymEntry(ast_node, type_)
def declare_static(self, routine_name, name, 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' % name)
self.symtab.statics.setdefault(routine_name, {})[name] = SymEntry(ast_node, type_)
self.syntax_error('Symbol "%s" already declared globally' % name)
self.symtab.locals.setdefault(routine_name, {})[name] = SymEntry(ast_node, type_)
# ---- symbol resolution
@ -306,17 +264,17 @@ class Parser(object):
type_ = self.defn_type()
if not isinstance(type_, RoutineType):
self.syntax_error("Can only define a routine, not {}".format(repr(type_)))
statics = []
locals_ = []
if self.scanner.consume('@'):
self.scanner.check_type('integer literal')
block = None
addr = int(self.scanner.token)
self.scanner.scan()
else:
statics = self.statics()
locals_ = self.locals()
block = self.block()
addr = None
return type_, Routine(self.scanner.line_number, name=name, block=block, addr=addr, statics=statics)
return type_, Routine(self.scanner.line_number, name=name, block=block, addr=addr, locals=locals_)
def labels(self):
accum = []
@ -370,13 +328,19 @@ class Parser(object):
loc = IndexedRef(loc, offset, index)
return loc
def statics(self):
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_static(self.current_routine_name, defn.name, defn, type_)
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
@ -424,6 +388,10 @@ class Parser(object):
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

50
src/sixtypical/symtab.py Normal file
View File

@ -0,0 +1,50 @@
# encoding: UTF-8
from sixtypical.model import (
TYPE_BIT, TYPE_BYTE, LocationRef,
)
class SymEntry(object):
def __init__(self, ast_node, type_):
self.ast_node = ast_node
self.type_ = type_
def __repr__(self):
return "%s(%r, %r)" % (self.__class__.__name__, self.ast_node, self.type_)
class SymbolTable(object):
def __init__(self):
self.symbols = {} # symbol name -> SymEntry
self.locals = {} # routine name -> (symbol name -> SymEntry)
self.typedefs = {} # type name -> Type AST
self.consts = {} # const name -> ConstantRef
for name in ('a', 'x', 'y'):
self.symbols[name] = SymEntry(None, TYPE_BYTE)
for name in ('c', 'z', 'n', 'v'):
self.symbols[name] = SymEntry(None, TYPE_BIT)
def __str__(self):
return "Symbols: {}\nLocals: {}\nTypedefs: {}\nConsts: {}".format(self.symbols, self.locals, self.typedefs, self.consts)
def has_local(self, routine_name, name):
return name in self.locals.get(routine_name, {})
def fetch_global_type(self, name):
return self.symbols[name].type_
def fetch_local_type(self, routine_name, name):
return self.locals[routine_name][name].type_
def fetch_global_ref(self, name):
if name in self.symbols:
return LocationRef(name)
return None
def fetch_local_ref(self, routine_name, name):
routine_locals = self.locals.get(routine_name, {})
if name in routine_locals:
return LocationRef(name)
return None

14
test.sh
View File

@ -1,7 +1,13 @@
#!/bin/sh
# This currently represents a lot of tests! If you only want to run a subset,
# it's probably best to run `falderal` manually on the file(s) you want to test.
falderal --substring-error \
tests/SixtyPical\ Syntax.md \
tests/SixtyPical\ Analysis.md \
tests/SixtyPical\ Fallthru.md \
tests/SixtyPical\ Compilation.md
"tests/appliances/sixtypical.md" \
"tests/SixtyPical Syntax.md" \
"tests/SixtyPical Analysis.md" \
"tests/SixtyPical Storage.md" \
"tests/SixtyPical Control Flow.md" \
"tests/SixtyPical Fallthru.md" \
"tests/SixtyPical Compilation.md"

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,6 @@ SixtyPical to 6502 machine code.
[Falderal]: http://catseye.tc/node/Falderal
-> Functionality "Compile SixtyPical program" is implemented by
-> shell command "bin/sixtypical --output-format=c64-basic-prg --traceback %(test-body-file) --output /tmp/foo && tests/appliances/bin/dcc6502-adapter </tmp/foo"
-> Tests for functionality "Compile SixtyPical program"
Null program.
@ -1400,7 +1397,7 @@ Subtracting a word memory location from another word memory location.
### Tables and Pointers
Load address of table into pointer.
Associate pointer with table. Does nothing by itself.
| byte table[256] tab
| pointer ptr @ 254
@ -1415,6 +1412,24 @@ Load address of table into pointer.
| }
| }
= $080D LDY #$00
= $080F RTS
Reset pointer to table.
| byte table[256] tab
| pointer ptr @ 254
|
| define main routine
| inputs tab
| outputs tab, y
| trashes a, z, n, ptr
| {
| ld y, 0
| point ptr into tab {
| reset ptr 0
| }
| }
= $080D LDY #$00
= $080F LDA #$18
= $0811 STA $FE
= $0813 LDA #$08
@ -1433,6 +1448,7 @@ Write literal through a pointer.
| {
| ld y, 0
| point ptr into tab {
| reset ptr 0
| copy 123, [ptr] + y
| }
| }
@ -1458,6 +1474,7 @@ Write stored value through a pointer.
| {
| ld y, 0
| point ptr into tab {
| reset ptr 0
| copy foo, [ptr] + y
| }
| }
@ -1483,6 +1500,7 @@ Read through a pointer, into a byte storage location, or the `a` register.
| {
| ld y, 0
| point ptr into tab {
| reset ptr 0
| copy [ptr] + y, foo
| ld a, [ptr] + y
| }
@ -1497,6 +1515,39 @@ Read through a pointer, into a byte storage location, or the `a` register.
= $081C LDA ($FE),Y
= $081E RTS
Multiple `reset`s may occur inside the same block.
| byte table[256] tab @ 1024
| pointer ptr @ 254
| byte foo
|
| define main routine
| inputs tab
| outputs y, foo
| trashes a, z, n, ptr
| {
| ld y, 0
| point ptr into tab {
| reset ptr 16
| copy [ptr] + y, foo
| reset ptr 18
| ld a, [ptr] + y
| }
| }
= $080D LDY #$00
= $080F LDA #$10
= $0811 STA $FE
= $0813 LDA #$04
= $0815 STA $FF
= $0817 LDA ($FE),Y
= $0819 STA $0827
= $081C LDA #$12
= $081E STA $FE
= $0820 LDA #$04
= $0822 STA $FF
= $0824 LDA ($FE),Y
= $0826 RTS
Read and write through two pointers.
| byte table[256] tab
@ -1510,7 +1561,9 @@ Read and write through two pointers.
| {
| ld y, 0
| point ptra into tab {
| reset ptra 0
| point ptrb into tab {
| reset ptrb 0
| copy [ptra] + y, [ptrb] + y
| }
| }
@ -1541,6 +1594,7 @@ Write the `a` register through a pointer.
| {
| ld y, 0
| point ptr into tab {
| reset ptr 0
| ld a, 255
| st a, [ptr] + y
| }
@ -1571,6 +1625,7 @@ Note that this is *not* range-checked. (Yet.)
| ld y, 0
| st off, c
| point ptr into tab {
| reset ptr 0
| add ptr, delta
| add ptr, word 1
| copy [ptr] + y, foo
@ -1619,9 +1674,9 @@ Trash does nothing except indicate that we do not care about the value anymore.
= $080E LDA #$00
= $0810 RTS
### static ###
### locals ###
Memory locations defined static to a routine are allocated
Memory locations defined local static to a routine are allocated
just the same as initialized global storage locations are.
| define foo routine
@ -1651,3 +1706,66 @@ just the same as initialized global storage locations are.
= $081D RTS
= $081E .byte $FF
= $081F .byte $07
Memory locations defined local dynamic to a routine are allocated
just the same as uninitialized global storage locations are.
| define foo routine
| inputs x
| outputs x
| trashes z, n
| local byte t
| {
| st x, t
| inc t
| ld x, t
| }
|
| define main routine
| trashes a, x, z, n
| local byte t
| {
| ld x, 0
| st x, t
| call foo
| }
= $080D LDX #$00
= $080F STX $0821
= $0812 JSR $0816
= $0815 RTS
= $0816 STX $0820
= $0819 INC $0820
= $081C LDX $0820
= $081F RTS
Memory locations defined local dynamic to a routine are allocated
just the same as uninitialized global storage locations are, even
when declared with a fixed address.
| define foo routine
| inputs x
| outputs x
| trashes z, n
| local byte t @ 1024
| {
| st x, t
| inc t
| ld x, t
| }
|
| define main routine
| trashes a, x, z, n
| local byte t @ 1025
| {
| ld x, 0
| st x, t
| call foo
| }
= $080D LDX #$00
= $080F STX $0401
= $0812 JSR $0816
= $0815 RTS
= $0816 STX $0400
= $0819 INC $0400
= $081C LDX $0400
= $081F RTS

File diff suppressed because it is too large Load Diff

View File

@ -61,12 +61,6 @@ to pass these tests to be considered an implementation of SixtyPical.
[Falderal]: http://catseye.tc/node/Falderal
-> Functionality "Dump fallthru info for SixtyPical program" is implemented by
-> shell command "bin/sixtypical --optimize-fallthru --dump-fallthru-info --analyze-only --traceback %(test-body-file)"
-> Functionality "Compile SixtyPical program with fallthru optimization" is implemented by
-> shell command "bin/sixtypical --output-format=c64-basic-prg --optimize-fallthru --traceback %(test-body-file) --output /tmp/foo && tests/appliances/bin/dcc6502-adapter </tmp/foo"
-> Tests for functionality "Dump fallthru info for SixtyPical program"
A single routine, obviously, falls through to nothing and has nothing fall

1984
tests/SixtyPical Storage.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,6 @@ but not necessarily sensible programs.
[Falderal]: http://catseye.tc/node/Falderal
-> Functionality "Check syntax of SixtyPical program" is implemented by
-> shell command "bin/sixtypical --parse-only --traceback %(test-body-file) && echo ok"
-> Tests for functionality "Check syntax of SixtyPical program"
Rudimentary program.
@ -176,6 +173,7 @@ Other blocks.
| ld a, 0
| }
| point ptr into tab {
| reset ptr 0
| ld a, [ptr] + y
| }
| }
@ -681,6 +679,7 @@ Tables and pointers.
|
| define main routine {
| point ptr into buf {
| reset ptr 0
| copy 123, [ptr] + y
| copy [ptr] + y, foo
| copy [ptr] + y, [ptrb] + y
@ -746,13 +745,14 @@ Only routines can be defined in the new style.
| }
? SyntaxError
Memory locations can be defined static to a routine.
Memory locations can be defined local to a routine.
| define foo routine
| inputs x
| outputs x
| trashes z, n
| static byte t : 0
| local word w
| {
| st x, t
| inc t
@ -762,13 +762,14 @@ Memory locations can be defined static to a routine.
| define main routine
| trashes a, x, z, n
| static byte t : 0
| local word w
| {
| ld x, t
| call foo
| }
= ok
Static memory locations must always be given an initial value.
Local static memory locations must always be given an initial value.
| define main routine
| inputs x
@ -782,7 +783,49 @@ Static memory locations must always be given an initial value.
| }
? SyntaxError
Name of a static cannot shadow an existing global or static.
Local static memory locations may not be given an address.
| define main routine
| inputs x
| outputs x
| trashes z, n
| static byte t @ 1024
| {
| st x, t
| inc t
| ld x, t
| }
? SyntaxError
Local dynamic memory locations may not be given an initial value.
| define main routine
| inputs x
| outputs x
| trashes z, n
| local byte t : 10
| {
| st x, t
| inc t
| ld x, t
| }
? SyntaxError
Local dynamic memory locations may be given an address.
| define main routine
| inputs x
| outputs x
| trashes z, n
| local byte t @ 1024
| {
| st x, t
| inc t
| ld x, t
| }
= ok
Name of a local cannot shadow an existing global or local.
| byte t
|
@ -790,7 +833,7 @@ Name of a static cannot shadow an existing global or static.
| inputs x
| outputs x
| trashes z, n
| static byte t
| static byte t : 10
| {
| st x, t
| inc t
@ -802,11 +845,89 @@ Name of a static cannot shadow an existing global or static.
| inputs x
| outputs x
| trashes z, n
| static byte t
| static byte t
| static byte t : 10
| static byte t : 20
| {
| st x, t
| inc t
| ld x, t
| }
? SyntaxError
| byte t
|
| define main routine
| inputs x
| outputs x
| trashes z, n
| local byte t
| {
| st x, t
| inc t
| ld x, t
| }
? SyntaxError
| define main routine
| inputs x
| outputs x
| trashes z, n
| local word w
| local word w
| {
| st x, t
| inc t
| ld x, t
| }
? SyntaxError
Since the names of locals are lexically local to a routine, they cannot
appear in the inputs, outputs, trashes list of the routine.
| define main routine
| inputs t
| static byte t : 0
| {
| inc t
| }
? SyntaxError
| define main routine
| outputs t
| static byte t : 0
| {
| inc t
| }
? SyntaxError
| define main routine
| trashes t
| static byte t : 0
| {
| inc t
| }
? SyntaxError
| define main routine
| inputs t
| local byte t
| {
| inc t
| }
? SyntaxError
| define main routine
| outputs t
| local byte t
| {
| inc t
| }
? SyntaxError
| define main routine
| trashes t
| local byte t
| {
| inc t
| }
? SyntaxError

View File

@ -0,0 +1,20 @@
This file contains only the [Falderal][] directives that define the different
functionalities tested by the test suite, assuming that it's the reference
implementation, `sixtypical`, that is going to implement these functionalities.
[Falderal]: http://catseye.tc/node/Falderal
-> Functionality "Check syntax of SixtyPical program" is implemented by
-> shell command "bin/sixtypical --parse-only --traceback %(test-body-file) && echo ok"
-> Functionality "Analyze SixtyPical program" is implemented by
-> shell command "bin/sixtypical --analyze-only --traceback %(test-body-file) && echo ok"
-> Functionality "Compile SixtyPical program" is implemented by
-> shell command "bin/sixtypical --output-format=c64-basic-prg --traceback %(test-body-file) --output /tmp/foo && tests/appliances/bin/dcc6502-adapter </tmp/foo"
-> Functionality "Dump fallthru info for SixtyPical program" is implemented by
-> shell command "bin/sixtypical --optimize-fallthru --dump-fallthru-info --analyze-only --traceback %(test-body-file)"
-> Functionality "Compile SixtyPical program with fallthru optimization" is implemented by
-> shell command "bin/sixtypical --output-format=c64-basic-prg --optimize-fallthru --traceback %(test-body-file) --output /tmp/foo && tests/appliances/bin/dcc6502-adapter </tmp/foo"