1
0
mirror of https://github.com/catseye/SixtyPical.git synced 2024-06-07 22:29:27 +00:00

Compare commits

...

58 Commits
0.19 ... master

Author SHA1 Message Date
Chris Pressey
df610e639c Fix link in README. 2024-03-16 11:53:54 +00:00
Chris Pressey
2bddea270f Remove old, out-of-sync license information. 2024-02-02 12:18:46 +00:00
Chris Pressey
5f8802b305 Arrange license info in repo to follow REUSE 3.0 convention. 2024-02-02 12:10:23 +00:00
Chris Pressey
5ecc4e6f56 Import "Future directions for SixtyPical" into doc subdirectory. 2023-12-10 12:16:14 +00:00
Chris Pressey
d5e6eeacd3
Merge pull request #23 from catseye/develop-2023-1
Develop 2023 1
2023-03-09 09:58:21 +00:00
Chris Pressey
880d1f53a1 Update HISTORY for this minor revision. 2023-03-09 09:20:08 +00:00
Chris Pressey
1887ad9377 Update .gitignore. 2023-03-08 13:02:11 +00:00
Chris Pressey
b670bd3316 Remove experimental Circle CI configuration. 2023-03-08 12:53:21 +00:00
Chris Pressey
9b74fd358f Add sixtypical-py2.7.md appliance and have tests pass under 2.7. 2023-03-08 12:50:22 +00:00
Chris Pressey
b38b94ed67 sixtypical and dcc6502-adapter run under Python 3 by default. 2023-03-07 22:17:11 +00:00
Chris Pressey
075ba9ff2a Stylistic improvements to README. 2020-10-19 16:13:11 +01:00
Chris Pressey
918b400fd4 Update README to link to more related information. 2020-10-19 15:07:44 +01:00
Chris Pressey
4b539930bd
Merge pull request #22 from catseye/develop-0.21
Develop 0.21
2019-10-25 12:51:24 +01:00
Chris Pressey
632c2f4517 Correct comment. 2019-10-23 15:00:03 +01:00
Chris Pressey
ff67e15208 Link to new test suite from README 2019-10-23 10:34:37 +01:00
Chris Pressey
023a415a14 Don't search for filenames given on cmdline in the include path. 2019-10-23 10:32:26 +01:00
Chris Pressey
a9917d3ea8 Tweak. 2019-10-23 10:23:43 +01:00
Chris Pressey
f4577804c7 A little reshuffling and improving of READMEs and notes. 2019-10-23 10:21:02 +01:00
Chris Pressey
fbfab44887 Update the errorful rudiments. 2019-10-22 16:56:19 +01:00
Chris Pressey
4b518508d5 called_routines are not stored in context at all. 2019-10-22 16:26:50 +01:00
Chris Pressey
1b5f4c0c4b Add architecture-dependent "joy2delta.60p" to "standard library". 2019-10-22 16:09:58 +01:00
Chris Pressey
dc4677ade8 Update READMEs. 2019-10-22 15:30:24 +01:00
Chris Pressey
d408e740ed Establish an include directory and --include-path option. 2019-10-22 15:10:55 +01:00
Chris Pressey
c70e4a55e4 Merge included programs. 2019-10-22 13:26:21 +01:00
Chris Pressey
175c07781a First cut at include files. 2019-10-22 12:54:04 +01:00
Chris Pressey
987974cc21 Simplify, fixing an apparent bug in the process. 2019-10-22 12:42:17 +01:00
Chris Pressey
565f959ee2 Our CINV interrupt service routine should be preserved. 2019-10-22 12:35:29 +01:00
Chris Pressey
92b1cfeefb Implement tail-call optimization. 2019-10-22 12:23:15 +01:00
Chris Pressey
78a1f2910c Simplify and improve Emitter abstraction. 2019-10-22 11:55:21 +01:00
Chris Pressey
44d97c33a2 Better generation of code at tail of routines. 2019-10-22 11:37:00 +01:00
Chris Pressey
b9df1482c6 Pass next routine to each routine being compiled. 2019-10-22 10:32:14 +01:00
Chris Pressey
ea788264f7 Make test as intended. It fails atm. Simplify driver code. 2019-10-22 10:18:56 +01:00
Chris Pressey
3d88226058 Implement --prune-unreachable-routines. 2019-10-22 09:41:30 +01:00
Chris Pressey
1df6941b01 Callgraph uses reachability. 2019-10-22 09:07:16 +01:00
Chris Pressey
1098347fa5 Routines can be declared preserved. 2019-10-21 21:45:59 +01:00
Chris Pressey
2192a48e0e Update HISTORY. 2019-10-21 21:37:30 +01:00
Chris Pressey
a84cd4de8c Expand on the callgraph tests. 2019-10-21 21:35:28 +01:00
Chris Pressey
40d9c57d64 Fix test, dump format. 2019-10-21 21:23:14 +01:00
Chris Pressey
684256f7e9 Start of tests for this. 2019-10-21 17:29:38 +01:00
Chris Pressey
b09d0c0b76 Routines that are goto'd are also in the call graph. 2019-10-21 17:15:28 +01:00
Chris Pressey
182935a088 First cut at finding all routines that could be assigned to vector. 2019-10-21 16:12:06 +01:00
Chris Pressey
7187fa6285 Refactor: move to dedicated module. 2019-10-21 15:41:17 +01:00
Chris Pressey
bcc256aa5d Checkpoint. 2019-10-21 15:07:54 +01:00
Chris Pressey
2e002fc33e Merge branch 'circle-ci' of https://github.com/catseye/SixtyPical into construct-callgraph 2019-10-21 14:48:27 +01:00
Chris Pressey
894fb1a0f2 Refine the callgraph algorithm. Still incomplete though. 2019-10-21 14:43:21 +01:00
Chris Pressey
58dc68f838 Merge branch 'develop-0.21' into construct-callgraph 2019-10-21 14:08:06 +01:00
Chris Pressey
a966a496d0 Bump version number. 2019-10-21 14:07:41 +01:00
Chris Pressey
87a2f70092 First cut at constructing a call graph. 2019-10-21 14:03:35 +01:00
Chris Pressey
5912bf2684 Fix dcc6502-adapter for latest version of tcarmelveilleux/dcc6502. 2019-10-21 13:34:53 +01:00
Chris Pressey
0ef0dc1628 dcc6502-adapter fixes. 2019-07-16 16:26:16 +01:00
Chris Pressey
775af38960 Change config. 2019-07-16 16:23:30 +01:00
Chris Pressey
952e3528a3 Try this. 2019-07-16 16:22:29 +01:00
Chris Pressey
8467cd947e
Merge pull request #21 from catseye/develop-0.20
Develop 0.20
2019-05-14 16:25:27 +01:00
Chris Pressey
2e2e80664e Forbid nested with interrupts blocks, and more refactoring. 2019-05-14 15:01:10 +01:00
Chris Pressey
3f666f4385 Add reset instruction; much refactoring. 2019-05-14 08:56:35 +01:00
Chris Pressey
dd29b6fd4a Implement local locations that aren't statically initialized. 2019-05-13 12:32:18 +01:00
Chris Pressey
81e28fa757 Jot down some TODO items before I forget them. 2019-04-18 09:16:23 +01:00
Chris Pressey
3d14b6ee6c Fix typo. 2019-04-16 13:59:27 +01:00
89 changed files with 6091 additions and 4443 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
*.pyc
__pycache__
vicerc

17
.reuse/dep5 Normal file
View File

@ -0,0 +1,17 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Files:
.gitignore
bin/sixtypical
eg/rudiments/errorful/*.60p
eg/rudiments/*.60p
eg/*/.gitignore
eg/*/*.png
eg/*/*.prg
eg/*/*.sh
images/*
src/sixtypical/__init__.py
test.sh
tests/appliances/*
Copyright: Chris Pressey, the original author of this work, has dedicated it to the public domain.
License: Unlicense

View File

@ -1,6 +1,70 @@
History of SixtyPical
=====================
<!--
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
-->
0.21-2023.0309
--------------
* Python 3 is the default interpreter for `sixtypical`.
* Test appliance added for testing under Python 2.7.
* `dcc6502` test adapter made runnable under both
Python 2 and Python 3.
0.21
----
* A source file can be included in another source file
by means of the `include` directive.
* A routine can be declared `preserved`, which prevents a
compiler from omitting it from the final executable, even
if it determines it is not called by any other routine.
* The reference implementation constructs a callgraph and
determines the set of routines which are not reachable
(directly or indirectly) from `main`, with an eye to
omitting them from the final executable.
* Added `--prune-unreachable-routines` option, which causes
the compiler to in fact omit routines determined to be
unreachable as described above.
* Added `--include-path` option, which configures the list
of directories that are searched when a source file is
included with the `include` directive.
* Code generation now performs modest peephole optimization
at the end of each routine. This results in better code
generation for constructs in tail position, notably
tail optimization of `calls`, but also for `goto`s and
`if` blocks at the end of a routine.
* Began collecting architecture-specific and portable
include-files for a nascent "standard library".
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
----

90
LICENSE
View File

@ -1,90 +0,0 @@
The contents of the SixtyPical distribution are distributed under the
following three licenses.
The documentation and tests (in the `doc` and `tests` directories) is
covered by the following BSD-compatible license, modelled after the
"Report on the Programming Language Haskell 98" license:
-----------------------------------------------------------------------------
Copyright (c)2014-2018 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
any purpose, provided that it is reproduced in its entirety,
including this Notice. Modified versions of this Report may also be
copied and distributed for any purpose, provided that the modified
version is clearly presented as such, and that it does not claim to
be a definition of the SixtyPical Programming Language.
-----------------------------------------------------------------------------
The source code for the reference implementation and supporting tools (in the
`src` subdirectory) is covered under the following BSD-style license:
-----------------------------------------------------------------------------
Copyright (c)2014-2018, Chris Pressey, Cat's Eye Technologies.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
Redistributions of source code must retain the above copyright
notices, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notices, this list of conditions, and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
Neither the names of the copyright holders nor the names of their
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
-----------------------------------------------------------------------------
All example sources in the `eg` directory were written by Chris Pressey,
and are hereby placed in the public domain, as described in the following
UNLICENSE:
-----------------------------------------------------------------------------
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@ -0,0 +1,27 @@
Copyright (c) 2014-2024, Chris Pressey, Cat's Eye Technologies.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
Redistributions of source code must retain the above copyright
notices, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notices, this list of conditions, and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

10
LICENSES/Unlicense.txt Normal file
View File

@ -0,0 +1,10 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@ -1,10 +1,28 @@
SixtyPical
==========
_Version 0.19. Work-in-progress, everything is subject to change._
<!--
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
-->
**SixtyPical** is a [low-level](#low-level) programming language
supporting a sophisticated [static analysis](#static-analysis).
_Version 0.21_
| _See also:_ [Bubble Escape 2K](https://codeberg.org/catseye/Bubble-Escape#bubble-escape)
∘ [SITU-SOL](https://git.catseye.tc/SITU-SOL/)
- - - -
_NOTE: Having met the majority of its goals, the SixtyPical project_
_might not undergo much more development going forward. See_
[Future directions for SixtyPical][] _for more information._
- - - -
**SixtyPical** brings advanced static analysis to the [6502][].
SixtyPical is a [low-level](#low-level) programming language
supporting some advanced [static analysis](#static-analysis) methods.
Its reference compiler can generate [efficient code](#efficient-code) for
several 6502-based [target platforms](#target-platforms) while catching many
common mistakes at compile-time, reducing the time spent in debugging.
@ -66,8 +84,9 @@ inclusion in the language is primarily to make programs easier to analyze.
### Static analysis
The SixtyPical language defines an [effect system][], and the reference
compiler [abstractly interprets][] the input program to check that
it conforms to it. It can detect common mistakes such as
compiler [abstractly interprets][] the input program in the manner of
[flow typing][] to confirm that it does not violate it. This can detect
common mistakes such as
* you forgot to clear carry before adding something to the accumulator
* a subroutine that you called trashes a register you thought it preserved
@ -109,9 +128,12 @@ 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)
* [Literate test suite for SixtyPical callgraph construction](tests/SixtyPical%20Callgraph.md)
Documentation
-------------
@ -122,9 +144,12 @@ Documentation
* [Output formats supported by `sixtypical`](doc/Output%20Formats.md)
* [TODO](TODO.md)
[MOS Technology 6520]: https://en.wikipedia.org/wiki/MOS_Technology_6502
[Future directions for SixtyPical]: doc/Future%20directions%20for%20SixtyPical.md
[6502]: https://en.wikipedia.org/wiki/MOS_Technology_6502
[MOS Technology 6502]: https://en.wikipedia.org/wiki/MOS_Technology_6502
[effect system]: https://en.wikipedia.org/wiki/Effect_system
[abstractly interprets]: https://en.wikipedia.org/wiki/Abstract_interpretation
[flow typing]: https://en.wikipedia.org/wiki/Flow-sensitive_typing
[calling conventions]: https://en.wikipedia.org/wiki/Calling_convention
[register allocation]: https://en.wikipedia.org/wiki/Register_allocation
[VICE]: http://vice-emu.sourceforge.net/

165
TODO.md
View File

@ -1,6 +1,15 @@
TODO for SixtyPical
===================
<!--
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
-->
Language
--------
### Save values to other-than-the-stack
Allow
@ -9,35 +18,106 @@ 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.
### Pointers into non-byte tables
### Reset pointer in `point into` blocks
Right now you cannot get a pointer into a non-byte (for instance, word or vector) table.
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.
Word and vector tables are stored as two byte tables in memory. This is useful for
indexed access, but makes pointer access more difficult.
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.
Laying them out for pointer access would make indexed access more difficult.
### Pointers associated globally with a table
### 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.)
However note, it's okay for a routine to goto itself. It's a common
pattern for implementing a state machine, for a routine to tail-goto a
vector, which might contain the address of the same routine.
The problems only come up, I think, when a routine calls itself re-entrantly.
So the callgraph would need to distinguish between these two 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.
Implementation
--------------
### Filename and line number 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.
### Better selection of options
`-O` should turn on the standard optimizations.
There should maybe be a flag to turn off tail-call optimization.
Some options should automatically add the appropriate architecture include
directory to the path.
Distribution
------------
### Demo game
Seems you're not be able to get killed unless you go off the top or bottom of
the screen? In particular, you cannot collide with a bar?
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,50 +133,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.
### 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

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
from os.path import realpath, dirname, join
import sys
@ -16,23 +16,24 @@ 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, load_program, merge_programs
from sixtypical.analyzer import Analyzer
from sixtypical.callgraph import construct_callgraph, prune_unreachable_routines
from sixtypical.outputter import outputter_class_for
from sixtypical.compiler import Compiler
def process_input_files(filenames, options):
symtab = SymbolTable()
include_path = options.include_path.split(':')
programs = []
for filename in options.filenames:
text = open(filename).read()
parser = Parser(symtab, text, filename)
program = load_program(filename, symtab, include_path, include_file=False)
if options.debug:
print(symtab)
program = parser.program()
programs.append(program)
if options.parse_only:
@ -46,25 +47,24 @@ def process_input_files(filenames, options):
analyzer.analyze_program(program)
finally:
if options.dump_exit_contexts:
sys.stdout.write(json.dumps(analyzer.exit_contexts_map, indent=4, sort_keys=True, separators=(',', ':')))
sys.stdout.write(json.dumps(analyzer.exit_contexts_map, indent=4, sort_keys=True, separators=(',', ': ')))
sys.stdout.write("\n")
callgraph = construct_callgraph(program)
if options.dump_callgraph:
sys.stdout.write(json.dumps(callgraph, indent=4, sort_keys=True, separators=(',', ': ')))
if options.prune_unreachable_routines:
program = prune_unreachable_routines(program, callgraph)
compilation_roster = None
if options.optimize_fallthru:
from sixtypical.fallthru import FallthruAnalyzer
def dump(data, label=None):
if not options.dump_fallthru_info:
return
if label:
sys.stdout.write("*** {}:\n".format(label))
sys.stdout.write(json.dumps(data, indent=4, sort_keys=True, separators=(',', ':')))
sys.stdout.write("\n")
fa = FallthruAnalyzer(symtab, debug=options.debug)
fa.analyze_program(program)
compilation_roster = fa.serialize()
dump(compilation_roster)
if options.dump_fallthru_info:
sys.stdout.write(json.dumps(compilation_roster, indent=4, sort_keys=True, separators=(',', ': ')))
if options.analyze_only or (options.output is None and not options.run_on):
return
@ -138,6 +138,12 @@ if __name__ == '__main__':
"Default: raw."
)
argparser.add_argument(
"--include-path", "-I", type=str, metavar='PATH', default='.',
help="A colon-separated list of directories in which to look for "
"files which are included during `include` directives."
)
argparser.add_argument(
"--analyze-only",
action="store_true",
@ -160,6 +166,17 @@ if __name__ == '__main__':
action="store_true",
help="Dump the ordered fallthru map, in JSON, to stdout after analyzing the program."
)
argparser.add_argument(
"--prune-unreachable-routines",
action="store_true",
help="Omit code for unreachable routines (as determined by the callgraph) "
"from the final output."
)
argparser.add_argument(
"--dump-callgraph",
action="store_true",
help="Dump the call graph, in JSON, to stdout after analyzing the program."
)
argparser.add_argument(
"--parse-only",
action="store_true",
@ -184,7 +201,7 @@ if __name__ == '__main__':
argparser.add_argument(
"--version",
action="version",
version="%(prog)s 0.19"
version="%(prog)s 0.21"
)
options, unknown = argparser.parse_known_args(sys.argv[1:])

View File

@ -1,6 +1,12 @@
6502 Opcodes
============
<!--
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
-->
As used or unused in SixtyPical.
### ld ###
@ -75,41 +81,41 @@ As used or unused in SixtyPical.
### if ###
if z → BEQ LABEL
if z → BEQ LABEL
if not z → BNE LABEL
if n → BMI LABEL
if not n → BPL LABEL
if not n → BPL LABEL
if c → BCS LABEL
if not c → BCC LABEL
if not c → BCC LABEL
if v → BVS LABEL
if not v → BVC LABEL
if not v → BVC LABEL
### 6502 instructions unsupported ###
ASL Shift Left One Bit (Memory or Accumulator)
LSR Shift Right One Bit (Memory or Accumulator)
ASL Shift Left One Bit (Memory or Accumulator)
LSR Shift Right One Bit (Memory or Accumulator)
BIT Test Bits in Memory with Accumulator
BRK Force Break
BIT Test Bits in Memory with Accumulator
BRK Force Break
CLD Clear Decimal Mode
CLI Clear interrupt Disable Bit
CLV Clear Overflow Flag
CLD Clear Decimal Mode
CLI Clear interrupt Disable Bit
CLV Clear Overflow Flag
NOP No Operation
NOP No Operation
JMP Jump to New Location // but may be generated as part of `if`
JMP Jump to New Location // but may be generated as part of `if`
PHA Push Accumulator on Stack
PHP Push Processor Status on Stack
PLA Pull Accumulator from Stack
PLP Pull Processor Status from Stack
PHA Push Accumulator on Stack
PHP Push Processor Status on Stack
PLA Pull Accumulator from Stack
PLP Pull Processor Status from Stack
RTI Return from Interrupt
RTS Return from Subroutine
RTI Return from Interrupt
RTS Return from Subroutine
SED Set Decimal Mode
SEI Set Interrupt Disable Status
TSX Transfer Stack Pointer to Index X
TXS Transfer Index X to Stack Pointer
SED Set Decimal Mode
SEI Set Interrupt Disable Status
TSX Transfer Stack Pointer to Index X
TXS Transfer Index X to Stack Pointer

View File

@ -1,6 +1,12 @@
Design Goals for SixtyPical
===========================
<!--
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
-->
(draft)
The intent of SixtyPical is to have a very low-level language that

View File

@ -0,0 +1,66 @@
Future directions for SixtyPical
================================
<!--
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
-->
[SixtyPical](https://codeberg.org/catseye/SixtyPical) has reached a mature
stage of development. There are small features that could be added, but
they are minor compared to the main features
(abstract-interpretation/symbolic-execution/flow-typing-based
static analysis and optimization of low-level 6502 programs).
So the question arises -- what would be next for SixtyPical?
It would be nice to port SixtyPical to work on a ISA other than the 6502 --
the Z80, for example. Or, a more practical choice would be the X86
architecture.
It would also be nice if it could somehow be made to work on regular
assembly language programs. It would still be acceptable if, in this
case, it only worked on programs written in a particular style, to be
compatible with SixtyPical (structured "for" loops, no arbitrary jumps,
and so forth).
It would also be nice to simply generalize the idea: a generic
low-level language with a generic set of registers
(global variables), the rules for which we can specify as part of
the program. The static analyzer then checks the program according to the rules.
It would also be nice to design a more formal theory behind all
this. While SixtyPical works well for what it does, much of it was
"unit-tested into existence" and the theories behind these parts are not made explicit.
In fact, I think we can combine all these ideas. (And, if these are
really all nice to have in a next version of SixtyPical -- we should.)
We can do this by splitting it up into a few different phases:
The first phase is a "frontend" that takes a 6502 assembly language program in a common
6502 assembly language format (perhaps with annotations, or perhaps accompanied by a
configuration file) and translated it into a program description in the generic language,
including the specification of the rules particular to the 6502.
There is then a generic analyzer which checks the program in the
generic language.
If all the checks pass, then our guarantees have been met and the
original assembly language program can simply be assembled, using any
assembler that can understand its format. (This approach is similar
to using a model checker.)
Unlike SixtyPical currently,
which is an optimizing translator, in this method there would be no
optimizations applied. But I think this is an acceptable trade-off.
It is especially acceptable if the assembly language input can be hand-optimized
and still be checked by the analyzer, but this would be trickier to accomplish.
Then, to support other architectures, one could define a similar "frontend"
which works on Z80 assembly code, or X86 assembly, or what have you.
In practice, it would probably be easiest to start with a
"frontend" which converts 6502 assembly to the existing SixtyPical
language. Then design and implement the generic language.
Then re-target the frontend to the generic language.

View File

@ -1,6 +1,12 @@
Output Formats
==============
<!--
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
-->
`sixtypical` can generate an output file in a number of formats.
### `raw`

View File

@ -1,7 +1,13 @@
SixtyPical
==========
This document describes the SixtyPical programming language version 0.19,
<!--
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
-->
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 +202,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 +665,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

@ -1,37 +1,26 @@
<!--
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
-->
This directory contains SixtyPical example programs, categorized
in subdirectories by machine architecture.
### rudiments
In the [rudiments](rudiments/) directory are programs which are not for
any particular machine, but meant to demonstrate the features of SixtyPical.
Some are meant to fail and produce an error message. Others can run on
any architecture where there is a routine at 65490 which outputs the value
of the accumulator as an ASCII character.
In the [rudiments](rudiments/) directory are programs which are
meant to demonstrate the elementary features of SixtyPical, and
to serve as manual integration test cases. See
[the README in that directory](rudiments/README.md) for details.
### c64
In the [c64](c64/) directory are programs that run on the Commodore 64.
The directory itself contains some simple demos, for example
[hearts.60p](c64/hearts.60p), while there are subdirectories for more
elaborate demos:
* [demo-game](c64/demo-game/): a little game-like program written as a
"can we write something you'd see in practice?" test case for SixtyPical.
* [ribos](c64/ribos/): a well-commented example of a C64 raster interrupt
routine. Originally written with the P65 assembler (which has since
been reborn as [Ophis][]).
The second version of Ribos has been translated to SixtyPical.
* [petulant](c64/petulant/): "The PETulant Cursor", a tiny (44 bytes)
"display hack". Originally written in the late 80's. Rewritten with
the P65 assembler (now Ophis) and re-released on April 1st, 2008 (a
hint as to its nature).
Translated to SixtyPical (in 2018), after adding some optimizations
to the SixtyPical compiler, the resulting executable is still 44 bytes!
elaborate demos, like the flagship demo game. See
[the README in that directory](c64/README.md) for details.
### vic20

View File

@ -1,3 +1,9 @@
<!--
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
-->
This directory contains SixtyPical example programs
specifically for the Apple II series of computers.

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
byte ds_graphics @ $C050
byte ds_text @ $C051
byte ds_full @ $C052

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
// Write ">AB>" to "standard output"
define cout routine

View File

@ -4,7 +4,9 @@
; plus an example of reading the joystick.
; By Chris Pressey, November 2, 2012.
;
; This work is in the public domain. See the file UNLICENSE for more info.
; SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
; For more information, please refer to <https://unlicense.org/>
; SPDX-License-Identifier: Unlicense
;
; Based on Chris Cracknell's Atari 2600 clock (also in the public domain):
; http://everything2.com/title/An+example+of+Atari+2600+source+code

View File

@ -1,6 +1,10 @@
// smiley.60p - SixtyPical translation of smiley.oph (2018),
// which is itself a stripped-down version of atari-2600-example.oph
// SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
byte VSYNC @ $00
byte VBLANK @ $01
byte WSYNC @ $02

View File

@ -2,7 +2,9 @@
; smiley.oph (2018)
; stripped-down version of atari-2600-example.oph (2012)
;
; This work is in the public domain. See the file UNLICENSE for more info.
; SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
; For more information, please refer to <https://unlicense.org/>
; SPDX-License-Identifier: Unlicense
;
; to build and run in Stella:
; ophis smiley.oph -o smiley.bin

View File

@ -1,5 +1,29 @@
<!--
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
-->
This directory contains SixtyPical example programs
specifically for the Commodore 64.
See the [README in the parent directory](../README.md) for
more information on these example programs.
There are subdirectories for more elaborate demos:
* [demo-game](demo-game/): a little game-like program written as a
"can we write something you'd see in practice?" test case for SixtyPical.
* [ribos](ribos/): a well-commented example of a C64 raster interrupt
routine. Originally written with the P65 assembler (which has since
been reborn as [Ophis][]).
The second version of Ribos has been translated to SixtyPical.
* [petulant](petulant/): "The PETulant Cursor", a tiny (44 bytes)
"display hack". Originally written in the late 80's. Rewritten with
the P65 assembler (now Ophis) and re-released on April 1st, 2008 (a
hint as to its nature).
Translated to SixtyPical (in 2018), after adding some optimizations
to the SixtyPical compiler, the resulting executable is still 44 bytes!
[Ophis]: http://michaelcmartin.github.io/Ophis/

View File

@ -2,6 +2,12 @@
// * Demo Game for SixtyPical *
// ****************************
// 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
include "joystick.60p"
// ----------------------------------------------------------------
// Type Definitions
// ----------------------------------------------------------------
@ -56,7 +62,6 @@ byte vic_border @ 53280
byte vic_bg @ 53281
byte table[2048] screen @ 1024
byte table[2048] colormap @ 55296
byte joy2 @ $dc00
// ----------------------------------------------------------------
// Global Variables
@ -69,7 +74,6 @@ word pos
word new_pos
word table[256] actor_delta
word delta
byte player_died
@ -103,71 +107,6 @@ vector game_state_routine
// Utility Routines
// ----------------------------------------------------------------
define read_stick routine
inputs joy2
outputs delta
trashes a, x, z, n
{
ld x, joy2
ld a, x
and a, 1 // up
if z {
copy $ffd8, delta // -40
} else {
ld a, x
and a, 2 // down
if z {
copy word 40, delta
} else {
ld a, x
and a, 4 // left
if z {
copy $ffff, delta // -1
} else {
ld a, x
and a, 8 // right
if z {
copy word 1, delta
} else {
copy word 0, delta
}
}
}
}
}
// You can repeatedly (i.e. as part of actor logic or an IRQ handler)
// call this routine.
// Upon return, if carry is set, the button was pressed then released.
define check_button routine
inputs joy2
outputs c
trashes a, z, n
static byte button_down : 0
{
ld a, button_down
if z {
ld a, joy2
and a, $10
if z {
ld a, 1
st a, button_down
}
st off, c
} else {
ld a, joy2
and a, $10
if not z {
ld a, 0
st a, button_down
st on, c
} else {
st off, c
}
}
}
define clear_screen routine
outputs screen, colormap
trashes a, y, c, n, z
@ -265,37 +204,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 +246,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
@ -424,7 +364,7 @@ define game_state_game_over game_state_routine
// * Main Game Loop Driver *
// *************************
define our_cinv game_state_routine
define our_cinv preserved game_state_routine
{
goto dispatch_game_state
}

23
eg/c64/demo-game/run.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/sh
# This script builds and runs the demo game. You need
# the VICE emulatore installed, in particular VICE's x64.
# You might want a `vicerc` file like the following:
# [C64]
# VICIIDoubleScan=0
# VICIIDoubleSize=0
# KeySet1NorthWest=0
# KeySet1North=273
# KeySet1NorthEast=0
# KeySet1East=275
# KeySet1SouthEast=0
# KeySet1South=274
# KeySet1SouthWest=0
# KeySet1West=276
# KeySet1Fire=306
# KeySetEnable=1
# JoyDevice1=0
# JoyDevice2=2
../../../bin/sixtypical --run-on x64 -I ../../../include/c64/ demo-game.60p

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
// Displays 256 hearts at the top of the Commodore 64's screen.
// Define where the screen starts in memory:

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
byte vic_border @ 53280
//

19
eg/c64/joystick-demo.60p Normal file
View File

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
include "joystick.60p"
word screen @ 1024
define main routine
inputs joy2
outputs delta
trashes a, x, z, n, screen
{
repeat {
call read_stick
copy delta, screen
ld a, 1
} until z
}

View File

@ -1,49 +0,0 @@
word screen @ 1024
byte joy2 @ $dc00
word delta
define read_stick routine
inputs joy2
outputs delta
trashes a, x, z, n
{
ld x, joy2
ld a, x
and a, 1 // up
if z {
copy $ffd8, delta // -40
} else {
ld a, x
and a, 2 // down
if z {
copy word 40, delta
} else {
ld a, x
and a, 4 // left
if z {
copy $ffff, delta // -1
} else {
ld a, x
and a, 8 // right
if z {
copy word 1, delta
} else {
copy word 0, delta
}
}
}
}
}
define main routine
inputs joy2
outputs delta
trashes a, x, z, n, screen
{
repeat {
call read_stick
copy delta, screen
ld a, 1
} until z
}

View File

@ -1,6 +1,12 @@
The PETulant Cursor
===================
<!--
SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
For more information, please refer to <https://unlicense.org/>
SPDX-License-Identifier: Unlicense
-->
This is a tiny (44 bytes long) machine-language demo for the Commodore 64,
somewhat in the style of later "display hacks" for the Amiga -- surprising
and silly ways to play with the user interface.
@ -10,8 +16,8 @@ So as not to not spoil the fun, try running it before reading the source.
To run it, make the file PETULANT.PRG accessible to your favourite Commodore
64 (or Commodore 64 emulator) in your favourite way, then
LOAD "PETULANT.PRG",8,1
SYS 679
LOAD "PETULANT.PRG",8,1
SYS 679
For further fun, try changing the text colour (hold the C= or CTRL key while
pressing a number) or the background colour (POKE 53281 with a number from
@ -27,6 +33,6 @@ makes it much more obvious when control has returned to BASIC immediate mode
Enjoy!
-Chris Pressey
April 1, 2008
Chicago, IL
-Chris Pressey
April 1, 2008
Chicago, IL

View File

@ -2,7 +2,10 @@
// Originally written by Chris Pressey sometime in the late 1980's
// Rewritten in P65 assembly and released March 2008
// Rewritten in SixtyPical in March 2018
// This work is part of the public domain.
// SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
// ----- Types -----

View File

@ -1,7 +1,10 @@
; petulant.p65 - The PETulant Cursor, a "display hack" for the Commodore 64
; Originally written by Chris Pressey sometime in the late 1980's
; Rewritten in P65 assembly and released March 2008
; This work is part of the public domain.
; SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
; For more information, please refer to <https://unlicense.org/>
; SPDX-License-Identifier: Unlicense
; ----- BEGIN petulant.p65 -----

View File

@ -1,6 +1,12 @@
Ribos
=====
<!--
SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
For more information, please refer to <https://unlicense.org/>
SPDX-License-Identifier: Unlicense
-->
This little demo is intended to be a well-commented example of how to
program a raster interrupt in 6502 assembly language on a Commodore 64.
@ -18,9 +24,9 @@ How to Run the Demo (using the VICE C64 emulator, x64)
1. Mount this project's directory as drive 8:
Make sure
Peripheral settings > Device #8 > Enable IEC Device
`Peripheral settings > Device #8 > Enable IEC Device`
is checked, then select
Peripheral settings > Device #8 > File system directory...
`Peripheral settings > Device #8 > File system directory...`
and enter the path to the project directory.
2. LOAD "RIBOS.PRG",8,1
@ -74,6 +80,6 @@ points. What I learned in the process is written into the comments.
Happy raster-interrupting!
-Chris Pressey
April 10, 2007
Vancouver, BC
-Chris Pressey
April 10, 2007
Vancouver, BC

View File

@ -2,8 +2,10 @@
; Demonstration of the VIC-II raster interrupt on the Commodore 64:
; Alter the border colour in the middle part of the screen only.
; Original (hardware IRQ vector) version.
; By Chris Pressey, Cat's Eye Technologies.
; This work has been placed in the public domain.
; SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
; For more information, please refer to <https://unlicense.org/>
; SPDX-License-Identifier: Unlicense
; ----- BEGIN ribos.p65 -----

View File

@ -2,8 +2,10 @@
// Demonstration of the VIC-II raster interrupt on the Commodore 64:
// Alter the border colour in the middle part of the screen only,
// Simplified (KERNAL IRQ vector) version.
// By Chris Pressey, Cat's Eye Technologies.
// This work has been placed in the public domain.
// SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
// For comments, see ribos2.p65.

View File

@ -2,8 +2,10 @@
; Demonstration of the VIC-II raster interrupt on the Commodore 64:
; Alter the border colour in the middle part of the screen only,
; Simplified (KERNAL IRQ vector) version.
; By Chris Pressey, Cat's Eye Technologies.
; This work has been placed in the public domain.
; SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
; For more information, please refer to <https://unlicense.org/>
; SPDX-License-Identifier: Unlicense
; ----- BEGIN ribos2.p65 -----

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
byte screen @ 1024
define main routine

View File

@ -1,17 +1,23 @@
<!--
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
-->
This directory contains example sources which demonstrate
the rudiments of SixtyPical.
Examples that are meant to fail and produce an error message
are in the `errorful/` subdirectory.
when being compiled are in the `errorful/` subdirectory.
These files are intended to be architecture-agnostic.
For the ones that do produce output, an appropriate source
under `support/` should be included first, so that system entry
points such as `chrout` are defined. In addition, some of these
programs use "standard" support modules, so those should be included
first too. For example:
The other sources are portable across architectures. They use
`include` directives to bring in architecture-dependent libraries
to produce output. Libraries for such are provided in the
architecture-specific subdirectories of the `include` directory
in the root directory of this repository; make sure it is on the
compiler's include search path. For example:
sixtypical --run-on=x64 support/c64.60p support/stdlib.60p vector-table.60p
sixtypical --run-on=x64 -I../../include/c64/:../../include/stdlib/ vector-table.60p
`chrout` is a routine with outputs the value of the accumulator
as an ASCII character, disturbing none of the other registers,

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print YY
include "chrout.60p"
word score
define main routine

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print Y
include "chrout.60p"
byte table[2048] buf
pointer ptr @ 254
byte foo
@ -11,10 +12,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

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print AA
include "chrout.60p"
define print routine
trashes a, z, n
{

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print ENGGL
include "chrout.60p"
byte b
define main routine

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print ENGGL
include "chrout.60p"
word w1
define main routine

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print ENGGL
include "chrout.60p"
word w1
word w2

View File

@ -1,7 +1,7 @@
// Demonstrates vector tables.
// Include `support/${PLATFORM}.60p` before this source
// Should print YN
include "chrout.60p"
define main routine
trashes a, x, y, z, n, c, v
{

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print YA
include "chrout.60p"
define main routine
trashes a, x, y, z, n, c, v
{

View File

@ -1,3 +1,9 @@
<!--
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
-->
This directory contains example SixtyPical sources which
are intentionally invalid (for demonstration purposes)
and are expected to elicit an error message from a

View File

@ -1,3 +1,8 @@
// should fail analysis with an UnmeaningfulReadError
// because adding 4 to the accumulator reads the carry
// but the carry has neither been provided as input
// nor set to anything in particular by this routine.
define add_four routine
inputs a
outputs a

View File

@ -1,3 +1,7 @@
// should fail analysis with a RangeExceededError
// because the index is detected to fall outside the
// allowable range of the table it is indexing.
byte table[8] message : "WHAT?"
define main routine

View File

@ -1,9 +1,14 @@
vector vec
// should fail analysis with a ConstantConstraintError
// because it cannot copy the address of `foo` into `vec`
// because it has incompatible constraints.
vector routine
inputs y
outputs y
trashes z, n
vec
routine foo
define foo routine
inputs x
outputs x
trashes z, n
@ -11,7 +16,7 @@ routine foo
inc x
}
routine main
define main routine
inputs foo
outputs vec
trashes a, z, n

View File

@ -1,6 +1,8 @@
// Include `support/${PLATFORM}.60p` and `support/stdlib.60p` before this source
// Should print 01
include "chrout.60p"
include "prbyte.60p"
byte lives
define main routine

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print AB
include "chrout.60p"
define bar routine trashes a, z, n {
ld a, 66
call chrout

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print ABCDEFGHIJKLMNOPQRSTUVWXYZ
include "chrout.60p"
define main routine
trashes a, y, z, n, c
{

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print AB
include "chrout.60p"
byte foo
define print routine

View File

@ -0,0 +1,34 @@
// Should print H (being ASCII 72 = 8 * 9)
include "chrout.60p"
// 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,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print A
include "chrout.60p"
define main routine
inputs a
trashes a, z, n

View File

@ -1,6 +0,0 @@
// Implementation of `chrout` for the Commodore 64 platform.
define chrout routine
inputs a
trashes a
@ 65490

View File

@ -1,6 +0,0 @@
// Implementation of `chrout` for the Commodore VIC-20 platform.
define chrout routine
inputs a
trashes a
@ 65490

View File

@ -1,7 +1,9 @@
// Demonstrates vector tables.
// Include `support/${PLATFORM}.60p` before this source
// Should print AABAB
// Demonstrates vector tables.
include "chrout.60p"
vector routine
trashes a, z, n
print

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print AB
include "chrout.60p"
vector routine
trashes a, z, n
print

View File

@ -1,6 +1,7 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print YY
include "chrout.60p"
word one
word table[256] many

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: Chris Pressey, the author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
// Displays 256 hearts at the top of the VIC-20's screen.
// Define where the screen starts in memory:

10
include/c64/chrout.60p Normal file
View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: Chris Pressey, the original author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
// Implementation of `chrout` for the Commodore 64 platform.
define chrout routine
inputs a
trashes a
@ 65490

75
include/c64/joystick.60p Normal file
View File

@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: Chris Pressey, the original author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
byte joy2 @ $dc00
word delta
// Read the joystick and compute the delta it represents
// in a row-based 40-column grid like the C64's screen.
define read_stick routine
inputs joy2
outputs delta
trashes a, x, z, n
{
ld x, joy2
ld a, x
and a, 1 // up
if z {
copy $ffd8, delta // -40
} else {
ld a, x
and a, 2 // down
if z {
copy word 40, delta
} else {
ld a, x
and a, 4 // left
if z {
copy $ffff, delta // -1
} else {
ld a, x
and a, 8 // right
if z {
copy word 1, delta
} else {
copy word 0, delta
}
}
}
}
}
// You can repeatedly (i.e. as part of actor logic or an IRQ handler)
// call this routine.
// Upon return, if carry is set, the button was pressed then released.
define check_button routine
inputs joy2
outputs c
trashes a, z, n
static byte button_down : 0
{
ld a, button_down
if z {
ld a, joy2
and a, $10
if z {
ld a, 1
st a, button_down
}
st off, c
} else {
ld a, joy2
and a, $10
if not z {
ld a, 0
st a, button_down
st on, c
} else {
st off, c
}
}
}

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: Chris Pressey, the original author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
byte table[16] hexchars : "0123456789ABCDEF"
define prbyte routine

10
include/vic20/chrout.60p Normal file
View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: Chris Pressey, the original author of this work, has dedicated it to the public domain.
// For more information, please refer to <https://unlicense.org/>
// SPDX-License-Identifier: Unlicense
// Implementation of `chrout` for the Commodore VIC-20 platform.
define chrout routine
inputs a
trashes a
@ 65490

View File

@ -1,8 +1,13 @@
# 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, 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 +85,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 +96,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):
@ -450,18 +145,32 @@ class Analyzer(object):
def analyze_program(self, program):
assert isinstance(program, Program)
for routine in program.routines:
context = self.analyze_routine(routine)
routine.called_routines = set()
context, type_ = self.analyze_routine(routine)
if type_:
routine.routine_type = type_
routine.encountered_gotos = list(context.encountered_gotos()) if context else []
routine.called_routines = list(routine.called_routines)
def analyze_routine(self, routine):
assert isinstance(routine, Routine)
type_ = self.get_type_for_name(routine.name)
if routine.block is None:
# it's an extern, that's fine
return None
return None, type_
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,12 +214,12 @@ 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
self.current_routine = None
return context
return context, type_
def analyze_block(self, block, context):
assert isinstance(block, Block)
@ -531,15 +240,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 +298,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 +467,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 +482,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 +495,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 +505,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)
@ -817,16 +522,19 @@ class Analyzer(object):
raise NotImplementedError(opcode)
def analyze_call(self, instr, context):
type = self.get_type(instr.location)
if not isinstance(type, (RoutineType, VectorType)):
type_ = self.get_type(instr.location)
if not isinstance(type_, (RoutineType, VectorType)):
raise TypeMismatchError(instr, instr.location.name)
if isinstance(type, VectorType):
type = type.of_type
for ref in type.inputs:
self.current_routine.called_routines.add((instr.location, type_))
if isinstance(type_, VectorType):
type_ = type_.of_type
for ref in type_.inputs:
context.assert_meaningful(ref)
for ref in type.outputs:
for ref in type_.outputs:
context.set_written(ref)
for ref in type.trashes:
for ref in type_.trashes:
context.assert_writeable(ref)
context.set_touched(ref)
context.set_unmeaningful(ref)
@ -838,6 +546,8 @@ class Analyzer(object):
if not isinstance(type_, (RoutineType, VectorType)):
raise TypeMismatchError(instr, location.name)
self.current_routine.called_routines.add((instr.location, type_))
# assert that the dest routine's inputs are all initialized
if isinstance(type_, VectorType):
type_ = type_.of_type
@ -865,7 +575,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 +682,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 +723,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 +737,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

@ -1,3 +1,7 @@
# 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
class AST(object):
@ -59,7 +63,7 @@ class Defn(AST):
class Routine(AST):
value_attrs = ('name', 'addr', 'initial',)
children_attrs = ('statics',)
children_attrs = ('locals',)
child_attrs = ('block',)
@ -75,6 +79,10 @@ class SingleOp(Instr):
value_attrs = ('opcode', 'dest', 'src',)
class Reset(Instr):
value_attrs = ('pointer', 'offset',)
class Call(Instr):
value_attrs = ('location',)

View File

@ -0,0 +1,72 @@
# 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
from sixtypical.ast import Program
from sixtypical.model import RoutineType, VectorType
def find_routines_matching_type(program, type_):
"""Return the subset of routines of the program whose value
may be assigned to a location of the given type_ (typically
a vector)."""
return [r for r in program.routines if RoutineType.executable_types_compatible(r.routine_type, type_)]
def mark_as_reachable(graph, routine_name):
node = graph[routine_name]
if node.get('reachable', False):
return
node['reachable'] = True
for next_routine_name in node['potentially-calls']:
mark_as_reachable(graph, next_routine_name)
def construct_callgraph(program):
graph = {}
for routine in program.routines:
potentially_calls = []
for (called_routine, called_routine_type) in routine.called_routines:
if isinstance(called_routine_type, RoutineType):
potentially_calls.append(called_routine.name)
elif isinstance(called_routine_type, VectorType):
for potentially_called in find_routines_matching_type(program, called_routine_type):
potentially_calls.append(potentially_called.name)
else:
raise NotImplementedError
graph[routine.name] = {
'potentially-calls': potentially_calls,
}
# Symmetric closure
# (Note, this information isn't used anywhere yet)
for routine in program.routines:
potentially_called_by = []
for (name, node) in graph.items():
potential_calls = node['potentially-calls']
if routine.name in potential_calls:
potentially_called_by.append(name)
graph[routine.name]['potentially-called-by'] = potentially_called_by
# Root set
root_set = set()
for routine in program.routines:
if getattr(routine, 'preserved', False) or routine.name == 'main':
root_set.add(routine)
# Reachability
for routine in root_set:
mark_as_reachable(graph, routine.name)
return graph
def prune_unreachable_routines(program, callgraph):
return Program(1, defns=program.defns, routines=[
r for r in program.routines if callgraph[r.name].get('reachable', False)
])

View File

@ -1,7 +1,11 @@
# 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, 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 +38,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 +81,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,25 +112,25 @@ 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']
for roster_row in compilation_roster:
for routine_name in roster_row[0:-1]:
self.compile_routine(self.routines[routine_name], skip_final_goto=True)
routine_name = roster_row[-1]
self.compile_routine(self.routines[routine_name])
for i, routine_name in enumerate(roster_row):
if i < len(roster_row) - 1:
self.compile_routine(self.routines[routine_name], next_routine=self.routines[roster_row[i + 1]])
else:
self.compile_routine(self.routines[routine_name])
for location, label in self.trampolines.items():
self.emitter.resolve_label(label)
@ -155,23 +160,45 @@ class Compiler(object):
if defn.initial is None and defn.addr is None:
self.emitter.resolve_bss_label(label)
def compile_routine(self, routine, skip_final_goto=False):
self.current_routine = routine
self.skip_final_goto = skip_final_goto
self.final_goto_seen = False
def compile_routine(self, routine, next_routine=None):
assert isinstance(routine, Routine)
self.current_routine = routine
if routine.block:
self.emitter.resolve_label(self.get_label(routine.name))
self.compile_block(routine.block)
if not self.final_goto_seen:
needs_rts = True
last_op = self.emitter.get_tail()
if isinstance(last_op, JSR):
if isinstance(last_op.operand, Absolute):
if isinstance(last_op.operand.value, Label):
label = last_op.operand.value
self.emitter.retract()
self.emitter.emit(JMP(Absolute(label)))
last_op = self.emitter.get_tail()
if isinstance(last_op, JMP):
needs_rts = False
if isinstance(last_op.operand, Absolute):
if isinstance(last_op.operand.value, Label):
if next_routine and last_op.operand.value.name == next_routine.name:
self.emitter.retract()
if needs_rts:
self.emitter.emit(RTS())
self.current_routine = None
self.skip_final_goto = False
def compile_block(self, block):
assert isinstance(block, Block)
block.shallow_contains_goto = False
for instr in block.instrs:
self.compile_instr(instr)
if isinstance(instr, GoTo):
block.shallow_contains_goto = True
def compile_instr(self, instr):
if isinstance(instr, SingleOp):
@ -192,6 +219,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
@ -435,19 +464,15 @@ class Compiler(object):
raise NotImplementedError(location_type)
def compile_goto(self, instr):
self.final_goto_seen = True
if self.skip_final_goto:
pass
location = instr.location
label = self.get_label(instr.location.name)
location_type = self.get_type(location)
if isinstance(location_type, RoutineType):
self.emitter.emit(JMP(Absolute(label)))
elif isinstance(location_type, VectorType):
self.emitter.emit(JMP(Indirect(label)))
else:
location = instr.location
label = self.get_label(instr.location.name)
location_type = self.get_type(location)
if isinstance(location_type, RoutineType):
self.emitter.emit(JMP(Absolute(label)))
elif isinstance(location_type, VectorType):
self.emitter.emit(JMP(Indirect(label)))
else:
raise NotImplementedError(location_type)
raise NotImplementedError(location_type)
def compile_cmp(self, instr, src, dest):
"""`instr` is only for reporting purposes"""
@ -660,12 +685,17 @@ class Compiler(object):
else_label = Label('else_label')
self.emitter.emit(cls(Relative(else_label)))
self.compile_block(instr.block1)
if instr.block2 is not None:
end_label = Label('end_label')
self.emitter.emit(JMP(Absolute(end_label)))
self.emitter.resolve_label(else_label)
self.compile_block(instr.block2)
self.emitter.resolve_label(end_label)
if instr.block1.shallow_contains_goto:
self.emitter.resolve_label(else_label)
self.compile_block(instr.block2)
else:
end_label = Label('end_label')
self.emitter.emit(JMP(Absolute(end_label)))
self.emitter.resolve_label(else_label)
self.compile_block(instr.block2)
self.emitter.resolve_label(end_label)
else:
self.emitter.resolve_label(else_label)
@ -742,12 +772,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)

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

@ -0,0 +1,332 @@
# 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.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

@ -1,3 +1,7 @@
# 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
"""Binary machine code emitter. Used in SixtyPical to emit 6502 machine code,
but not specific to SixtyPical, or 6502. Not even necessarily machine code -
though some parts are written around the assumptions of 8-bit architectures."""
@ -128,7 +132,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 +147,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):
@ -171,6 +175,16 @@ class Emitter(object):
self.accum.append(thing)
self.addr += thing.size()
def get_tail(self):
if self.accum:
return self.accum[-1]
else:
return None
def retract(self):
thing = self.accum.pop()
self.addr -= thing.size()
def serialize_to(self, stream):
"""`stream` should be a file opened in binary mode."""
addr = self.start_addr

View File

@ -1,3 +1,7 @@
# 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 copy import copy

View File

@ -1,3 +1,7 @@
# 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
"""Emittables for 6502 machine code."""
from sixtypical.emitter import Emittable, Byte, Label, Offset, LowAddressByte, HighAddressByte

View File

@ -1,3 +1,7 @@
# 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
"""Data/storage model for SixtyPical."""
from collections import namedtuple

View File

@ -1,3 +1,7 @@
# 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
"""Executable file writer."""
from sixtypical.emitter import Emitter, Byte, Word

View File

@ -1,23 +1,19 @@
# 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, 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,45 +24,10 @@ 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):
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
@ -76,7 +37,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 +49,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
@ -138,6 +101,12 @@ class Parser(object):
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()
@ -153,13 +122,20 @@ class Parser(object):
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
@ -306,17 +282,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 +346,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 +406,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
@ -498,6 +484,19 @@ class Parser(object):
# - - - -
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."""

View File

@ -1,3 +1,7 @@
# 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
import re

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

@ -0,0 +1,54 @@
# 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.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

17
test.sh
View File

@ -1,7 +1,16 @@
#!/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.
# Note also that the `sixtypical-py2.7.md` appliance, in the same directory as
# `sixtypical.md`, can be used to run the tests under Python 2.7.
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 Callgraph.md" \
"tests/SixtyPical Compilation.md"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,229 @@
SixtyPical Callgraph
====================
<!--
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
-->
This is a test suite, written in [Falderal][] format, for the ability of
a SixtyPical analyzer to construct a callgraph of which routines call which
other routines, and its ability to discover which routines will never be
called.
[Falderal]: http://catseye.tc/node/Falderal
-> Tests for functionality "Dump callgraph info for SixtyPical program"
The `main` routine is always called. The thing that it will
be called by is the system, but the callgraph analyzer simply
considers it to be "reachable".
| define main routine
| {
| }
= {
= "main": {
= "potentially-called-by": [],
= "potentially-calls": [],
= "reachable": true
= }
= }
If a routine is called by another routine, this fact will be noted.
If it is reachable (directly or indirectly) from `main`, this will
be noted as well.
| define main routine
| {
| call other
| }
|
| define other routine
| {
| }
= {
= "main": {
= "potentially-called-by": [],
= "potentially-calls": [
= "other"
= ],
= "reachable": true
= },
= "other": {
= "potentially-called-by": [
= "main"
= ],
= "potentially-calls": [],
= "reachable": true
= }
= }
If a routine is not potentially called by any other routine that is
ultimately potentially called by `main`, this absence will be noted
— the routine will not be considered reachable — and a compiler or
linker will be permitted to omit it from the final executable.
| define main routine
| {
| }
|
| define other routine
| {
| }
= {
= "main": {
= "potentially-called-by": [],
= "potentially-calls": [],
= "reachable": true
= },
= "other": {
= "potentially-called-by": [],
= "potentially-calls": []
= }
= }
If a routine is not called by another routine, but it is declared
explicitly as `preserved`, then it will still be considered
reachable, and a compiler or linker will not be permitted to omit it
from the final executable. This is useful for interrupt routines
and such that really are used by some part of the system, even if
not directly by another SixtyPical routine.
| define main routine
| {
| }
|
| define other preserved routine
| {
| }
= {
= "main": {
= "potentially-called-by": [],
= "potentially-calls": [],
= "reachable": true
= },
= "other": {
= "potentially-called-by": [],
= "potentially-calls": [],
= "reachable": true
= }
= }
If a routine is called from a preserved routine, that routine is
reachable.
| define main routine
| {
| }
|
| define other1 preserved routine
| {
| call other2
| }
|
| define other2 preserved routine
| {
| }
= {
= "main": {
= "potentially-called-by": [],
= "potentially-calls": [],
= "reachable": true
= },
= "other1": {
= "potentially-called-by": [],
= "potentially-calls": [
= "other2"
= ],
= "reachable": true
= },
= "other2": {
= "potentially-called-by": [
= "other1"
= ],
= "potentially-calls": [],
= "reachable": true
= }
= }
If a group of routines potentially call each other, but neither is
found to be reachable (directly or indirectly) from `main` or a
`preserved` routine, the routines in the group will not be considered
reachable.
| define main routine
| {
| }
|
| define other1 routine
| {
| call other2
| }
|
| define other2 routine
| {
| call other1
| }
= {
= "main": {
= "potentially-called-by": [],
= "potentially-calls": [],
= "reachable": true
= },
= "other1": {
= "potentially-called-by": [
= "other2"
= ],
= "potentially-calls": [
= "other2"
= ]
= },
= "other2": {
= "potentially-called-by": [
= "other1"
= ],
= "potentially-calls": [
= "other1"
= ]
= }
= }
-> Tests for functionality "Compile SixtyPical program with unreachable routine removal"
Basic test for actually removing unreachable routines from the resulting
executable when compiling SixtyPical programs.
| define main routine outputs a trashes z, n
| {
| ld a, 100
| }
|
| define other1 routine
| {
| call other2
| }
|
| define other2 routine
| {
| call other1
| }
= $080D LDA #$64
= $080F RTS
Test that marking routine as `preserved` preserves it in the output.
| define main routine outputs a trashes z, n
| {
| ld a, 100
| }
|
| define other preserved routine outputs a trashes z, n
| {
| ld a, 5
| }
= $080D LDA #$64
= $080F RTS
= $0810 LDA #$05
= $0812 RTS

View File

@ -1,14 +1,17 @@
SixtyPical Compilation
======================
<!--
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
-->
This is a test suite, written in [Falderal][] format, for compiling
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.
@ -54,10 +57,12 @@ Call extern.
| {
| ld a, 65
| call chrout
| ld a, 0
| }
= $080D LDA #$41
= $080F JSR $FFD2
= $0812 RTS
= $0812 LDA #$00
= $0814 RTS
Call defined routine.
@ -74,13 +79,39 @@ Call defined routine.
| trashes a, x, y, z, n
| {
| call foo
| ld a, 1
| }
= $080D JSR $0811
= $0810 RTS
= $0811 LDA #$00
= $0813 LDX #$00
= $0815 LDY #$00
= $0817 RTS
= $080D JSR $0813
= $0810 LDA #$01
= $0812 RTS
= $0813 LDA #$00
= $0815 LDX #$00
= $0817 LDY #$00
= $0819 RTS
Tail call is optimized into a jump.
| define foo routine
| outputs a, x, y
| trashes z, n
| {
| ld a, 0
| ld x, 0
| ld y, 0
| }
|
| define main routine
| trashes a, x, y, z, n
| {
| ld a, 1
| call foo
| }
= $080D LDA #$01
= $080F JMP $0812
= $0812 LDA #$00
= $0814 LDX #$00
= $0816 LDY #$00
= $0818 RTS
Access a defined memory location.
@ -613,7 +644,6 @@ Compiling `repeat forever`.
= $080D LDY #$41
= $080F INY
= $0810 JMP $080F
= $0813 RTS
The body of `repeat forever` can be empty.
@ -623,7 +653,6 @@ The body of `repeat forever` can be empty.
| } forever
| }
= $080D JMP $080D
= $0810 RTS
Compiling `for ... up to`.
@ -1058,7 +1087,7 @@ Copy word to word table and back, with constant offsets.
= $0848 STA $084D
= $084B RTS
Indirect call.
Indirect call. TODO: we don't need the final RTS here, omit it.
| vector routine
| outputs x
@ -1079,16 +1108,15 @@ Indirect call.
| copy bar, foo
| call foo
| }
= $080D LDA #$1B
= $080F STA $0822
= $080D LDA #$1A
= $080F STA $0821
= $0812 LDA #$08
= $0814 STA $0823
= $0817 JSR $081E
= $081A RTS
= $081B LDX #$C8
= $081D RTS
= $081E JMP ($0822)
= $0821 RTS
= $0814 STA $0822
= $0817 JMP $081D
= $081A LDX #$C8
= $081C RTS
= $081D JMP ($0821)
= $0820 RTS
Compiling `goto`. Note that no `RTS` is emitted after the `JMP`.
@ -1142,28 +1170,27 @@ Copying to and from a vector table.
| call one
| }
= $080D LDX #$00
= $080F LDA #$3F
= $0811 STA $0846
= $080F LDA #$3E
= $0811 STA $0845
= $0814 LDA #$08
= $0816 STA $0847
= $0819 LDA #$3F
= $081B STA $0848,X
= $0816 STA $0846
= $0819 LDA #$3E
= $081B STA $0847,X
= $081E LDA #$08
= $0820 STA $0948,X
= $0823 LDA $0846
= $0826 STA $0848,X
= $0829 LDA $0847
= $082C STA $0948,X
= $082F LDA $0848,X
= $0832 STA $0846
= $0835 LDA $0948,X
= $0838 STA $0847
= $083B JSR $0842
= $083E RTS
= $083F LDX #$C8
= $0841 RTS
= $0842 JMP ($0846)
= $0845 RTS
= $0820 STA $0947,X
= $0823 LDA $0845
= $0826 STA $0847,X
= $0829 LDA $0846
= $082C STA $0947,X
= $082F LDA $0847,X
= $0832 STA $0845
= $0835 LDA $0947,X
= $0838 STA $0846
= $083B JMP $0841
= $083E LDX #$C8
= $0840 RTS
= $0841 JMP ($0845)
= $0844 RTS
Copying to and from a vector table, with constant offsets.
@ -1193,28 +1220,27 @@ Copying to and from a vector table, with constant offsets.
| call one
| }
= $080D LDX #$00
= $080F LDA #$3F
= $0811 STA $0846
= $080F LDA #$3E
= $0811 STA $0845
= $0814 LDA #$08
= $0816 STA $0847
= $0819 LDA #$3F
= $081B STA $0849,X
= $0816 STA $0846
= $0819 LDA #$3E
= $081B STA $0848,X
= $081E LDA #$08
= $0820 STA $0949,X
= $0823 LDA $0846
= $0826 STA $084A,X
= $0829 LDA $0847
= $082C STA $094A,X
= $082F LDA $084B,X
= $0832 STA $0846
= $0835 LDA $094B,X
= $0838 STA $0847
= $083B JSR $0842
= $083E RTS
= $083F LDX #$C8
= $0841 RTS
= $0842 JMP ($0846)
= $0845 RTS
= $0820 STA $0948,X
= $0823 LDA $0845
= $0826 STA $0849,X
= $0829 LDA $0846
= $082C STA $0949,X
= $082F LDA $084A,X
= $0832 STA $0845
= $0835 LDA $094A,X
= $0838 STA $0846
= $083B JMP $0841
= $083E LDX #$C8
= $0840 RTS
= $0841 JMP ($0845)
= $0844 RTS
### add, sub
@ -1400,7 +1426,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 +1441,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 +1477,7 @@ Write literal through a pointer.
| {
| ld y, 0
| point ptr into tab {
| reset ptr 0
| copy 123, [ptr] + y
| }
| }
@ -1458,6 +1503,7 @@ Write stored value through a pointer.
| {
| ld y, 0
| point ptr into tab {
| reset ptr 0
| copy foo, [ptr] + y
| }
| }
@ -1483,6 +1529,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 +1544,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 +1590,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 +1623,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 +1654,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 +1703,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
@ -1642,12 +1726,72 @@ just the same as initialized global storage locations are.
| ld x, t
| call foo
| }
= $080D LDX $081F
= $0810 JSR $0814
= $0813 RTS
= $0814 STX $081E
= $0817 INC $081E
= $081A LDX $081E
= $081D RTS
= $081E .byte $FF
= $081F .byte $07
= $080D LDX $081E
= $0810 JMP $0813
= $0813 STX $081D
= $0816 INC $081D
= $0819 LDX $081D
= $081C RTS
= $081D .byte $FF
= $081E .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 $0820
= $0812 JMP $0815
= $0815 STX $081F
= $0818 INC $081F
= $081B LDX $081F
= $081E 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 JMP $0815
= $0815 STX $0400
= $0818 INC $0400
= $081B LDX $0400
= $081E RTS

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,12 @@
SixtyPical Fallthru
===================
<!--
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
-->
This is a test suite, written in [Falderal][] format, for SixtyPical's
ability to detect which routines make tail calls to other routines,
and thus can be re-arranged to simply "fall through" to them.
@ -61,12 +67,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
@ -390,11 +390,6 @@ It can optimize out one of the `goto`s if they are the same.
It cannot optimize out the `goto`s if they are different.
Note, this currently produces unfortunately unoptimized code,
because generating code for the "true" branch of an `if` always
generates a jump out of the `if`, even if the last instruction
in the "true" branch is a `goto`.
| define foo routine trashes a, z, n
| {
| ld a, 0
@ -417,11 +412,10 @@ in the "true" branch is a `goto`.
| }
= $080D RTS
= $080E LDA #$00
= $0810 BNE $081A
= $0810 BNE $0817
= $0812 LDA #$01
= $0814 JMP $081F
= $0817 JMP $081F
= $081A LDA #$02
= $081C JMP $080D
= $081F LDA #$FF
= $0821 RTS
= $0814 JMP $081C
= $0817 LDA #$02
= $0819 JMP $080D
= $081C LDA #$FF
= $081E RTS

1990
tests/SixtyPical Storage.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,12 @@
SixtyPical Syntax
=================
<!--
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
-->
This is a test suite, written in [Falderal][] format, for the syntax of
the Sixtypical language, disgregarding execution, static analysis, etc.
@ -9,9 +15,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.
@ -76,6 +79,18 @@ Extern routines
| @ 65487
= ok
Preserved routine.
| define main routine {
| ld a, $ff
| add a, $01
| }
| define foo preserved routine {
| ld a, 0
| add a, 1
| }
= ok
Trash.
| define main routine {
@ -176,6 +191,7 @@ Other blocks.
| ld a, 0
| }
| point ptr into tab {
| reset ptr 0
| ld a, [ptr] + y
| }
| }
@ -681,6 +697,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 +763,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 +780,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 +801,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 +851,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 +863,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

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# script that allows the binary output of sixtypical --output-format=c64-basic-prg --compile to be
# disassembled by https://github.com/tcarmelveilleux/dcc6502
@ -8,7 +8,10 @@ import re
from subprocess import check_output
from tempfile import NamedTemporaryFile
bytes = sys.stdin.read()
try:
bytes = sys.stdin.buffer.read()
except AttributeError:
bytes = sys.stdin.read()
bytes = bytes[14:]
@ -17,7 +20,8 @@ filename = f.name
f.write(bytes)
f.close()
lines = [line for line in check_output("dcc6502 -o 2061 {}".format(filename), shell=True).split('\n') if line and not line.startswith(';')]
output = check_output("dcc6502 -o 2061 {}".format(filename), shell=True)
output_lines = output.decode('utf-8').split('\n')
lines = [line for line in output_lines if line and not line.startswith(';')]
lines = [re.sub(r'\s*\;.*$', '', line) for line in lines]
lines.pop()
sys.stdout.write('\n'.join(lines))

View File

@ -0,0 +1,30 @@
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,
and additionally that `sixtypical` is running under Python 2.7.
NOTE that this is not well-supported anymore, given that Python 2.7 is past
end-of-life.
[Falderal]: http://catseye.tc/node/Falderal
-> Functionality "Check syntax of SixtyPical program" is implemented by
-> shell command "python2.7 bin/sixtypical --parse-only --traceback %(test-body-file) && echo ok"
-> Functionality "Analyze SixtyPical program" is implemented by
-> shell command "python2.7 bin/sixtypical --analyze-only --traceback %(test-body-file) && echo ok"
-> Functionality "Compile SixtyPical program" is implemented by
-> shell command "python2.7 bin/sixtypical --output-format=c64-basic-prg --traceback %(test-body-file) --output /tmp/foo && python2.7 tests/appliances/bin/dcc6502-adapter </tmp/foo"
-> Functionality "Dump callgraph info for SixtyPical program" is implemented by
-> shell command "python2.7 bin/sixtypical --dump-callgraph --analyze-only --traceback %(test-body-file)"
-> Functionality "Compile SixtyPical program with unreachable routine removal" is implemented by
-> shell command "python2.7 bin/sixtypical --output-format=c64-basic-prg --prune-unreachable-routines --traceback %(test-body-file) --output /tmp/foo && python2.7 tests/appliances/bin/dcc6502-adapter </tmp/foo"
-> Functionality "Dump fallthru info for SixtyPical program" is implemented by
-> shell command "python2.7 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 "python2.7 bin/sixtypical --output-format=c64-basic-prg --optimize-fallthru --traceback %(test-body-file) --output /tmp/foo && python2.7 tests/appliances/bin/dcc6502-adapter </tmp/foo"

View File

@ -0,0 +1,26 @@
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 callgraph info for SixtyPical program" is implemented by
-> shell command "bin/sixtypical --dump-callgraph --analyze-only --traceback %(test-body-file)"
-> Functionality "Compile SixtyPical program with unreachable routine removal" is implemented by
-> shell command "bin/sixtypical --output-format=c64-basic-prg --prune-unreachable-routines --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"