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

Compare commits

...

78 Commits
0.18 ... 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
Chris Pressey
1c7efb019d
Merge pull request #20 from catseye/develop-0.19
Develop 0.19
2019-04-16 12:35:59 +01:00
Chris Pressey
0c65954bc5 Updates to README. Fix awkward punctuation in usage message. 2019-04-16 11:37:46 +01:00
Chris Pressey
a10f1c6528 Replace --run option with terser --run-on option. 2019-04-16 10:35:59 +01:00
Chris Pressey
4d82e2352e Merge branch 'develop-0.19' into reform-loadngo 2019-04-15 17:39:09 +01:00
Chris Pressey
c246424930 Fix bug raising InconsistentExitError, and type-bug in ribos2.60p. 2019-04-15 17:35:17 +01:00
Chris Pressey
6d867867fe Remove the "local" loadngo script from here as well. 2019-04-15 13:16:07 +01:00
Chris Pressey
a44b007ff0 Declare that --run replaces loadngo.sh, and remove the latter. 2019-04-15 13:11:43 +01:00
Chris Pressey
ce8e83908b First cut at a --run option for sixtypical, replacing loadngo.sh. 2019-04-11 16:53:43 +01:00
Chris Pressey
04a9438898 The VICE emulators just keep going if they can't find the vicerc. 2019-04-10 16:53:01 +01:00
Chris Pressey
c906ab7817 A few further small edits to README. 2019-04-10 12:29:59 +01:00
Chris Pressey
1ca5cb0336 You could argue that it's not *that* low level, so, okay. 2019-04-10 12:23:50 +01:00
Chris Pressey
7854f71706 More edits to docs. 2019-04-10 12:02:35 +01:00
Chris Pressey
652ab1dc5c Clean up ArgumentParser usage message, add --version argument. 2019-04-10 10:42:50 +01:00
Chris Pressey
21a187a105 Update docs. 2019-04-10 09:29:45 +01:00
Chris Pressey
8d6e5e090d The Type and Ref class hierarchies are now namedtuples. 2019-04-10 08:50:13 +01:00
Chris Pressey
a0328b8840 Store type information in SymbolTable shared across phases. 2019-04-10 08:48:33 +01:00
Chris Pressey
394fbddad6 Refactor to avoid storing LocationRefs in SymEntry. 2019-04-09 09:42:22 +01:00
Chris Pressey
4615d8d054 Distinct AST nodes for call and goto instructions. 2019-04-08 16:26:51 +01:00
Chris Pressey
bd462d6d8b Include more info in --dump-exit-contexts. 2019-04-08 12:23:44 +01:00
Chris Pressey
b19267d3ba Checkpoint import of changes for version 0.19. 2019-04-08 11:50:54 +01:00
91 changed files with 7249 additions and 4755 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,94 @@
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
----
* A `table` may be defined with more than 256 entries, even
though the conventional index syntax can only refer to the
first 256 entries.
* A `pointer` may point inside values of type `byte table`,
allowing access to entries beyond the 256th.
* `buffer` types have been eliminated from the language,
as the above two improvements allow `byte table`s to
do everything `buffer`s previously did.
* When accessing a table with an index, a constant offset
can also be given.
* Accessing a `table` through a `pointer` must be done in
the context of a `point ... into` block. This allows the
analyzer to check *which* table is being accessed.
* Refactored compiler internals so that type information
is stored in a single symbol table shared by all phases.
* Refactored internal data structures that represent
references and types to be immutable `namedtuple`s.
* Added `--dump-exit-contexts` option to `sixtypical`.
* Added a new `--run-on=<emu>` option to `sixtypical`, which
replaces the old `loadngo.sh` script.
0.18
----

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/>

174
README.md
View File

@ -1,14 +1,58 @@
SixtyPical
==========
_Version 0.18. 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 programming language with advanced
static analysis. Many of its primitive instructions resemble
those of the 6502 CPU — in fact it is intended to be compiled to
6502 machine code — but along with these instructions are
constructs which ease structuring and analyzing the code. The
language aims to fill this niche:
_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.
Quick Start
-----------
Make sure you have Python (2.7 or 3.5+) installed. Then
clone this repository and put its `bin` directory on your
executable search path. Then you can run:
sixtypical
If you have the [VICE][] emulator suite installed, you can run
sixtypical --run-on=x64 eg/c64/hearts.60p
and it will compile the [hearts.60p source code](eg/c64/hearts.60p) and
automatically start it in the `x64` emulator, and you should see:
![Screenshot of result of running hearts.60p](images/hearts.png?raw=true)
You can try `sixtypical --run-on` on other sources in the `eg` directory
tree, which contains more extensive examples, including an entire
game(-like program); see [eg/README.md](eg/README.md) for a listing.
Features
--------
SixtyPical aims to fill this niche:
* You'd use assembly, but you don't want to spend hours
debugging (say) a memory overrun that happened because of a
@ -19,7 +63,30 @@ language aims to fill this niche:
SixtyPical gives the programmer a coding regimen on par with assembly
language in terms of size and hands-on-ness, but also able to catch
many ridiculous silly errors at compile time, such as
many ridiculous silly errors at compile time.
### Low level
Many of SixtyPical's primitive instructions resemble those of the
[MOS Technology 6502][] — it is in fact intended to be compiled to 6502
machine code. However, it also provides some "higher-level" operations
based on common 8-bit machine-language programming idioms, including
* copying values from one register to another (via a third register when
there are no underlying instructions that directly support it)
* copying, adding, and comparing 16-bit values (done in two steps)
* explicit tail calls
* indirect subroutine calls
While a programmer will find these constructs convenient, their
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 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
@ -27,58 +94,69 @@ many ridiculous silly errors at compile time, such as
* you tried to write the address of something that was not a routine, to
a jump vector
Many of these checks are done with _abstract interpretation_, where we
go through the program step by step, tracking not just the changes that
happen during a _specific_ execution of the program, but _sets_ of changes
that could _possibly_ happen in any run of the program.
### Efficient code
SixtyPical also provides some convenient operations based on
machine-language programming idioms, such as
Unlike most conventional languages, in SixtyPical the programmer must manage
memory very explicitly, selecting the registers and memory locations to store
each piece of data in. So, unlike a C compiler such as [cc65][], a SixtyPical
compiler doesn't need to generate code to handle [calling conventions][] or
[register allocation][]. This results in smaller (and thus faster) programs.
* copying values from one register to another (via a third register when
there are no underlying instructions that directly support it); this
includes 16-bit values, which are copied in two steps
* explicit tail calls
* indirect subroutine calls
The flagship demo, a minigame for the Commodore 64, compiles to
a **930**-byte `.PRG` file.
### Target platforms
The reference implementation can analyze and compile SixtyPical programs to
6502 machine code formats which can run on several 6502-based 8-bit architectures:
* [Commodore 64][]
* [Commodore VIC-20][]
* [Atari 2600][]
* [Apple II series][]
For example programs for each of these, see [eg/README.md](eg/README.md).
Specification
-------------
SixtyPical is defined by a specification document, a set of test cases,
and a reference implementation written in Python 2. The reference
implementation can analyze and compile SixtyPical programs to 6502 machine
code, which can be run on several 6502-based 8-bit architectures:
and a reference implementation written in Python.
* Commodore 64
* Commodore VIC-20
* Atari 2600 VCS
* Apple II
There are over 400 test cases, written in [Falderal][] format for readability.
In order to run the tests for compilation, [dcc6502][] needs to be installed.
Quick Start
-----------
If you have the [VICE][] emulator installed, from this directory, you can run
./loadngo.sh c64 eg/c64/hearts.60p
and it will compile the [hearts.60p source code](eg/c64/hearts.60p) and
automatically start it in the `x64` emulator, and you should see:
![Screenshot of result of running hearts.60p](images/hearts.png?raw=true)
You can try the `loadngo.sh` script on other sources in the `eg` directory
tree, which contains more extensive examples, including an entire
game(-like program); see [eg/README.md](eg/README.md) for a listing.
[VICE]: http://vice-emu.sourceforge.net/
* [SixtyPical specification](doc/SixtyPical.md)
* [Literate test suite for SixtyPical syntax](tests/SixtyPical%20Syntax.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
-------------
* [Design Goals](doc/Design%20Goals.md)
* [SixtyPical specification](doc/SixtyPical.md)
* [SixtyPical revision history](HISTORY.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 compilation](tests/SixtyPical%20Compilation.md)
* [Literate test suite for SixtyPical fallthru optimization](tests/SixtyPical%20Fallthru.md)
* [6502 Opcodes used/not used in SixtyPical](doc/6502%20Opcodes.md)
* [Output formats supported by `sixtypical`](doc/Output%20Formats.md)
* [TODO](TODO.md)
[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/
[cc65]: https://cc65.github.io/
[Commodore 64]: https://en.wikipedia.org/wiki/Commodore_64
[Commodore VIC-20]: https://en.wikipedia.org/wiki/Commodore_VIC-20
[Atari 2600]: https://en.wikipedia.org/wiki/Atari_2600
[Apple II series]: https://en.wikipedia.org/wiki/Apple_II_series
[Falderal]: https://catseye.tc/node/Falderal
[dcc6502]: https://github.com/tcarmelveilleux/dcc6502

136
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,57 +18,120 @@ 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.
### Associate each pointer with the buffer it points into
### Copy byte to/from table
Check that the buffer being read or written to through pointer, appears in appropriate
inputs or outputs set.
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.
In the analysis, when we obtain a pointer, we need to record, in context, what buffer
that pointer came from.
### Character literals
When we write through that pointer, we need to set that buffer as written.
For goodness sake, let the programmer say `'A'` instead of `65`.
When we read through the pointer, we need to check that the buffer is readable.
### Character set mapping
### Table overlays
Not all computers think `'A'` should be `65`. Allow the character set to be
mapped. Probably copy what Ophis does.
They are uninitialized, but the twist is, the address is a buffer that is
an input to and/or output of the routine. So, they are defined (insofar
as the buffer is defined.)
### Pointers into non-byte tables
They are therefore a "view" of a section of a buffer.
Right now you cannot get a pointer into a non-byte (for instance, word or vector) table.
This is slightly dangerous since it does permit aliases: the buffer and the
table refer to the same memory.
Word and vector tables are stored as two byte tables in memory. This is useful for
indexed access, but makes pointer access more difficult.
Although, if they are `static`, you could say, in the routine in which they
are `static`, as soon as you've established one, you can no longer use the
buffer; and the ones you establish must be disjoint.
Laying them out for pointer access would make indexed access more difficult.
(That seems to be the most compelling case for restricting them to `static`.)
### Saving non-byte values
An alternative would be `static` pointers, which are currently not possible because
pointers must be zero-page, thus `@`, thus uninitialized.
Right now you cannot save a word value.
### Tail-call optimization
There doesn't seem to be a hugely pressing reason why not.
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.
Analysis
--------
As long as the routine has consistent type context every place it exits, that should be fine.
### Forbid recursion
### "Include" directives
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.)
Search a searchlist of include paths. And use them to make libraries of routines.
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.
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.
The problems only come up, I think, when a routine calls itself re-entrantly.
### Line numbers in analysis error messages
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.
We could associate every pointer variable with a specific table variable, in its
declaration. This makes some things simple, and would allow us to know what table a
pointer is supposed to point into, even if that pointer was passed into our routine.
One drawback is that it would limit each pointer to be used only on one table. Since a
pointer basically represents a zero-page location, and since those are a relatively scarce
resource, we would prefer if a single pointer could be used to point into different tables
at different times.
These can co-exist with general, non-specific-table-linked `pointer` variables.
If we have local pointers and space optimization for local non-statics, though,
these don't add as much.

View File

@ -1,9 +1,4 @@
#!/usr/bin/env python
"""Usage: sixtypical [OPTIONS] FILES
Analyzes and compiles a Sixtypical program.
"""
#!/usr/bin/env python3
from os.path import realpath, dirname, join
import sys
@ -12,29 +7,33 @@ sys.path.insert(0, join(dirname(realpath(sys.argv[0])), '..', 'src'))
# ----------------------------------------------------------------- #
import codecs
from argparse import ArgumentParser
import codecs
import json
from pprint import pprint
from subprocess import check_call
import sys
from tempfile import NamedTemporaryFile
import traceback
from sixtypical.parser import Parser, ParsingContext, 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):
context = ParsingContext()
symtab = SymbolTable()
include_path = options.include_path.split(':')
programs = []
for filename in options.filenames:
text = open(filename).read()
parser = Parser(context, text, filename)
program = load_program(filename, symtab, include_path, include_file=False)
if options.debug:
print(context)
program = parser.program()
print(symtab)
programs.append(program)
if options.parse_only:
@ -42,28 +41,32 @@ def process_input_files(filenames, options):
program = merge_programs(programs)
analyzer = Analyzer(debug=options.debug)
analyzer.analyze_program(program)
analyzer = Analyzer(symtab, debug=options.debug)
try:
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("\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):
import json
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(debug=options.debug)
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:
if options.analyze_only or (options.output is None and not options.run_on):
return
start_addr = None
@ -73,20 +76,46 @@ def process_input_files(filenames, options):
else:
start_addr = int(options.origin, 10)
with open(options.output, 'wb') as fh:
outputter = outputter_class_for(options.output_format)(fh, start_addr=start_addr)
outputter.write_prelude()
compiler = Compiler(outputter.emitter)
compiler.compile_program(program, compilation_roster=compilation_roster)
outputter.write_postlude()
if options.debug:
pprint(outputter.emitter)
else:
outputter.emitter.serialize_to(fh)
if options.run_on:
fh = NamedTemporaryFile(delete=False)
output_filename = fh.name
Outputter = outputter_class_for({
'x64': 'c64-basic-prg',
'xvic': 'vic20-basic-prg',
'stella': 'atari2600-cart',
}.get(options.run_on))
else:
fh = open(options.output, 'wb')
output_filename = options.output
Outputter = outputter_class_for(options.output_format)
outputter = Outputter(fh, start_addr=start_addr)
outputter.write_prelude()
compiler = Compiler(symtab, outputter.emitter)
compiler.compile_program(program, compilation_roster=compilation_roster)
outputter.write_postlude()
if options.debug:
pprint(outputter.emitter)
else:
outputter.emitter.serialize_to(fh)
fh.close()
if options.run_on:
emu = {
'x64': "x64 -config vicerc",
'xvic': "xvic -config vicerc",
'stella': "stella"
}.get(options.run_on)
if not emu:
raise ValueError("No emulator configured for selected --run-on '{}'".format(options.output_format))
command = "{} {}".format(emu, output_filename)
check_call(command, shell=True)
if __name__ == '__main__':
argparser = ArgumentParser(__doc__.strip())
argparser = ArgumentParser()
argparser.add_argument(
'filenames', metavar='FILENAME', type=str, nargs='+',
@ -109,11 +138,23 @@ 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",
help="Only parse and analyze the program; do not compile it."
)
argparser.add_argument(
"--dump-exit-contexts",
action="store_true",
help="Dump a map, in JSON, of the analysis context at each exit of each routine "
"after analyzing the program."
)
argparser.add_argument(
"--optimize-fallthru",
action="store_true",
@ -123,7 +164,18 @@ if __name__ == '__main__':
argparser.add_argument(
"--dump-fallthru-info",
action="store_true",
help="Dump the fallthru map and ordering to stdout after analyzing the program."
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",
@ -135,11 +187,22 @@ if __name__ == '__main__':
action="store_true",
help="Display debugging information when analyzing and compiling."
)
argparser.add_argument(
"--run-on", type=str, default=None,
help="If given, engage 'load-and-go' operation with the given emulator: write "
"the output to a temporary filename using an appropriate --output-format, "
"and boot the emulator with it. Options are: x64, xvic, stella."
)
argparser.add_argument(
"--traceback",
action="store_true",
help="When an error occurs, display a full Python traceback."
)
argparser.add_argument(
"--version",
action="version",
version="%(prog)s 0.21"
)
options, unknown = argparser.parse_known_args(sys.argv[1:])
remainder = ' '.join(unknown)

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.15,
<!--
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.)
@ -12,34 +18,55 @@ are even more normative.
Refer to the bottom of this document for an EBNF grammar of the syntax of
the language.
Types
-----
Data Model
----------
There are five *primitive types* in SixtyPical:
SixtyPical defines a data model where every value has some type
information associated with it. The values include those that are
directly manipulable by a SixtyPical program, but are not limited to them.
Type information includes not only what kind of structure the data has,
but other properties as well (sometimes called "type annotations".)
### Basic types ###
SixtyPical defines a handful of basic types. There are three types that
are "primitive" in that they are not parameterized in any way:
* bit (2 possible values)
* byte (256 possible values)
* word (65536 possible values)
* routine (code stored somewhere in memory, read-only)
* pointer (address of a byte in a buffer)
There are also three *type constructors*:
Types can also be parameterized and constructed from other types
(which is a kind of parameterization). One such type constructor is
* T table[N] (N entries, 1 ≤ N ≤ 256; each entry holds a value
of type T, where T is `byte`, `word`, or `vector`)
* buffer[N] (N entries; each entry is a byte; 1 ≤ N ≤ 65536)
* pointer (16-bit address of a byte inside a byte table)
* vector T (address of a value of type T; T must be a routine type)
### User-defined ###
Values of the above-listed types are directly manipulable by a SixtyPical
program. Other types describe values which can only be indirectly
manipulated by a program:
* routine (code stored somewhere in memory, read-only)
* T table[N] (series of 1 ≤ N ≤ 65536 values of type T)
There are some restrictions here; for example, a table may only
consist of `byte`, `word`, or `vector` types. A pointer may only
point to a byte inside a `table` of `byte` type.
Each routine is associated with a rich set of type information,
which is basically the types and statuses of memory locations that
have been declared as being relevant to that routine.
#### User-defined ####
A program may define its own types using the `typedef` feature. Typedefs
must occur before everything else in the program. A typedef takes a
type expression and an identifier which has not previously been used in
the program. It associates that identifer with that type. This is merely
a type alias; two types with different names will compare as equal.
a type alias; if two types have identical structure but different names,
they will compare as equal.
Memory locations
----------------
### Memory locations ###
A primary concept in SixtyPical is the *memory location*. At any given point
in time during execution, each memory location is either *uninitialized* or
@ -51,7 +78,7 @@ the program text; thus, it is a static property.
There are four general kinds of memory location. The first three are
pre-defined and built-in.
### Registers ###
#### Registers ####
Each of these hold a byte. They are initially uninitialized.
@ -59,7 +86,7 @@ Each of these hold a byte. They are initially uninitialized.
x
y
### Flags ###
#### Flags ####
Each of these hold a bit. They are initially uninitialized.
@ -68,7 +95,7 @@ Each of these hold a bit. They are initially uninitialized.
v (overflow)
n (negative)
### Constants ###
#### Constants ####
It may be strange to think of constants as memory locations, but keep in mind
that a memory location in SixtyPical need not map to a memory location in the
@ -97,7 +124,7 @@ and sixty-five thousand five hundred and thirty-six word constants,
Note that if a word constant is between 256 and 65535, the leading `word`
token can be omitted.
### User-defined ###
#### User-defined ####
There may be any number of user-defined memory locations. They are defined
by giving the type (which may be any type except `bit` and `routine`) and the
@ -137,13 +164,37 @@ This is actually useful, at least at this point, as you can rely on the fact
that literal integers in the code are always immediate values. (But this
may change at some point.)
### Buffers and Pointers ###
### Tables and Pointers ###
Roughly speaking, a `buffer` is a table that can be longer than 256 bytes,
and a `pointer` is an address within a buffer.
A table is a collection of memory locations that can be indexed in a number
of ways.
The simplest way is to use another memory location as an index. There
are restrictions on which memory locations can be used as indexes;
only the `x` and `y` locations can be used this way. Since those can
only hold a byte, this method, by itself, only allows access to the first
256 entries of the table.
byte table[1024] tab
...
ld a, tab + x
st a, tab + y
However, by combining indexing with a constant _offset_, entries beyond the
256th entry can be accessed.
byte table[1024] tab
...
ld a, tab + 512 + x
st a, tab + 512 + y
Even with an offset, the range of indexing still cannot exceed 256 entries.
Accessing entries at an arbitrary address inside a table can be done with
a `pointer`. Pointers can only be point inside `byte` tables. When a
pointer is used, indexing with `x` or `y` will also take place.
A `pointer` is implemented as a zero-page memory location, and accessing the
buffer pointed to is implemented with "indirect indexed" addressing, as in
table pointed to is implemented with "indirect indexed" addressing, as in
LDA ($02), Y
STA ($02), Y
@ -151,14 +202,16 @@ buffer 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:
copy ^buf, ptr // this is the only way to initialize a pointer
add ptr, 4 // ok, but only if it does not exceed buffer's size
ld y, 0 // you must set this to something yourself
copy [ptr] + y, byt // read memory through pointer, into byte
copy 100, [ptr] + y // write memory through pointer (still trashes a)
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
copy 100, [ptr] + y // write memory through pointer (still trashes a)
} // after this block, ptr can no longer be used
where `ptr` is a user-defined storage location of `pointer` type, and the
`+ y` part is mandatory.
where `ptr` is a user-defined storage location of `pointer` type, `buf`
is a `table` of `byte` type, and the `+ y` part is mandatory.
Routines
--------
@ -300,17 +353,7 @@ and it trashes the `z` and `n` flags and the `a` register.
After execution, dest is considered initialized, and `z` and `n`, and
`a` are considered uninitialized.
There are two extra modes that this instruction can be used in. The first is
to load an address into a pointer:
copy ^<src-memory-location>, <dest-memory-location>
This copies the address of src into dest. In this case, src must be
of type buffer, and dest must be of type pointer. src will not be
considered a memory location that is read, since it is only its address
that is being retrieved.
The second is to read or write indirectly through a pointer.
There is an extra mode that this instruction can be used in:
copy [<src-memory-location>] + y, <dest-memory-location>
copy <src-memory-location>, [<dest-memory-location>] + y
@ -350,7 +393,7 @@ In fact, this instruction trashes the `a` register in all cases except
when the dest is `a`.
NOTE: If dest is a pointer, the addition does not check if the result of
the pointer arithmetic continues to be valid (within a buffer) or not.
the pointer arithmetic continues to be valid (within a table) or not.
### inc ###
@ -581,11 +624,10 @@ Grammar
Program ::= {ConstDefn | TypeDefn} {Defn} {Routine}.
ConstDefn::= "const" Ident<new> Const.
TypeDefn::= "typedef" Type Ident<new>.
Defn ::= Type Ident<new> [Constraints] (":" Const | "@" LitWord).
Defn ::= Type Ident<new> (":" Const | "@" LitWord).
Type ::= TypeTerm ["table" TypeSize].
TypeExpr::= "byte"
| "word"
| "buffer" TypeSize
| "pointer"
| "vector" TypeTerm
| "routine" Constraints
@ -594,10 +636,8 @@ Grammar
TypeSize::= "[" LitWord "]".
Constrnt::= ["inputs" LocExprs] ["outputs" LocExprs] ["trashes" LocExprs].
Routine ::= "define" Ident<new> Type (Block | "@" LitWord).
| "routine" Ident<new> Constraints (Block | "@" LitWord)
.
LocExprs::= LocExpr {"," LocExpr}.
LocExpr ::= Register | Flag | Const | Ident.
LocExpr ::= Register | Flag | Const | Ident [["+" Const] "+" Register].
Register::= "a" | "x" | "y".
Flag ::= "c" | "z" | "n" | "v".
Const ::= Literal | Ident<const>.
@ -625,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
@ -46,4 +35,10 @@ Atari 2600 (4K cartridge). The directory itself contains a simple
demo, [smiley.60p](atari2600/smiley.60p) which was converted from an
older Atari 2600 skeleton program written in [Ophis][].
### apple2
In the [apple2](apple2/) directory are programs that run on
Apple II series computers (Apple II+, Apple //e). `sixtypical`'s
support for this architecture could be called embryonic.
[Ophis]: http://michaelcmartin.github.io/Ophis/

35
eg/apple2/README.md Normal file
View File

@ -0,0 +1,35 @@
<!--
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.
See the [README in the parent directory](../README.md) for
more information on these example programs.
Note that `sixtypical` does not currently support "load
and go" execution of these programs, because constructing
an Apple II disk image file on the fly is not something
it can currently do. If you have the linapple sources
checked out, and the a2tools available, you could do
something like this:
bin/sixtypical --traceback --origin=0x2000 --output-format=raw eg/apple2/prog.60p --output prog.bin
cp /path/to/linapple/res/Master.dsk sixtypical.dsk
a2rm sixtypical.dsk PROG
a2in B sixtypical.dsk PROG prog.bin
linapple -d1 sixtypical.dsk -autoboot
and then enter
BLOAD PROG
CALL 8192
Ideally you could
BRUN PROG
But that does not always return to BASIC and I'm not sure why.

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
// ----------------------------------------------------------------
@ -21,24 +27,16 @@
// and the end of their own routines, so the type needs to be compatible.
// (In a good sense, it is a continuation.)
//
// Further,
//
// It's very arguable that screen1/2/3/4 and colormap1/2/3/4 are not REALLY inputs.
// They're only there to support the fact that game states sometimes clear the
// screen, and sometimes don't. When they don't, they preserve the screen, and
// currently the way to say "we preserve the screen" is to have it as both input
// and output. There is probably a better way to do this, but it needs thought.
//
typedef routine
inputs joy2, press_fire_msg, dispatch_game_state,
actor_pos, actor_delta, actor_logic,
player_died,
screen, screen1, screen2, screen3, screen4, colormap1, colormap2, colormap3, colormap4
screen, colormap
outputs dispatch_game_state,
actor_pos, actor_delta, actor_logic,
player_died,
screen, screen1, screen2, screen3, screen4, colormap1, colormap2, colormap3, colormap4
screen, colormap
trashes a, x, y, c, z, n, v, pos, new_pos, delta, ptr, dispatch_logic
game_state_routine
@ -62,19 +60,8 @@ typedef routine
byte vic_border @ 53280
byte vic_bg @ 53281
byte table[256] screen1 @ 1024
byte table[256] screen2 @ 1274
byte table[256] screen3 @ 1524
byte table[256] screen4 @ 1774
byte table[256] colormap1 @ 55296
byte table[256] colormap2 @ 55546
byte table[256] colormap3 @ 55796
byte table[256] colormap4 @ 56046
buffer[2048] screen @ 1024
byte joy2 @ $dc00
byte table[2048] screen @ 1024
byte table[2048] colormap @ 55296
// ----------------------------------------------------------------
// Global Variables
@ -87,7 +74,6 @@ word pos
word new_pos
word table[256] actor_delta
word delta
byte player_died
@ -121,88 +107,23 @@ 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 screen1, screen2, screen3, screen4, colormap1, colormap2, colormap3, colormap4
outputs screen, colormap
trashes a, y, c, n, z
{
ld y, 0
repeat {
ld a, 1
st a, colormap1 + y
st a, colormap2 + y
st a, colormap3 + y
st a, colormap4 + y
st a, colormap + y
st a, colormap + 250 + y
st a, colormap + 500 + y
st a, colormap + 750 + y
ld a, 32
st a, screen1 + y
st a, screen2 + y
st a, screen3 + y
st a, screen4 + y
st a, screen + y
st a, screen + 250 + y
st a, screen + 500 + y
st a, screen + 750 + y
inc y
cmp y, 250
@ -282,35 +203,38 @@ define player_logic logic_routine
call check_new_position_in_bounds
if c {
copy ^screen, ptr
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 {
copy ^screen, ptr
point ptr into screen {
reset ptr 0
st off, c
add ptr, pos
copy 32, [ptr] + y
add ptr, new_pos
ld y, 0
copy new_pos, pos
// check collision.
ld a, [ptr] + y
copy ^screen, ptr
st off, c
add ptr, pos
copy 81, [ptr] + y
} else {
ld a, 1
st a, player_died
// 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
reset ptr 0
st off, c
add ptr, pos
copy 81, [ptr] + y
}
}
} else {
ld a, 1
st a, player_died
}
}
@ -321,31 +245,34 @@ define enemy_logic logic_routine
call check_new_position_in_bounds
if c {
copy ^screen, ptr
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 {
copy ^screen, ptr
point ptr into screen {
reset ptr 0
st off, c
add ptr, pos
copy 32, [ptr] + y
add ptr, new_pos
ld y, 0
copy new_pos, pos
// check collision.
ld a, [ptr] + y
copy ^screen, ptr
st off, c
add ptr, pos
copy 82, [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 {
reset ptr 0
st off, c
add ptr, pos
copy 32, [ptr] + y
copy new_pos, pos
reset ptr 0
st off, c
add ptr, pos
copy 82, [ptr] + y
}
}
} else {
copy delta, compare_target
@ -372,7 +299,7 @@ define game_state_title_screen game_state_routine
st on, c
sub a, 64 // yuck. oh well
st a, screen1 + y
st a, screen + y
}
st off, c
@ -437,15 +364,14 @@ 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
}
define main routine
inputs cinv
outputs cinv, save_cinv, pos, dispatch_game_state,
screen1, screen2, screen3, screen4, colormap1, colormap2, colormap3, colormap4
outputs cinv, save_cinv, pos, dispatch_game_state, screen, colormap
trashes a, y, n, c, z, vic_border, vic_bg
{
ld a, 5

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.
@ -50,8 +52,9 @@ byte scanline : 85 // %01010101
// be practical. So we just jump to this location instead.
define pla_tay_pla_tax_pla_rti routine
inputs a
trashes a
inputs border_color, vic_intr
outputs border_color, vic_intr
trashes a, z, n, c
@ $EA81
// ----- Interrupt Handler -----

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,20 +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 `platform/`, should be included first, like
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 platform/c64.60p vector-table.60p
so that system entry points such as `chrout` are defined.
There's a `loadngo.sh` script in this directory that does this.
./loadngo.sh c64 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,7 +1,8 @@
// Include `support/${PLATFORM}.60p` before this source
// Should print Y
buffer[2048] buf
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,36 +0,0 @@
#!/bin/sh
usage="Usage: loadngo.sh (c64|vic20) <source.60p>"
arch="$1"
shift 1
if [ "X$arch" = "Xc64" ]; then
output_format='c64-basic-prg'
if [ -e vicerc ]; then
emu="x64 -config vicerc"
else
emu="x64"
fi
elif [ "X$arch" = "Xvic20" ]; then
output_format='vic20-basic-prg'
if [ -e vicerc ]; then
emu="xvic -config vicerc"
else
emu="xvic"
fi
else
echo $usage && exit 1
fi
src="$1"
if [ "X$src" = "X" ]; then
echo $usage && exit 1
fi
### do it ###
out=/tmp/a-out.prg
../../bin/sixtypical --traceback --output-format=$output_format support/$arch.60p support/stdlib.60p $src --output $out || exit 1
ls -la $out
$emu $out
rm -f $out

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,59 +0,0 @@
#!/bin/sh
usage="Usage: loadngo.sh (c64|vic20|atari2600|apple2) [--dry-run] <source.60p>"
arch="$1"
shift 1
if [ "X$arch" = "Xc64" ]; then
output_format='c64-basic-prg'
if [ -e vicerc ]; then
emu="x64 -config vicerc"
else
emu="x64"
fi
elif [ "X$arch" = "Xvic20" ]; then
output_format='vic20-basic-prg'
if [ -e vicerc ]; then
emu="xvic -config vicerc"
else
emu="xvic"
fi
elif [ "X$arch" = "Xatari2600" ]; then
output_format='atari2600-cart'
emu='stella'
elif [ "X$arch" = "Xapple2" ]; then
src="$1"
out=/tmp/a-out.bin
bin/sixtypical --traceback --origin=0x2000 --output-format=raw $src --output $out || exit 1
ls -la $out
cp ~/scratchpad/linapple/res/Master.dsk sixtypical.dsk
# TODO: replace HELLO with something that does like
# BLOAD "PROG"
# CALL 8192
# (not BRUN because it does not always return to BASIC afterwards not sure why)
a2rm sixtypical.dsk PROG
a2in B sixtypical.dsk PROG $out
linapple -d1 sixtypical.dsk -autoboot
rm -f $out sixtypical.dsk
exit 0
else
echo $usage && exit 1
fi
if [ "X$1" = "X--dry-run" ]; then
shift 1
emu='echo'
fi
src="$1"
if [ "X$src" = "X" ]; then
echo $usage && exit 1
fi
### do it ###
out=/tmp/a-out.prg
bin/sixtypical --traceback --output-format=$output_format $src --output $out || exit 1
ls -la $out
$emu $out
rm -f $out

View File

@ -1,10 +1,17 @@
# 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, If, Repeat, For, WithInterruptsOff, Save
from sixtypical.ast import (
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, BufferType, PointerType, VectorType, RoutineType,
ConstantRef, LocationRef, IndirectRef, IndexedRef, AddressRef,
TableType, PointerType, VectorType, RoutineType,
ConstantRef, LocationRef, IndirectRef, IndexedRef,
REG_A, REG_Y, FLAG_Z, FLAG_N, FLAG_V, FLAG_C
)
@ -78,298 +85,31 @@ class IncompatibleConstraintsError(ConstraintsError):
pass
def routine_has_static(routine, ref):
if not hasattr(routine, 'statics'):
return False
for static in routine.statics:
if static.location == ref:
return True
return False
class Context(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, routines, routine, inputs, outputs, trashes):
self.routines = routines # Location -> AST node
self.routine = routine
self._touched = set()
self._range = dict()
self._writeable = set()
self._terminated = False
self._gotos_encountered = set()
for ref in inputs:
if ref.is_constant():
raise ConstantConstraintError(self.routine, ref.name)
self._range[ref] = ref.max_range()
output_names = set()
for ref in outputs:
if ref.is_constant():
raise ConstantConstraintError(self.routine, ref.name)
output_names.add(ref.name)
self._writeable.add(ref)
for ref in trashes:
if ref.is_constant():
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 "Context(\n _touched={},\n _range={},\n _writeable={}\n)".format(
LocationRef.format_set(self._touched), LocationRef.format_set(self._range), LocationRef.format_set(self._writeable)
)
def clone(self):
c = Context(self.routines, self.routine, [], [], [])
c._touched = set(self._touched)
c._range = dict(self._range)
c._writeable = set(self._writeable)
return c
def update_from(self, other):
self.routines = other.routines
self.routine = other.routine
self._touched = set(other._touched)
self._range = dict(other._range)
self._writeable = set(other._writeable)
self._terminated = other._terminated
self._gotos_encounters = set(other._gotos_encountered)
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 routine_has_static(self.routine, ref):
continue
if ref.is_constant() or ref in self.routines:
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 routine_has_static(self.routine, ref):
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):
# FIXME there's a bit of I'm-not-sure-the-best-way-to-do-this-ness, here...
# 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 = outside.max_range()
if isinstance(outside.type, TableType):
outside_range = (0, outside.type.size-1)
if inside_range[0] < outside_range[0] or inside_range[1] > outside_range[1]:
raise RangeExceededError(self.routine,
"Possible range of {} {} exceeds acceptable range of {} {}".format(
inside, inside_range, 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] = ref.max_range()
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 = src.max_range()
self._range[dest] = src_range
def invalidate_range(self, ref):
self.assert_meaningful(ref)
self._range[ref] = ref.max_range()
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 assert_types_for_read_table(self, instr, src, dest, type_):
if (not TableType.is_a_table_type(src.ref.type, type_)) or (not dest.type == type_):
raise TypeMismatchError(instr, '{} and {}'.format(src.ref.name, dest.name))
self.assert_meaningful(src, src.index)
self.assert_in_range(src.index, src.ref)
def assert_types_for_update_table(self, instr, dest, type_):
if not TableType.is_a_table_type(dest.ref.type, type_):
raise TypeMismatchError(instr, '{}'.format(dest.ref.name))
self.assert_meaningful(dest.index)
self.assert_in_range(dest.index, dest.ref)
self.set_written(dest.ref)
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)
class Analyzer(object):
def __init__(self, debug=False):
def __init__(self, symtab, debug=False):
self.symtab = symtab
self.current_routine = None
self.routines = {}
self.debug = debug
self.exit_contexts_map = {}
# - - - - helper methods - - - -
def get_type_for_name(self, 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(str(ref))
return self.get_type_for_name(ref.name)
def assert_type(self, type_, *locations):
for location in locations:
if location.type != type_:
if self.get_type(location) != type_:
raise TypeMismatchError(self.current_routine, location.name)
def assert_affected_within(self, name, affecting_type, limiting_type):
@ -387,43 +127,60 @@ class Analyzer(object):
)
raise IncompatibleConstraintsError(self.current_routine, message)
def assert_types_for_read_table(self, context, instr, src, dest, type_, offset):
if (not TableType.is_a_table_type(self.get_type(src.ref), type_)) or (not self.get_type(dest) == type_):
raise TypeMismatchError(instr, '{} and {}'.format(src.ref.name, dest.name))
context.assert_meaningful(src, src.index)
context.assert_in_range(src.index, src.ref, offset)
def assert_types_for_update_table(self, context, instr, dest, type_, offset):
if not TableType.is_a_table_type(self.get_type(dest.ref), type_):
raise TypeMismatchError(instr, '{}'.format(dest.ref.name))
context.assert_meaningful(dest.index)
context.assert_in_range(dest.index, dest.ref, offset)
context.set_written(dest.ref)
# - - - - visitor methods - - - -
def analyze_program(self, program):
assert isinstance(program, Program)
self.routines = {r.location: r for r in program.routines}
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
return None, type_
self.current_routine = routine
type_ = routine.location.type
context = Context(self.routines, routine, type_.inputs, type_.outputs, type_.trashes)
self.exit_contexts = []
if self.debug:
print("at start of routine `{}`:".format(routine.name))
print(context)
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)
trashed = set(context.each_touched()) - set(context.each_meaningful())
if self.debug:
print("at end of routine `{}`:".format(routine.name))
print(context)
print("trashed: ", LocationRef.format_set(trashed))
print("outputs: ", LocationRef.format_set(type_.outputs))
trashed_outputs = type_.outputs & trashed
if trashed_outputs:
print("TRASHED OUTPUTS: ", LocationRef.format_set(trashed_outputs))
print('')
print('-' * 79)
print('')
self.exit_contexts_map[routine.name] = {
'end_context': context.to_json_data(),
'exit_contexts': [e.to_json_data() for e in self.exit_contexts]
}
if self.exit_contexts:
# check that they are all consistent
@ -433,11 +190,15 @@ class Analyzer(object):
exit_writeable = set(exit_context.each_writeable())
for ex in self.exit_contexts[1:]:
if set(ex.each_meaningful()) != exit_meaningful:
raise InconsistentExitError("Exit contexts are not consistent")
raise InconsistentExitError(routine, "Exit contexts are not consistent")
if set(ex.each_touched()) != exit_touched:
raise InconsistentExitError("Exit contexts are not consistent")
raise InconsistentExitError(routine, "Exit contexts are not consistent")
if set(ex.each_writeable()) != exit_writeable:
raise InconsistentExitError("Exit contexts are not consistent")
raise InconsistentExitError(routine, "Exit contexts are not consistent")
# We now set the main context to the (consistent) exit context
# so that this routine is perceived as having the same effect
# that any of the goto'ed routines have.
context.update_from(exit_context)
# these all apply whether we encountered goto(s) in this routine, or not...:
@ -453,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 routine_has_static(routine, ref):
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)
@ -468,6 +229,10 @@ class Analyzer(object):
def analyze_instr(self, instr, context):
if isinstance(instr, SingleOp):
self.analyze_single_op(instr, context)
elif isinstance(instr, Call):
self.analyze_call(instr, context)
elif isinstance(instr, GoTo):
self.analyze_goto(instr, context)
elif isinstance(instr, If):
self.analyze_if(instr, context)
elif isinstance(instr, Repeat):
@ -475,13 +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):
@ -494,15 +261,21 @@ class Analyzer(object):
if opcode == 'ld':
if isinstance(src, IndexedRef):
context.assert_types_for_read_table(instr, src, dest, TYPE_BYTE)
self.assert_types_for_read_table(context, instr, src, dest, TYPE_BYTE, src.offset)
elif isinstance(src, IndirectRef):
# copying this analysis from the matching branch in `copy`, below
if isinstance(src.ref.type, PointerType) and dest.type == TYPE_BYTE:
if isinstance(self.get_type(src.ref), PointerType) and self.get_type(dest) == TYPE_BYTE:
pass
else:
raise TypeMismatchError(instr, (src, dest))
origin = context.get_assoc(src.ref)
if not origin:
raise UnmeaningfulReadError(instr, src.ref)
context.assert_meaningful(origin)
context.assert_meaningful(src.ref, REG_Y)
elif src.type != dest.type:
elif self.get_type(src) != self.get_type(dest):
raise TypeMismatchError(instr, '{} and {}'.format(src.name, dest.name))
else:
context.assert_meaningful(src)
@ -510,18 +283,24 @@ class Analyzer(object):
context.set_written(dest, FLAG_Z, FLAG_N)
elif opcode == 'st':
if isinstance(dest, IndexedRef):
if src.type != TYPE_BYTE:
if self.get_type(src) != TYPE_BYTE:
raise TypeMismatchError(instr, (src, dest))
context.assert_types_for_update_table(instr, dest, TYPE_BYTE)
self.assert_types_for_update_table(context, instr, dest, TYPE_BYTE, dest.offset)
elif isinstance(dest, IndirectRef):
# copying this analysis from the matching branch in `copy`, below
if isinstance(dest.ref.type, PointerType) and src.type == TYPE_BYTE:
if isinstance(self.get_type(dest.ref), PointerType) and self.get_type(src) == TYPE_BYTE:
pass
else:
raise TypeMismatchError(instr, (src, dest))
context.assert_meaningful(dest.ref, REG_Y)
context.set_written(dest.ref)
elif src.type != dest.type:
target = context.get_assoc(dest.ref)
if not target:
raise ForbiddenWriteError(instr, dest.ref)
context.set_written(target)
elif self.get_type(src) != self.get_type(dest):
raise TypeMismatchError(instr, '{} and {}'.format(src, dest))
else:
context.set_written(dest)
@ -530,18 +309,19 @@ class Analyzer(object):
elif opcode == 'add':
context.assert_meaningful(src, dest, FLAG_C)
if isinstance(src, IndexedRef):
context.assert_types_for_read_table(instr, src, dest, TYPE_BYTE)
elif src.type == TYPE_BYTE:
self.assert_types_for_read_table(context, instr, src, dest, TYPE_BYTE, src.offset)
elif self.get_type(src) == TYPE_BYTE:
self.assert_type(TYPE_BYTE, src, dest)
if dest != REG_A:
context.set_touched(REG_A)
context.set_unmeaningful(REG_A)
else:
self.assert_type(TYPE_WORD, src)
if dest.type == TYPE_WORD:
dest_type = self.get_type(dest)
if dest_type == TYPE_WORD:
context.set_touched(REG_A)
context.set_unmeaningful(REG_A)
elif isinstance(dest.type, PointerType):
elif isinstance(dest_type, PointerType):
context.set_touched(REG_A)
context.set_unmeaningful(REG_A)
else:
@ -551,8 +331,8 @@ class Analyzer(object):
elif opcode == 'sub':
context.assert_meaningful(src, dest, FLAG_C)
if isinstance(src, IndexedRef):
context.assert_types_for_read_table(instr, src, dest, TYPE_BYTE)
elif src.type == TYPE_BYTE:
self.assert_types_for_read_table(context, instr, src, dest, TYPE_BYTE, src.offset)
elif self.get_type(src) == TYPE_BYTE:
self.assert_type(TYPE_BYTE, src, dest)
if dest != REG_A:
context.set_touched(REG_A)
@ -566,8 +346,8 @@ class Analyzer(object):
elif opcode == 'cmp':
context.assert_meaningful(src, dest)
if isinstance(src, IndexedRef):
context.assert_types_for_read_table(instr, src, dest, TYPE_BYTE)
elif src.type == TYPE_BYTE:
self.assert_types_for_read_table(context, instr, src, dest, TYPE_BYTE, src.offset)
elif self.get_type(src) == TYPE_BYTE:
self.assert_type(TYPE_BYTE, src, dest)
else:
self.assert_type(TYPE_WORD, src, dest)
@ -576,7 +356,7 @@ class Analyzer(object):
context.set_written(FLAG_Z, FLAG_N, FLAG_C)
elif opcode == 'and':
if isinstance(src, IndexedRef):
context.assert_types_for_read_table(instr, src, dest, TYPE_BYTE)
self.assert_types_for_read_table(context, instr, src, dest, TYPE_BYTE, src.offset)
else:
self.assert_type(TYPE_BYTE, src, dest)
context.assert_meaningful(src, dest)
@ -588,7 +368,7 @@ class Analyzer(object):
context.set_top_of_range(dest, context.get_top_of_range(src))
elif opcode in ('or', 'xor'):
if isinstance(src, IndexedRef):
context.assert_types_for_read_table(instr, src, dest, TYPE_BYTE)
self.assert_types_for_read_table(context, instr, src, dest, TYPE_BYTE, src.offset)
else:
self.assert_type(TYPE_BYTE, src, dest)
context.assert_meaningful(src, dest)
@ -597,7 +377,7 @@ class Analyzer(object):
elif opcode in ('inc', 'dec'):
context.assert_meaningful(dest)
if isinstance(dest, IndexedRef):
context.assert_types_for_update_table(instr, dest, TYPE_BYTE)
self.assert_types_for_update_table(context, instr, dest, TYPE_BYTE, dest.offset)
context.set_written(dest.ref, FLAG_Z, FLAG_N)
#context.invalidate_range(dest)
else:
@ -620,84 +400,65 @@ class Analyzer(object):
elif opcode in ('shl', 'shr'):
context.assert_meaningful(dest, FLAG_C)
if isinstance(dest, IndexedRef):
context.assert_types_for_update_table(instr, dest, TYPE_BYTE)
self.assert_types_for_update_table(context, instr, dest, TYPE_BYTE, dest.offset)
context.set_written(dest.ref, FLAG_Z, FLAG_N, FLAG_C)
#context.invalidate_range(dest)
else:
self.assert_type(TYPE_BYTE, dest)
context.set_written(dest, FLAG_Z, FLAG_N, FLAG_C)
context.invalidate_range(dest)
elif opcode == 'call':
type = instr.location.type
if not isinstance(type, (RoutineType, VectorType)):
raise TypeMismatchError(instr, instr.location)
if isinstance(type, VectorType):
type = type.of_type
for ref in type.inputs:
context.assert_meaningful(ref)
for ref in type.outputs:
context.set_written(ref)
for ref in type.trashes:
context.assert_writeable(ref)
context.set_touched(ref)
context.set_unmeaningful(ref)
elif opcode == 'copy':
if dest == REG_A:
raise ForbiddenWriteError(instr, "{} cannot be used as destination for copy".format(dest))
# 1. check that their types are compatible
if isinstance(src, AddressRef) and isinstance(dest, LocationRef):
if isinstance(src.ref.type, BufferType) and isinstance(dest.type, PointerType):
pass
else:
raise TypeMismatchError(instr, (src, dest))
elif isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndirectRef):
if src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType):
if isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndirectRef):
if self.get_type(src) == TYPE_BYTE and isinstance(self.get_type(dest.ref), PointerType):
pass
else:
raise TypeMismatchError(instr, (src, dest))
elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef):
if isinstance(src.ref.type, PointerType) and dest.type == TYPE_BYTE:
if isinstance(self.get_type(src.ref), PointerType) and self.get_type(dest) == TYPE_BYTE:
pass
else:
raise TypeMismatchError(instr, (src, dest))
elif isinstance(src, IndirectRef) and isinstance(dest, IndirectRef):
if isinstance(src.ref.type, PointerType) and isinstance(dest.ref.type, PointerType):
if isinstance(self.get_type(src.ref), PointerType) and isinstance(self.get_type(dest.ref), PointerType):
pass
else:
raise TypeMismatchError(instr, (src, dest))
elif isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, IndexedRef):
if src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD):
if self.get_type(src) == TYPE_WORD and TableType.is_a_table_type(self.get_type(dest.ref), TYPE_WORD):
pass
elif (isinstance(src.type, VectorType) and isinstance(dest.ref.type, TableType) and
RoutineType.executable_types_compatible(src.type.of_type, dest.ref.type.of_type)):
elif (isinstance(self.get_type(src), VectorType) and isinstance(self.get_type(dest.ref), TableType) and
RoutineType.executable_types_compatible(self.get_type(src).of_type, self.get_type(dest.ref).of_type)):
pass
elif (isinstance(src.type, RoutineType) and isinstance(dest.ref.type, TableType) and
RoutineType.executable_types_compatible(src.type, dest.ref.type.of_type)):
elif (isinstance(self.get_type(src), RoutineType) and isinstance(self.get_type(dest.ref), TableType) and
RoutineType.executable_types_compatible(self.get_type(src), self.get_type(dest.ref).of_type)):
pass
else:
raise TypeMismatchError(instr, (src, dest))
context.assert_in_range(dest.index, dest.ref)
context.assert_in_range(dest.index, dest.ref, dest.offset)
elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef):
if TableType.is_a_table_type(src.ref.type, TYPE_WORD) and dest.type == TYPE_WORD:
if TableType.is_a_table_type(self.get_type(src.ref), TYPE_WORD) and self.get_type(dest) == TYPE_WORD:
pass
elif (isinstance(src.ref.type, TableType) and isinstance(dest.type, VectorType) and
RoutineType.executable_types_compatible(src.ref.type.of_type, dest.type.of_type)):
elif (isinstance(self.get_type(src.ref), TableType) and isinstance(self.get_type(dest), VectorType) and
RoutineType.executable_types_compatible(self.get_type(src.ref).of_type, self.get_type(dest).of_type)):
pass
else:
raise TypeMismatchError(instr, (src, dest))
context.assert_in_range(src.index, src.ref)
context.assert_in_range(src.index, src.ref, src.offset)
elif isinstance(src, (LocationRef, ConstantRef)) and isinstance(dest, LocationRef):
if src.type == dest.type:
if self.get_type(src) == self.get_type(dest):
pass
elif isinstance(src.type, RoutineType) and isinstance(dest.type, VectorType):
self.assert_affected_within('inputs', src.type, dest.type.of_type)
self.assert_affected_within('outputs', src.type, dest.type.of_type)
self.assert_affected_within('trashes', src.type, dest.type.of_type)
elif isinstance(self.get_type(src), RoutineType) and isinstance(self.get_type(dest), VectorType):
self.assert_affected_within('inputs', self.get_type(src), self.get_type(dest).of_type)
self.assert_affected_within('outputs', self.get_type(src), self.get_type(dest).of_type)
self.assert_affected_within('trashes', self.get_type(src), self.get_type(dest).of_type)
else:
raise TypeMismatchError(instr, (src, dest))
else:
@ -706,17 +467,36 @@ 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)
# TODO this will need to be more sophisticated. it's the thing ref points to that is written, not ref itself.
context.set_written(dest.ref)
context.assert_meaningful(src, dest.ref, REG_Y)
target = context.get_assoc(dest.ref)
if not target:
raise ForbiddenWriteError(instr, dest.ref)
context.set_written(target)
elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef):
context.assert_meaningful(src.ref, REG_Y)
# TODO more sophisticated?
origin = context.get_assoc(src.ref)
if not origin:
raise UnmeaningfulReadError(instr, src.ref)
context.assert_meaningful(origin)
context.set_written(dest)
elif isinstance(src, IndirectRef) and isinstance(dest, IndirectRef):
context.assert_meaningful(src.ref, REG_Y)
# TODO more sophisticated?
context.set_written(dest.ref)
context.assert_meaningful(src.ref, dest.ref, REG_Y)
origin = context.get_assoc(src.ref)
if not origin:
raise UnmeaningfulReadError(instr, src.ref)
context.assert_meaningful(origin)
target = context.get_assoc(dest.ref)
if not target:
raise ForbiddenWriteError(instr, dest.ref)
context.set_written(target)
elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef):
context.assert_meaningful(src, dest.ref, dest.index)
context.set_written(dest.ref)
@ -725,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)
@ -733,59 +512,6 @@ class Analyzer(object):
context.set_touched(REG_A, FLAG_Z, FLAG_N)
context.set_unmeaningful(REG_A, FLAG_Z, FLAG_N)
elif opcode == 'goto':
location = instr.location
type_ = location.type
if not isinstance(type_, (RoutineType, VectorType)):
raise TypeMismatchError(instr, location)
# assert that the dest routine's inputs are all initialized
if isinstance(type_, VectorType):
type_ = type_.of_type
for ref in type_.inputs:
context.assert_meaningful(ref)
# and that this routine's trashes and output constraints are a
# superset of the called routine's
current_type = self.current_routine.location.type
self.assert_affected_within('outputs', type_, current_type)
self.assert_affected_within('trashes', type_, current_type)
context.encounter_gotos(set([instr.location]))
# Now that we have encountered a goto, we update the
# context here to match what someone calling the goto'ed
# function directly, would expect. (which makes sense
# when you think about it; if this goto's F, then calling
# this is like calling F, from the perspective of what is
# returned.)
#
# However, this isn't the current context anymore. This
# is an exit context of this routine.
exit_context = context.clone()
for ref in type_.outputs:
exit_context.set_touched(ref) # ?
exit_context.set_written(ref)
for ref in type_.trashes:
exit_context.assert_writeable(ref)
exit_context.set_touched(ref)
exit_context.set_unmeaningful(ref)
self.exit_contexts.append(exit_context)
# When we get to the end, we'll check that all the
# exit contexts are consistent with each other.
# We set the current context as having terminated.
# If we are in a branch, the merge will deal with
# having terminated. If we are at the end of the
# routine, the routine end will deal with that.
context.set_terminated()
elif opcode == 'trash':
context.set_touched(instr.dest)
@ -795,6 +521,79 @@ class Analyzer(object):
else:
raise NotImplementedError(opcode)
def analyze_call(self, instr, context):
type_ = self.get_type(instr.location)
if not isinstance(type_, (RoutineType, VectorType)):
raise TypeMismatchError(instr, instr.location.name)
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:
context.set_written(ref)
for ref in type_.trashes:
context.assert_writeable(ref)
context.set_touched(ref)
context.set_unmeaningful(ref)
def analyze_goto(self, instr, context):
location = instr.location
type_ = self.get_type(instr.location)
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
for ref in type_.inputs:
context.assert_meaningful(ref)
# and that this routine's trashes and output constraints are a
# superset of the called routine's
current_type = self.get_type_for_name(self.current_routine.name)
self.assert_affected_within('outputs', type_, current_type)
self.assert_affected_within('trashes', type_, current_type)
context.encounter_gotos(set([instr.location]))
# Now that we have encountered a goto, we update the
# context here to match what someone calling the goto'ed
# function directly, would expect. (which makes sense
# when you think about it; if this goto's F, then calling
# this is like calling F, from the perspective of what is
# returned.)
#
# However, this isn't the current context anymore. This
# is an exit context of this routine.
exit_context = context.clone()
for ref in type_.outputs:
exit_context.set_written(ref)
for ref in type_.trashes:
exit_context.assert_writeable(ref)
exit_context.set_touched(ref)
exit_context.set_unmeaningful(ref)
self.exit_contexts.append(exit_context)
# When we get to the end, we'll check that all the
# exit contexts are consistent with each other.
# We set the current context as having terminated.
# If we are in a branch, the merge will deal with
# having terminated. If we are at the end of the
# routine, the routine end will deal with that.
context.set_terminated()
def analyze_if(self, instr, context):
incoming_meaningful = set(context.each_meaningful())
@ -883,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:
@ -905,3 +711,47 @@ class Analyzer(object):
else:
context.set_touched(REG_A)
context.set_unmeaningful(REG_A)
def analyze_point_into(self, instr, context):
if not isinstance(self.get_type(instr.pointer), PointerType):
raise TypeMismatchError(instr, instr.pointer)
if not TableType.is_a_table_type(self.get_type(instr.table), TYPE_BYTE):
raise TypeMismatchError(instr, instr.table)
# check that pointer is not yet associated with any table.
if context.get_assoc(instr.pointer):
raise ForbiddenWriteError(instr, instr.pointer)
# 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_unmeaningful(instr.pointer)
self.analyze_block(instr.block, context)
if context.encountered_gotos():
raise IllegalJumpError(instr, instr)
# unassociate pointer with table, mark as unmeaningful.
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):
@ -54,12 +58,12 @@ class Program(AST):
class Defn(AST):
value_attrs = ('name', 'addr', 'initial', 'location',)
value_attrs = ('name', 'addr', 'initial',)
class Routine(AST):
value_attrs = ('name', 'addr', 'initial', 'location',)
children_attrs = ('statics',)
value_attrs = ('name', 'addr', 'initial',)
children_attrs = ('locals',)
child_attrs = ('block',)
@ -72,7 +76,19 @@ class Instr(AST):
class SingleOp(Instr):
value_attrs = ('opcode', 'dest', 'src', 'location',)
value_attrs = ('opcode', 'dest', 'src',)
class Reset(Instr):
value_attrs = ('pointer', 'offset',)
class Call(Instr):
value_attrs = ('location',)
class GoTo(Instr):
value_attrs = ('location',)
class If(Instr):
@ -97,3 +113,8 @@ class WithInterruptsOff(Instr):
class Save(Instr):
value_attrs = ('locations',)
child_attrs = ('block',)
class PointInto(Instr):
value_attrs = ('pointer', 'table',)
child_attrs = ('block',)

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,10 +1,16 @@
# 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, If, Repeat, For, WithInterruptsOff, Save
from sixtypical.ast import (
Program, Routine, Block, SingleOp, Reset, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
)
from sixtypical.model import (
ConstantRef, LocationRef, IndexedRef, IndirectRef, AddressRef,
ConstantRef, LocationRef, IndexedRef, IndirectRef,
TYPE_BIT, TYPE_BYTE, TYPE_WORD,
TableType, BufferType, PointerType, RoutineType, VectorType,
TableType, PointerType, RoutineType, VectorType,
REG_A, REG_X, REG_Y, FLAG_C
)
from sixtypical.emitter import Byte, Word, Table, Label, Offset, LowAddressByte, HighAddressByte
@ -28,15 +34,29 @@ class UnsupportedOpcodeError(KeyError):
class Compiler(object):
def __init__(self, emitter):
def __init__(self, symtab, emitter):
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
# - - - - helper methods - - - -
def get_type_for_name(self, 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
return self.get_type_for_name(ref.name)
def addressing_mode_for_index(self, index):
if index == REG_X:
@ -48,24 +68,22 @@ class Compiler(object):
def compute_length_of_defn(self, defn):
length = None
type_ = defn.location.type
type_ = self.get_type_for_name(defn.name)
if type_ == TYPE_BYTE:
length = 1
elif type_ == TYPE_WORD or isinstance(type_, (PointerType, VectorType)):
length = 2
elif isinstance(type_, TableType):
length = type_.size * (1 if type_.of_type == TYPE_BYTE else 2)
elif isinstance(type_, BufferType):
length = type_.size
if length is None:
raise NotImplementedError("Need size for type {}".format(type_))
return length
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):
@ -74,18 +92,18 @@ class Compiler(object):
else:
return Absolute(label)
# visitor methods
# - - - - visitor methods - - - -
def compile_program(self, program, compilation_roster=None):
assert isinstance(program, Program)
defn_labels = []
declarations = []
for defn in program.defns:
length = self.compute_length_of_defn(defn)
label = Label(defn.name, addr=defn.addr, length=length)
self.labels[defn.name] = label
defn_labels.append((defn, label))
declarations.append((defn, self.symtab.fetch_global_type(defn.name), label))
for routine in program.routines:
self.routines[routine.name] = routine
@ -94,23 +112,25 @@ class Compiler(object):
label.set_addr(routine.addr)
self.labels[routine.name] = label
if hasattr(routine, 'statics'):
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
defn_labels.append((defn, label))
self.routine_statics[routine.name] = static_labels
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)
@ -118,10 +138,9 @@ class Compiler(object):
self.emitter.emit(RTS())
# initialized data
for defn, label in defn_labels:
for defn, type_, label in declarations:
if defn.initial is not None:
initial_data = None
type_ = defn.location.type
if type_ == TYPE_BYTE:
initial_data = Byte(defn.initial)
elif type_ == TYPE_WORD:
@ -137,31 +156,57 @@ class Compiler(object):
self.emitter.emit(initial_data)
# uninitialized, "BSS" data
for defn, label in defn_labels:
for defn, type_, label in declarations:
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):
return self.compile_single_op(instr)
elif isinstance(instr, Call):
return self.compile_call(instr)
elif isinstance(instr, GoTo):
return self.compile_goto(instr)
elif isinstance(instr, If):
return self.compile_if(instr)
elif isinstance(instr, Repeat):
@ -172,6 +217,10 @@ class Compiler(object):
return self.compile_with_interrupts_off(instr)
elif isinstance(instr, Save):
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
@ -190,10 +239,10 @@ class Compiler(object):
elif isinstance(src, ConstantRef):
self.emitter.emit(LDA(Immediate(Byte(src.value))))
elif isinstance(src, IndexedRef) and src.index == REG_X:
self.emitter.emit(LDA(AbsoluteX(self.get_label(src.ref.name))))
self.emitter.emit(LDA(AbsoluteX(Offset(self.get_label(src.ref.name), src.offset.value))))
elif isinstance(src, IndexedRef) and src.index == REG_Y:
self.emitter.emit(LDA(AbsoluteY(self.get_label(src.ref.name))))
elif isinstance(src, IndirectRef) and isinstance(src.ref.type, PointerType):
self.emitter.emit(LDA(AbsoluteY(Offset(self.get_label(src.ref.name), src.offset.value))))
elif isinstance(src, IndirectRef) and isinstance(self.get_type(src.ref), PointerType):
self.emitter.emit(LDA(IndirectY(self.get_label(src.ref.name))))
else:
self.emitter.emit(LDA(self.absolute_or_zero_page(self.get_label(src.name))))
@ -203,7 +252,7 @@ class Compiler(object):
elif isinstance(src, ConstantRef):
self.emitter.emit(LDX(Immediate(Byte(src.value))))
elif isinstance(src, IndexedRef) and src.index == REG_Y:
self.emitter.emit(LDX(AbsoluteY(self.get_label(src.ref.name))))
self.emitter.emit(LDX(AbsoluteY(Offset(self.get_label(src.ref.name), src.offset.value))))
else:
self.emitter.emit(LDX(self.absolute_or_zero_page(self.get_label(src.name))))
elif dest == REG_Y:
@ -212,7 +261,7 @@ class Compiler(object):
elif isinstance(src, ConstantRef):
self.emitter.emit(LDY(Immediate(Byte(src.value))))
elif isinstance(src, IndexedRef) and src.index == REG_X:
self.emitter.emit(LDY(AbsoluteX(self.get_label(src.ref.name))))
self.emitter.emit(LDY(AbsoluteX(Offset(self.get_label(src.ref.name), src.offset.value))))
else:
self.emitter.emit(LDY(self.absolute_or_zero_page(self.get_label(src.name))))
else:
@ -234,8 +283,8 @@ class Compiler(object):
REG_X: AbsoluteX,
REG_Y: AbsoluteY,
}[dest.index]
operand = mode_cls(self.get_label(dest.ref.name))
elif isinstance(dest, IndirectRef) and isinstance(dest.ref.type, PointerType):
operand = mode_cls(Offset(self.get_label(dest.ref.name), dest.offset.value))
elif isinstance(dest, IndirectRef) and isinstance(self.get_type(dest.ref), PointerType):
operand = IndirectY(self.get_label(dest.ref.name))
else:
operand = self.absolute_or_zero_page(self.get_label(dest.name))
@ -250,10 +299,11 @@ class Compiler(object):
if isinstance(src, ConstantRef):
self.emitter.emit(ADC(Immediate(Byte(src.value))))
elif isinstance(src, IndexedRef):
self.emitter.emit(ADC(self.addressing_mode_for_index(src.index)(self.get_label(src.ref.name))))
mode = self.addressing_mode_for_index(src.index)
self.emitter.emit(ADC(mode(Offset(self.get_label(src.ref.name), src.offset.value))))
else:
self.emitter.emit(ADC(Absolute(self.get_label(src.name))))
elif isinstance(dest, LocationRef) and src.type == TYPE_BYTE and dest.type == TYPE_BYTE:
elif isinstance(dest, LocationRef) and self.get_type(src) == TYPE_BYTE and self.get_type(dest) == TYPE_BYTE:
if isinstance(src, ConstantRef):
dest_label = self.get_label(dest.name)
self.emitter.emit(LDA(Absolute(dest_label)))
@ -267,7 +317,7 @@ class Compiler(object):
self.emitter.emit(STA(Absolute(dest_label)))
else:
raise UnsupportedOpcodeError(instr)
elif isinstance(dest, LocationRef) and src.type == TYPE_WORD and dest.type == TYPE_WORD:
elif isinstance(dest, LocationRef) and self.get_type(src) == TYPE_WORD and self.get_type(dest) == TYPE_WORD:
if isinstance(src, ConstantRef):
dest_label = self.get_label(dest.name)
self.emitter.emit(LDA(Absolute(dest_label)))
@ -287,7 +337,7 @@ class Compiler(object):
self.emitter.emit(STA(Absolute(Offset(dest_label, 1))))
else:
raise UnsupportedOpcodeError(instr)
elif isinstance(dest, LocationRef) and src.type == TYPE_WORD and isinstance(dest.type, PointerType):
elif isinstance(dest, LocationRef) and self.get_type(src) == TYPE_WORD and isinstance(self.get_type(dest), PointerType):
if isinstance(src, ConstantRef):
dest_label = self.get_label(dest.name)
self.emitter.emit(LDA(ZeroPage(dest_label)))
@ -316,10 +366,11 @@ class Compiler(object):
if isinstance(src, ConstantRef):
self.emitter.emit(SBC(Immediate(Byte(src.value))))
elif isinstance(src, IndexedRef):
self.emitter.emit(SBC(self.addressing_mode_for_index(src.index)(self.get_label(src.ref.name))))
mode = self.addressing_mode_for_index(src.index)
self.emitter.emit(SBC(mode(Offset(self.get_label(src.ref.name), src.offset.value))))
else:
self.emitter.emit(SBC(Absolute(self.get_label(src.name))))
elif isinstance(dest, LocationRef) and src.type == TYPE_BYTE and dest.type == TYPE_BYTE:
elif isinstance(dest, LocationRef) and self.get_type(src) == TYPE_BYTE and self.get_type(dest) == TYPE_BYTE:
if isinstance(src, ConstantRef):
dest_label = self.get_label(dest.name)
self.emitter.emit(LDA(Absolute(dest_label)))
@ -333,7 +384,7 @@ class Compiler(object):
self.emitter.emit(STA(Absolute(dest_label)))
else:
raise UnsupportedOpcodeError(instr)
elif isinstance(dest, LocationRef) and src.type == TYPE_WORD and dest.type == TYPE_WORD:
elif isinstance(dest, LocationRef) and self.get_type(src) == TYPE_WORD and self.get_type(dest) == TYPE_WORD:
if isinstance(src, ConstantRef):
dest_label = self.get_label(dest.name)
self.emitter.emit(LDA(Absolute(dest_label)))
@ -367,7 +418,8 @@ class Compiler(object):
if isinstance(src, ConstantRef):
self.emitter.emit(cls(Immediate(Byte(src.value))))
elif isinstance(src, IndexedRef):
self.emitter.emit(cls(self.addressing_mode_for_index(src.index)(self.get_label(src.ref.name))))
mode = self.addressing_mode_for_index(src.index)
self.emitter.emit(cls(mode(Offset(self.get_label(src.ref.name), src.offset.value))))
else:
self.emitter.emit(cls(self.absolute_or_zero_page(self.get_label(src.name))))
else:
@ -384,34 +436,10 @@ class Compiler(object):
if dest == REG_A:
self.emitter.emit(cls())
elif isinstance(dest, IndexedRef):
self.emitter.emit(cls(self.addressing_mode_for_index(dest.index)(self.get_label(dest.ref.name))))
mode = self.addressing_mode_for_index(dest.index)
self.emitter.emit(cls(mode(Offset(self.get_label(dest.ref.name), dest.offset.value))))
else:
self.emitter.emit(cls(self.absolute_or_zero_page(self.get_label(dest.name))))
elif opcode == 'call':
location = instr.location
label = self.get_label(instr.location.name)
if isinstance(location.type, RoutineType):
self.emitter.emit(JSR(Absolute(label)))
elif isinstance(location.type, VectorType):
trampoline = self.trampolines.setdefault(
location, Label(location.name + '_trampoline')
)
self.emitter.emit(JSR(Absolute(trampoline)))
else:
raise NotImplementedError
elif opcode == 'goto':
self.final_goto_seen = True
if self.skip_final_goto:
pass
else:
location = instr.location
label = self.get_label(instr.location.name)
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
elif opcode == 'copy':
self.compile_copy(instr, instr.src, instr.dest)
elif opcode == 'trash':
@ -421,9 +449,34 @@ class Compiler(object):
else:
raise NotImplementedError(opcode)
def compile_call(self, instr):
location = instr.location
label = self.get_label(instr.location.name)
location_type = self.get_type(location)
if isinstance(location_type, RoutineType):
self.emitter.emit(JSR(Absolute(label)))
elif isinstance(location_type, VectorType):
trampoline = self.trampolines.setdefault(
location, Label(location.name + '_trampoline')
)
self.emitter.emit(JSR(Absolute(trampoline)))
else:
raise NotImplementedError(location_type)
def compile_goto(self, instr):
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)
def compile_cmp(self, instr, src, dest):
"""`instr` is only for reporting purposes"""
if isinstance(src, LocationRef) and src.type == TYPE_WORD:
if isinstance(src, LocationRef) and self.get_type(src) == TYPE_WORD:
src_label = self.get_label(src.name)
dest_label = self.get_label(dest.name)
self.emitter.emit(LDA(Absolute(dest_label)))
@ -434,7 +487,7 @@ class Compiler(object):
self.emitter.emit(CMP(Absolute(Offset(src_label, 1))))
self.emitter.resolve_label(end_label)
return
if isinstance(src, ConstantRef) and src.type == TYPE_WORD:
if isinstance(src, ConstantRef) and self.get_type(src) == TYPE_WORD:
dest_label = self.get_label(dest.name)
self.emitter.emit(LDA(Absolute(dest_label)))
self.emitter.emit(CMP(Immediate(Byte(src.low_byte()))))
@ -455,7 +508,8 @@ class Compiler(object):
self.emitter.emit(cls(Immediate(Byte(src.value))))
elif isinstance(src, IndexedRef):
# FIXME might not work for some dest's (that is, cls's)
self.emitter.emit(cls(self.addressing_mode_for_index(src.index)(self.get_label(src.ref.name))))
mode = self.addressing_mode_for_index(src.index)
self.emitter.emit(cls(mode(Offset(self.get_label(src.ref.name), src.offset.value))))
else:
self.emitter.emit(cls(Absolute(self.get_label(src.name))))
@ -466,7 +520,8 @@ class Compiler(object):
elif dest == REG_Y:
self.emitter.emit(INY())
elif isinstance(dest, IndexedRef):
self.emitter.emit(INC(self.addressing_mode_for_index(dest.index)(self.get_label(dest.ref.name))))
mode = self.addressing_mode_for_index(dest.index)
self.emitter.emit(INC(mode(Offset(self.get_label(dest.ref.name), dest.offset.value))))
else:
self.emitter.emit(INC(Absolute(self.get_label(dest.name))))
@ -477,105 +532,115 @@ class Compiler(object):
elif dest == REG_Y:
self.emitter.emit(DEY())
elif isinstance(dest, IndexedRef):
self.emitter.emit(DEC(self.addressing_mode_for_index(dest.index)(self.get_label(dest.ref.name))))
mode = self.addressing_mode_for_index(dest.index)
self.emitter.emit(DEC(mode(Offset(self.get_label(dest.ref.name), dest.offset.value))))
else:
self.emitter.emit(DEC(Absolute(self.get_label(dest.name))))
def compile_copy(self, instr, src, dest):
if isinstance(src, ConstantRef) and isinstance(dest, IndirectRef) and src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType):
if isinstance(src, (IndirectRef, IndexedRef)):
src_ref_type = self.get_type(src.ref)
else:
src_type = self.get_type(src)
if isinstance(dest, (IndirectRef, IndexedRef)):
dest_ref_type = self.get_type(dest.ref)
else:
dest_type = self.get_type(dest)
if isinstance(src, ConstantRef) and isinstance(dest, IndirectRef) and src_type == TYPE_BYTE and isinstance(dest_ref_type, PointerType):
### copy 123, [ptr] + y
dest_label = self.get_label(dest.ref.name)
self.emitter.emit(LDA(Immediate(Byte(src.value))))
self.emitter.emit(STA(IndirectY(dest_label)))
elif isinstance(src, LocationRef) and isinstance(dest, IndirectRef) and src.type == TYPE_BYTE and isinstance(dest.ref.type, PointerType):
elif isinstance(src, LocationRef) and isinstance(dest, IndirectRef) and src_type == TYPE_BYTE and isinstance(dest_ref_type, PointerType):
### copy b, [ptr] + y
src_label = self.get_label(src.name)
dest_label = self.get_label(dest.ref.name)
self.emitter.emit(LDA(Absolute(src_label)))
self.emitter.emit(STA(IndirectY(dest_label)))
elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef) and dest.type == TYPE_BYTE and isinstance(src.ref.type, PointerType):
elif isinstance(src, IndirectRef) and isinstance(dest, LocationRef) and dest_type == TYPE_BYTE and isinstance(src_ref_type, PointerType):
### copy [ptr] + y, b
src_label = self.get_label(src.ref.name)
dest_label = self.get_label(dest.name)
self.emitter.emit(LDA(IndirectY(src_label)))
self.emitter.emit(STA(Absolute(dest_label)))
elif isinstance(src, IndirectRef) and isinstance(dest, IndirectRef) and isinstance(src.ref.type, PointerType) and isinstance(dest.ref.type, PointerType):
elif isinstance(src, IndirectRef) and isinstance(dest, IndirectRef) and isinstance(src_ref_type, PointerType) and isinstance(dest_ref_type, PointerType):
### copy [ptra] + y, [ptrb] + y
src_label = self.get_label(src.ref.name)
dest_label = self.get_label(dest.ref.name)
self.emitter.emit(LDA(IndirectY(src_label)))
self.emitter.emit(STA(IndirectY(dest_label)))
elif isinstance(src, AddressRef) and isinstance(dest, LocationRef) and isinstance(src.ref.type, BufferType) and isinstance(dest.type, PointerType):
### copy ^buf, ptr
src_label = self.get_label(src.ref.name)
dest_label = self.get_label(dest.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))))
elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef) and src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD):
elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef) and src_type == TYPE_WORD and TableType.is_a_table_type(dest_ref_type, TYPE_WORD):
### copy w, wtab + y
src_label = self.get_label(src.name)
dest_label = self.get_label(dest.ref.name)
mode = self.addressing_mode_for_index(dest.index)
self.emitter.emit(LDA(Absolute(src_label)))
self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label)))
self.emitter.emit(STA(mode(Offset(dest_label, dest.offset.value))))
self.emitter.emit(LDA(Absolute(Offset(src_label, 1))))
self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256))))
elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef) and isinstance(src.type, VectorType) and isinstance(dest.ref.type, TableType) and isinstance(dest.ref.type.of_type, VectorType):
self.emitter.emit(STA(mode(Offset(dest_label, dest.offset.value + 256))))
elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef) and isinstance(src_type, VectorType) and isinstance(dest_ref_type, TableType) and isinstance(dest_ref_type.of_type, VectorType):
### copy vec, vtab + y
# FIXME this is the exact same as above - can this be simplified?
src_label = self.get_label(src.name)
dest_label = self.get_label(dest.ref.name)
mode = self.addressing_mode_for_index(dest.index)
self.emitter.emit(LDA(Absolute(src_label)))
self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label)))
self.emitter.emit(STA(mode(Offset(dest_label, dest.offset.value))))
self.emitter.emit(LDA(Absolute(Offset(src_label, 1))))
self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256))))
elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef) and isinstance(src.type, RoutineType) and isinstance(dest.ref.type, TableType) and isinstance(dest.ref.type.of_type, VectorType):
self.emitter.emit(STA(mode(Offset(dest_label, dest.offset.value + 256))))
elif isinstance(src, LocationRef) and isinstance(dest, IndexedRef) and isinstance(src_type, RoutineType) and isinstance(dest_ref_type, TableType) and isinstance(dest_ref_type.of_type, VectorType):
### copy routine, vtab + y
src_label = self.get_label(src.name)
dest_label = self.get_label(dest.ref.name)
mode = self.addressing_mode_for_index(dest.index)
self.emitter.emit(LDA(Immediate(HighAddressByte(src_label))))
self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label)))
self.emitter.emit(STA(mode(Offset(dest_label, dest.offset.value))))
self.emitter.emit(LDA(Immediate(LowAddressByte(src_label))))
self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256))))
elif isinstance(src, ConstantRef) and isinstance(dest, IndexedRef) and src.type == TYPE_WORD and TableType.is_a_table_type(dest.ref.type, TYPE_WORD):
self.emitter.emit(STA(mode(Offset(dest_label, dest.offset.value + 256))))
elif isinstance(src, ConstantRef) and isinstance(dest, IndexedRef) and src_type == TYPE_WORD and TableType.is_a_table_type(dest_ref_type, TYPE_WORD):
### copy 9999, wtab + y
dest_label = self.get_label(dest.ref.name)
mode = self.addressing_mode_for_index(dest.index)
self.emitter.emit(LDA(Immediate(Byte(src.low_byte()))))
self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(dest_label)))
self.emitter.emit(STA(mode(Offset(dest_label, dest.offset.value))))
self.emitter.emit(LDA(Immediate(Byte(src.high_byte()))))
self.emitter.emit(STA(self.addressing_mode_for_index(dest.index)(Offset(dest_label, 256))))
elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef) and TableType.is_a_table_type(src.ref.type, TYPE_WORD) and dest.type == TYPE_WORD:
self.emitter.emit(STA(mode(Offset(dest_label, dest.offset.value + 256))))
elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef) and TableType.is_a_table_type(src_ref_type, TYPE_WORD) and dest_type == TYPE_WORD:
### copy wtab + y, w
src_label = self.get_label(src.ref.name)
dest_label = self.get_label(dest.name)
self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(src_label)))
mode = self.addressing_mode_for_index(src.index)
self.emitter.emit(LDA(mode(Offset(src_label, src.offset.value))))
self.emitter.emit(STA(Absolute(dest_label)))
self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(Offset(src_label, 256))))
self.emitter.emit(LDA(mode(Offset(src_label, src.offset.value + 256))))
self.emitter.emit(STA(Absolute(Offset(dest_label, 1))))
elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef) and isinstance(dest.type, VectorType) and isinstance(src.ref.type, TableType) and isinstance(src.ref.type.of_type, VectorType):
elif isinstance(src, IndexedRef) and isinstance(dest, LocationRef) and isinstance(dest_type, VectorType) and isinstance(src_ref_type, TableType) and isinstance(src_ref_type.of_type, VectorType):
### copy vtab + y, vec
# FIXME this is the exact same as above - can this be simplified?
src_label = self.get_label(src.ref.name)
dest_label = self.get_label(dest.name)
self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(src_label)))
mode = self.addressing_mode_for_index(src.index)
self.emitter.emit(LDA(mode(Offset(src_label, src.offset.value))))
self.emitter.emit(STA(Absolute(dest_label)))
self.emitter.emit(LDA(self.addressing_mode_for_index(src.index)(Offset(src_label, 256))))
self.emitter.emit(LDA(mode(Offset(src_label, src.offset.value + 256))))
self.emitter.emit(STA(Absolute(Offset(dest_label, 1))))
elif src.type == TYPE_BYTE and dest.type == TYPE_BYTE and not isinstance(src, ConstantRef):
elif src_type == TYPE_BYTE and dest_type == TYPE_BYTE and not isinstance(src, ConstantRef):
### copy b1, b2
src_label = self.get_label(src.name)
dest_label = self.get_label(dest.name)
self.emitter.emit(LDA(Absolute(src_label)))
self.emitter.emit(STA(Absolute(dest_label)))
elif src.type == TYPE_WORD and dest.type == TYPE_WORD and isinstance(src, ConstantRef):
elif src_type == TYPE_WORD and dest_type == TYPE_WORD and isinstance(src, ConstantRef):
### copy 9999, w
dest_label = self.get_label(dest.name)
self.emitter.emit(LDA(Immediate(Byte(src.low_byte()))))
self.emitter.emit(STA(Absolute(dest_label)))
self.emitter.emit(LDA(Immediate(Byte(src.high_byte()))))
self.emitter.emit(STA(Absolute(Offset(dest_label, 1))))
elif src.type == TYPE_WORD and dest.type == TYPE_WORD and not isinstance(src, ConstantRef):
elif src_type == TYPE_WORD and dest_type == TYPE_WORD and not isinstance(src, ConstantRef):
### copy w1, w2
src_label = self.get_label(src.name)
dest_label = self.get_label(dest.name)
@ -583,7 +648,7 @@ class Compiler(object):
self.emitter.emit(STA(Absolute(dest_label)))
self.emitter.emit(LDA(Absolute(Offset(src_label, 1))))
self.emitter.emit(STA(Absolute(Offset(dest_label, 1))))
elif isinstance(src.type, VectorType) and isinstance(dest.type, VectorType):
elif isinstance(src_type, VectorType) and isinstance(dest_type, VectorType):
### copy v1, v2
src_label = self.get_label(src.name)
dest_label = self.get_label(dest.name)
@ -591,7 +656,7 @@ class Compiler(object):
self.emitter.emit(STA(Absolute(dest_label)))
self.emitter.emit(LDA(Absolute(Offset(src_label, 1))))
self.emitter.emit(STA(Absolute(Offset(dest_label, 1))))
elif isinstance(src.type, RoutineType) and isinstance(dest.type, VectorType):
elif isinstance(src_type, RoutineType) and isinstance(dest_type, VectorType):
### copy routine, vec
src_label = self.get_label(src.name)
dest_label = self.get_label(dest.name)
@ -600,7 +665,7 @@ class Compiler(object):
self.emitter.emit(LDA(Immediate(LowAddressByte(src_label))))
self.emitter.emit(STA(Absolute(Offset(dest_label, 1))))
else:
raise NotImplementedError(src.type)
raise NotImplementedError(src_type)
def compile_if(self, instr):
cls = {
@ -620,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)
@ -700,3 +770,18 @@ class Compiler(object):
src_label = self.get_label(location.name)
self.emitter.emit(PLA())
self.emitter.emit(STA(Absolute(src_label)))
def compile_point_into(self, instr):
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))))

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
@ -7,7 +11,8 @@ from sixtypical.model import RoutineType
class FallthruAnalyzer(object):
def __init__(self, debug=False):
def __init__(self, symtab, debug=False):
self.symtab = symtab
self.debug = debug
def analyze_program(self, program):
@ -16,7 +21,7 @@ class FallthruAnalyzer(object):
self.fallthru_map = {}
for routine in program.routines:
encountered_gotos = list(routine.encountered_gotos)
if len(encountered_gotos) == 1 and isinstance(encountered_gotos[0].type, RoutineType):
if len(encountered_gotos) == 1 and isinstance(self.symtab.fetch_global_type(encountered_gotos[0].name), RoutineType):
self.fallthru_map[routine.name] = encountered_gotos[0].name
else:
self.fallthru_map[routine.name] = None

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,53 +1,39 @@
# 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."""
class Type(object):
def __init__(self, name, max_range=None):
self.name = name
self.max_range = max_range
def __repr__(self):
return 'Type(%r)' % self.name
def __str__(self):
return self.name
def __eq__(self, other):
return isinstance(other, Type) and other.name == self.name
def __hash__(self):
return hash(self.name)
from collections import namedtuple
TYPE_BIT = Type('bit', max_range=(0, 1))
TYPE_BYTE = Type('byte', max_range=(0, 255))
TYPE_WORD = Type('word', max_range=(0, 65535))
class BitType(namedtuple('BitType', ['typename'])):
max_range = (0, 1)
def __new__(cls):
return super(BitType, cls).__new__(cls, 'bit')
TYPE_BIT = BitType()
class ByteType(namedtuple('ByteType', ['typename'])):
max_range = (0, 255)
def __new__(cls):
return super(ByteType, cls).__new__(cls, 'byte')
TYPE_BYTE = ByteType()
class RoutineType(Type):
class WordType(namedtuple('WordType', ['typename'])):
max_range = (0, 65535)
def __new__(cls):
return super(WordType, cls).__new__(cls, 'word')
TYPE_WORD = WordType()
class RoutineType(namedtuple('RoutineType', ['typename', 'inputs', 'outputs', 'trashes'])):
"""This memory location contains the code for a routine."""
def __init__(self, inputs, outputs, trashes):
self.name = 'routine'
self.inputs = inputs
self.outputs = outputs
self.trashes = trashes
max_range = (0, 0)
def __repr__(self):
return '%s(%r, inputs=%r, outputs=%r, trashes=%r)' % (
self.__class__.__name__, self.name, self.inputs, self.outputs, self.trashes
)
def __eq__(self, other):
return isinstance(other, RoutineType) and (
other.name == self.name and
other.inputs == self.inputs and
other.outputs == self.outputs and
other.trashes == self.trashes
)
def __hash__(self):
return hash(self.name) ^ hash(self.inputs) ^ hash(self.outputs) ^ hash(self.trashes)
def __new__(cls, *args):
return super(RoutineType, cls).__new__(cls, 'routine', *args)
@classmethod
def executable_types_compatible(cls_, src, dest):
@ -67,214 +53,68 @@ class RoutineType(Type):
return False
class VectorType(Type):
class VectorType(namedtuple('VectorType', ['typename', 'of_type'])):
"""This memory location contains the address of some other type (currently, only RoutineType)."""
def __init__(self, of_type):
self.name = 'vector'
self.of_type = of_type
max_range = (0, 65535)
def __repr__(self):
return '%s(%r)' % (
self.__class__.__name__, self.of_type
)
def __eq__(self, other):
return self.name == other.name and self.of_type == other.of_type
def __hash__(self):
return hash(self.name) ^ hash(self.of_type)
def __new__(cls, *args):
return super(VectorType, cls).__new__(cls, 'vector', *args)
class TableType(Type):
def __init__(self, of_type, size):
self.of_type = of_type
self.size = size
self.name = '{} table[{}]'.format(self.of_type.name, self.size)
class TableType(namedtuple('TableType', ['typename', 'of_type', 'size'])):
def __repr__(self):
return '%s(%r, %r)' % (
self.__class__.__name__, self.of_type, self.size
)
def __new__(cls, *args):
return super(TableType, cls).__new__(cls, 'table', *args)
@property
def max_range(self):
return (0, self.size - 1)
@classmethod
def is_a_table_type(cls_, x, of_type):
return isinstance(x, TableType) and x.of_type == of_type
class BufferType(Type):
def __init__(self, size):
self.size = size
self.name = 'buffer[%s]' % self.size
class PointerType(namedtuple('PointerType', ['typename'])):
max_range = (0, 65535)
def __new__(cls):
return super(PointerType, cls).__new__(cls, 'pointer')
class PointerType(Type):
def __init__(self):
self.name = 'pointer'
# --------------------------------------------------------
class Ref(object):
def is_constant(self):
"""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."""
raise NotImplementedError("class {} must implement is_constant()".format(self.__class__.__name__))
def max_range(self):
raise NotImplementedError("class {} must implement max_range()".format(self.__class__.__name__))
class LocationRef(Ref):
def __init__(self, type, name):
self.type = type
self.name = name
def __eq__(self, other):
# Ordinarily there will only be one ref with a given name,
# but because we store the type in here and we want to treat
# these objects as immutable, we compare the types, too,
# just to be sure.
equal = isinstance(other, self.__class__) and other.name == self.name
if equal:
assert other.type == self.type, repr((self, other))
return equal
def __hash__(self):
return hash(self.name + str(self.type))
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.type, self.name)
def __str__(self):
return "{}:{}".format(self.name, self.type)
def is_constant(self):
return isinstance(self.type, RoutineType)
def max_range(self):
try:
return self.type.max_range
except:
return (0, 0)
class LocationRef(namedtuple('LocationRef', ['reftype', 'name'])):
def __new__(cls, *args):
return super(LocationRef, cls).__new__(cls, 'location', *args)
@classmethod
def format_set(cls, location_refs):
return '{%s}' % ', '.join([str(loc) for loc in sorted(location_refs, key=lambda x: x.name)])
class IndirectRef(Ref):
def __init__(self, ref):
self.ref = ref
def __eq__(self, other):
return isinstance(other, self.__class__) and self.ref == other.ref
def __hash__(self):
return hash(self.__class__.name) ^ hash(self.ref)
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.ref)
class IndirectRef(namedtuple('IndirectRef', ['reftype', 'ref'])):
def __new__(cls, *args):
return super(IndirectRef, cls).__new__(cls, 'indirect', *args)
@property
def name(self):
return '[{}]+y'.format(self.ref.name)
def is_constant(self):
return False
class IndexedRef(Ref):
def __init__(self, ref, index):
self.ref = ref
self.index = index
def __eq__(self, other):
return isinstance(other, self.__class__) and self.ref == other.ref and self.index == other.index
def __hash__(self):
return hash(self.__class__.name) ^ hash(self.ref) ^ hash(self.index)
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.ref, self.index)
class IndexedRef(namedtuple('IndexedRef', ['reftype', 'ref', 'offset', 'index'])):
def __new__(cls, *args):
return super(IndexedRef, cls).__new__(cls, 'indexed', *args)
@property
def name(self):
return '{}+{}'.format(self.ref.name, self.index.name)
def is_constant(self):
return False
return '{}+{}+{}'.format(self.ref.name, self.offset, self.index.name)
class AddressRef(Ref):
def __init__(self, ref):
self.ref = ref
def __eq__(self, other):
return self.ref == other.ref
def __hash__(self):
return hash(self.__class__.name) ^ hash(self.ref)
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.ref)
@property
def name(self):
return '^{}'.format(self.ref.name)
def is_constant(self):
return True
class PartRef(Ref):
"""For 'low byte of' location and 'high byte of' location modifiers.
height=0 = low byte, height=1 = high byte.
NOTE: Not actually used yet. Might require more thought before it's usable.
"""
def __init__(self, ref, height):
assert isinstance(ref, Ref)
assert ref.type == TYPE_WORD
self.ref = ref
self.height = height
self.type = TYPE_BYTE
def __eq__(self, other):
return isinstance(other, PartRef) and (
other.height == self.height and other.ref == self.ref
)
def __hash__(self):
return hash(self.ref) ^ hash(self.height) ^ hash(self.type)
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.ref, self.height)
def is_constant(self):
return self.ref.is_constant()
class ConstantRef(Ref):
def __init__(self, type, value):
self.type = type
self.value = value
def __eq__(self, other):
return isinstance(other, ConstantRef) and (
other.type == self.type and other.value == self.value
)
def __hash__(self):
return hash(str(self.value) + str(self.type))
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.type, self.value)
def is_constant(self):
return True
def max_range(self):
return (self.value, self.value)
class ConstantRef(namedtuple('ConstantRef', ['reftype', 'type', 'value'])):
def __new__(cls, *args):
return super(ConstantRef, cls).__new__(cls, 'constant', *args)
def high_byte(self):
return (self.value >> 8) & 255
@ -296,12 +136,16 @@ class ConstantRef(Ref):
value -= 256
return ConstantRef(self.type, value)
@property
def name(self):
return 'constant({})'.format(self.value)
REG_A = LocationRef(TYPE_BYTE, 'a')
REG_X = LocationRef(TYPE_BYTE, 'x')
REG_Y = LocationRef(TYPE_BYTE, 'y')
FLAG_Z = LocationRef(TYPE_BIT, 'z')
FLAG_C = LocationRef(TYPE_BIT, 'c')
FLAG_N = LocationRef(TYPE_BIT, 'n')
FLAG_V = LocationRef(TYPE_BIT, 'v')
REG_A = LocationRef('a')
REG_X = LocationRef('x')
REG_Y = LocationRef('y')
FLAG_Z = LocationRef('z')
FLAG_C = LocationRef('c')
FLAG_N = LocationRef('n')
FLAG_V = LocationRef('v')

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,21 +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, If, Repeat, For, WithInterruptsOff, Save
from sixtypical.ast import (
Program, Defn, Routine, Block, SingleOp, Reset, Call, GoTo, If, Repeat, For, WithInterruptsOff, Save, PointInto
)
from sixtypical.model import (
TYPE_BIT, TYPE_BYTE, TYPE_WORD,
RoutineType, VectorType, TableType, BufferType, PointerType,
LocationRef, ConstantRef, IndirectRef, IndexedRef, AddressRef,
RoutineType, VectorType, TableType, PointerType,
ConstantRef, IndirectRef, IndexedRef,
)
from sixtypical.scanner import Scanner
class SymEntry(object):
def __init__(self, ast_node, model):
self.ast_node = ast_node
self.model = model
def __repr__(self):
return "%s(%r, %r)" % (self.__class__.__name__, self.ast_node, self.model)
from sixtypical.symtab import SymEntry
class ForwardReference(object):
@ -26,77 +24,62 @@ class ForwardReference(object):
return "%s(%r)" % (self.__class__.__name__, self.name)
class ParsingContext(object):
def __init__(self):
self.symbols = {} # token -> SymEntry
self.statics = {} # token -> SymEntry
self.typedefs = {} # token -> Type AST
self.consts = {} # token -> Loc
for token in ('a', 'x', 'y'):
self.symbols[token] = SymEntry(None, LocationRef(TYPE_BYTE, token))
for token in ('c', 'z', 'n', 'v'):
self.symbols[token] = SymEntry(None, LocationRef(TYPE_BIT, token))
def __str__(self):
return "Symbols: {}\nStatics: {}\nTypedefs: {}\nConsts: {}".format(self.symbols, self.statics, self.typedefs, self.consts)
def fetch(self, name):
if name in self.statics:
return self.statics[name].model
if name in self.symbols:
return self.symbols[name].model
return None
class Parser(object):
def __init__(self, context, text, filename):
self.context = context
def __init__(self, symtab, text, filename, include_path):
self.symtab = symtab
self.include_path = include_path
self.scanner = Scanner(text, filename)
self.current_routine_name = None
def syntax_error(self, msg):
self.scanner.syntax_error(msg)
def lookup(self, name):
model = self.context.fetch(name)
def lookup(self, name, allow_forward=False, routine_name=None):
model = self.symtab.fetch_global_ref(name)
if model is None and routine_name:
model = self.symtab.fetch_local_ref(routine_name, name)
if model is None and allow_forward:
return ForwardReference(name)
if model is None:
self.syntax_error('Undefined symbol "{}"'.format(name))
return model
def declare(self, name, symentry, static=False):
if self.context.fetch(name):
def declare(self, name, ast_node, type_):
if self.symtab.fetch_global_ref(name):
self.syntax_error('Symbol "%s" already declared' % name)
if static:
self.context.statics[name] = symentry
else:
self.context.symbols[name] = symentry
self.symtab.symbols[name] = SymEntry(ast_node, type_)
def clear_statics(self):
self.context.statics = {}
def declare_local(self, routine_name, name, ast_node, type_):
if self.symtab.fetch_local_ref(routine_name, name):
self.syntax_error('Symbol "%s" already declared locally' % name)
if self.symtab.fetch_global_ref(name):
self.syntax_error('Symbol "%s" already declared globally' % name)
self.symtab.locals.setdefault(routine_name, {})[name] = SymEntry(ast_node, type_)
# ---- symbol resolution
def resolve_symbols(self, program):
# This could stand to be better unified.
def backpatch_constraint_labels(type_):
def resolve(w):
if not isinstance(w, ForwardReference):
return w
return self.lookup(w.name)
if isinstance(type_, TableType):
backpatch_constraint_labels(type_.of_type)
elif isinstance(type_, VectorType):
backpatch_constraint_labels(type_.of_type)
elif isinstance(type_, RoutineType):
type_.inputs = set([resolve(w) for w in type_.inputs])
type_.outputs = set([resolve(w) for w in type_.outputs])
type_.trashes = set([resolve(w) for w in type_.trashes])
def resolve(w):
return self.lookup(w.name) if isinstance(w, ForwardReference) else w
for defn in program.defns:
backpatch_constraint_labels(defn.location.type)
for routine in program.routines:
backpatch_constraint_labels(routine.location.type)
def backpatched_type(type_):
if isinstance(type_, TableType):
return TableType(backpatched_type(type_.of_type), type_.size)
elif isinstance(type_, VectorType):
return VectorType(backpatched_type(type_.of_type))
elif isinstance(type_, RoutineType):
return RoutineType(
frozenset([resolve(w) for w in type_.inputs]),
frozenset([resolve(w) for w in type_.outputs]),
frozenset([resolve(w) for w in type_.trashes]),
)
else:
return type_
for name, symentry in self.symtab.symbols.items():
symentry.type_ = backpatched_type(symentry.type_)
def resolve_fwd_reference(obj, field):
field_value = getattr(obj, field, None)
@ -108,35 +91,51 @@ class Parser(object):
for node in program.all_children():
if isinstance(node, SingleOp):
resolve_fwd_reference(node, 'location')
resolve_fwd_reference(node, 'src')
resolve_fwd_reference(node, 'dest')
if isinstance(node, (Call, GoTo)):
resolve_fwd_reference(node, 'location')
# --- grammar productions
def program(self):
defns = []
routines = []
includes = []
while self.scanner.consume('include'):
filename = self.scanner.token
self.scanner.scan()
program = load_program(filename, self.symtab, self.include_path, include_file=True)
includes.append(program)
while self.scanner.on('typedef', 'const'):
if self.scanner.on('typedef'):
self.typedef()
if self.scanner.on('const'):
self.defn_const()
typenames = ['byte', 'word', 'table', 'vector', 'buffer', 'pointer'] # 'routine',
typenames.extend(self.context.typedefs.keys())
typenames = ['byte', 'word', 'table', 'vector', 'pointer'] # 'routine',
typenames.extend(self.symtab.typedefs.keys())
while self.scanner.on(*typenames):
defn = self.defn()
self.declare(defn.name, SymEntry(defn, defn.location))
type_, defn = self.defn()
self.declare(defn.name, defn, type_)
defns.append(defn)
while self.scanner.consume('define'):
name = self.scanner.token
self.scanner.scan()
routine = self.routine(name)
self.declare(name, SymEntry(routine, routine.location))
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
@ -144,18 +143,18 @@ class Parser(object):
self.scanner.expect('typedef')
type_ = self.defn_type()
name = self.defn_name()
if name in self.context.typedefs:
if name in self.symtab.typedefs:
self.syntax_error('Type "%s" already declared' % name)
self.context.typedefs[name] = type_
self.symtab.typedefs[name] = type_
return type_
def defn_const(self):
self.scanner.expect('const')
name = self.defn_name()
if name in self.context.consts:
if name in self.symtab.consts:
self.syntax_error('Const "%s" already declared' % name)
loc = self.const()
self.context.consts[name] = loc
self.symtab.consts[name] = loc
return loc
def defn(self):
@ -185,9 +184,7 @@ class Parser(object):
if initial is not None and addr is not None:
self.syntax_error("Definition cannot have both initial value and explicit address")
location = LocationRef(type_, name)
return Defn(self.scanner.line_number, name=name, addr=addr, initial=initial, location=location)
return type_, Defn(self.scanner.line_number, name=name, addr=addr, initial=initial)
def const(self):
if self.scanner.token in ('on', 'off'):
@ -204,8 +201,8 @@ class Parser(object):
loc = ConstantRef(TYPE_WORD, int(self.scanner.token))
self.scanner.scan()
return loc
elif self.scanner.token in self.context.consts:
loc = self.context.consts[self.scanner.token]
elif self.scanner.token in self.symtab.consts:
loc = self.symtab.consts[self.scanner.token]
self.scanner.scan()
return loc
else:
@ -222,8 +219,8 @@ class Parser(object):
if self.scanner.consume('table'):
size = self.defn_size()
if size <= 0 or size > 256:
self.syntax_error("Table size must be > 0 and <= 256")
if size <= 0 or size > 65536:
self.syntax_error("Table size must be > 0 and <= 65536")
type_ = TableType(type_, size)
return type_
@ -247,18 +244,15 @@ class Parser(object):
type_ = VectorType(type_)
elif self.scanner.consume('routine'):
(inputs, outputs, trashes) = self.constraints()
type_ = RoutineType(inputs=inputs, outputs=outputs, trashes=trashes)
elif self.scanner.consume('buffer'):
size = self.defn_size()
type_ = BufferType(size)
type_ = RoutineType(frozenset(inputs), frozenset(outputs), frozenset(trashes))
elif self.scanner.consume('pointer'):
type_ = PointerType()
else:
type_name = self.scanner.token
self.scanner.scan()
if type_name not in self.context.typedefs:
if type_name not in self.symtab.typedefs:
self.syntax_error("Undefined type '%s'" % type_name)
type_ = self.context.typedefs[type_name]
type_ = self.symtab.typedefs[type_name]
return type_
@ -287,29 +281,18 @@ class Parser(object):
def routine(self, name):
type_ = self.defn_type()
if not isinstance(type_, RoutineType):
self.syntax_error("Can only define a routine, not %r" % type_)
statics = []
self.syntax_error("Can only define a routine, not {}".format(repr(type_)))
locals_ = []
if self.scanner.consume('@'):
self.scanner.check_type('integer literal')
block = None
addr = int(self.scanner.token)
self.scanner.scan()
else:
statics = self.statics()
self.clear_statics()
for defn in statics:
self.declare(defn.name, SymEntry(defn, defn.location), static=True)
locals_ = self.locals()
block = self.block()
self.clear_statics()
addr = None
location = LocationRef(type_, name)
return Routine(
self.scanner.line_number,
name=name, block=block, addr=addr,
location=location, statics=statics
)
return type_, Routine(self.scanner.line_number, name=name, block=block, addr=addr, locals=locals_)
def labels(self):
accum = []
@ -333,16 +316,12 @@ class Parser(object):
return accum
def locexpr(self):
if self.scanner.token in ('on', 'off', 'word') or self.scanner.token in self.context.consts or self.scanner.on_type('integer literal'):
if self.scanner.token in ('on', 'off', 'word') or self.scanner.token in self.symtab.consts or self.scanner.on_type('integer literal'):
return self.const()
else:
name = self.scanner.token
self.scanner.scan()
loc = self.context.fetch(name)
if loc:
return loc
else:
return ForwardReference(name)
return self.lookup(name, allow_forward=True, routine_name=self.current_routine_name)
def indlocexpr(self):
if self.scanner.consume('['):
@ -351,9 +330,6 @@ class Parser(object):
self.scanner.expect('+')
self.scanner.expect('y')
return IndirectRef(loc)
elif self.scanner.consume('^'):
loc = self.locexpr()
return AddressRef(loc)
else:
return self.indexed_locexpr()
@ -361,17 +337,28 @@ class Parser(object):
loc = self.locexpr()
if not isinstance(loc, str):
index = None
offset = ConstantRef(TYPE_BYTE, 0)
if self.scanner.consume('+'):
if self.scanner.token in self.symtab.consts or self.scanner.on_type('integer literal'):
offset = self.const()
self.scanner.expect('+')
index = self.locexpr()
loc = IndexedRef(loc, index)
loc = IndexedRef(loc, offset, index)
return loc
def statics(self):
def locals(self):
defns = []
while self.scanner.consume('static'):
defn = self.defn()
type_, defn = self.defn()
if defn.initial is None:
self.syntax_error("Static definition {} must have initial value".format(defn))
self.declare_local(self.current_routine_name, defn.name, defn, type_)
defns.append(defn)
while self.scanner.consume('local'):
type_, defn = self.defn()
if defn.initial is not None:
self.syntax_error("Local definition {} may not have initial value".format(defn))
self.declare_local(self.current_routine_name, defn.name, defn, type_)
defns.append(defn)
return defns
@ -380,7 +367,7 @@ class Parser(object):
self.scanner.expect('{')
while not self.scanner.on('}'):
instrs.append(self.instr())
if isinstance(instrs[-1], SingleOp) and instrs[-1].opcode == 'goto':
if isinstance(instrs[-1], GoTo):
break
self.scanner.expect('}')
return Block(self.scanner.line_number, instrs=instrs)
@ -419,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
@ -450,12 +441,15 @@ class Parser(object):
opcode = self.scanner.token
self.scanner.scan()
return SingleOp(self.scanner.line_number, opcode=opcode, dest=None, src=None)
elif self.scanner.token in ("call", "goto"):
opcode = self.scanner.token
self.scanner.scan()
elif self.scanner.consume("call"):
name = self.scanner.token
self.scanner.scan()
instr = SingleOp(self.scanner.line_number, opcode=opcode, location=ForwardReference(name), dest=None, src=None)
instr = Call(self.scanner.line_number, location=ForwardReference(name))
return instr
elif self.scanner.consume("goto"):
name = self.scanner.token
self.scanner.scan()
instr = GoTo(self.scanner.line_number, location=ForwardReference(name))
return instr
elif self.scanner.token in ("copy",):
opcode = self.scanner.token
@ -474,6 +468,12 @@ class Parser(object):
locations = self.locexprs()
block = self.block()
return Save(self.scanner.line_number, locations=locations, block=block)
elif self.scanner.consume("point"):
pointer = self.locexpr()
self.scanner.expect("into")
table = self.locexpr()
block = self.block()
return PointInto(self.scanner.line_number, pointer=pointer, table=table, block=block)
elif self.scanner.consume("trash"):
dest = self.locexpr()
return SingleOp(self.scanner.line_number, opcode='trash', src=None, dest=dest)
@ -484,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.
@ -385,6 +416,68 @@ Some instructions on tables. (3/3)
= $081B DEC $081F,X
= $081E RTS
Using a constant offset, you can read and write entries in
the table beyond the 256th.
| byte one
| byte table[1024] many
|
| define main routine
| inputs many
| outputs many
| trashes a, x, n, z
| {
| ld x, 0
| ld a, many + x
| st a, many + x
| ld a, many + 999 + x
| st a, many + 1000 + x
| }
= $080D LDX #$00
= $080F LDA $081D,X
= $0812 STA $081D,X
= $0815 LDA $0C04,X
= $0818 STA $0C05,X
= $081B RTS
Instructions on tables, with constant offsets.
| byte table[256] many
|
| define main routine
| inputs many
| outputs many
| trashes a, x, c, n, z, v
| {
| ld x, 0
| ld a, 0
| st off, c
| add a, many + 1 + x
| sub a, many + 2 + x
| cmp a, many + 3 + x
| and a, many + 4 + x
| or a, many + 5 + x
| xor a, many + 6 + x
| shl many + 7 + x
| shr many + 8 + x
| inc many + 9 + x
| dec many + 10 + x
| }
= $080D LDX #$00
= $080F LDA #$00
= $0811 CLC
= $0812 ADC $0832,X
= $0815 SBC $0833,X
= $0818 CMP $0834,X
= $081B AND $0835,X
= $081E ORA $0836,X
= $0821 EOR $0837,X
= $0824 ROL $0838,X
= $0827 ROR $0839,X
= $082A INC $083A,X
= $082D DEC $083B,X
= $0830 RTS
Compiling 16-bit `cmp`.
| word za @ 60001
@ -551,7 +644,6 @@ Compiling `repeat forever`.
= $080D LDY #$41
= $080F INY
= $0810 JMP $080F
= $0813 RTS
The body of `repeat forever` can be empty.
@ -561,7 +653,6 @@ The body of `repeat forever` can be empty.
| } forever
| }
= $080D JMP $080D
= $0810 RTS
Compiling `for ... up to`.
@ -876,6 +967,42 @@ Copy routine (by forward reference) to vector.
= $0818 INX
= $0819 RTS
Copy byte to byte table and back, with both `x` and `y` as indexes,
plus constant offsets.
| byte one
| byte table[256] many
|
| define main routine
| inputs one, many
| outputs one, many
| trashes a, x, y, n, z
| {
| ld x, 0
| ld y, 0
| ld a, 77
| st a, many + x
| st a, many + y
| st a, many + 1 + x
| st a, many + 1 + y
| ld a, many + x
| ld a, many + y
| ld a, many + 8 + x
| ld a, many + 8 + y
| }
= $080D LDX #$00
= $080F LDY #$00
= $0811 LDA #$4D
= $0813 STA $082D,X
= $0816 STA $082D,Y
= $0819 STA $082E,X
= $081C STA $082E,Y
= $081F LDA $082D,X
= $0822 LDA $082D,Y
= $0825 LDA $0835,X
= $0828 LDA $0835,Y
= $082B RTS
Copy word to word table and back, with both `x` and `y` as indexes.
| word one
@ -918,7 +1045,49 @@ Copy word to word table and back, with both `x` and `y` as indexes.
= $0848 STA $084D
= $084B RTS
Indirect call.
Copy word to word table and back, with constant offsets.
| word one
| word table[256] many
|
| define main routine
| inputs one, many
| outputs one, many
| trashes a, x, y, n, z
| {
| ld x, 0
| ld y, 0
| copy 777, one
| copy one, many + 1 + x
| copy one, many + 2 + y
| copy many + 3 + x, one
| copy many + 4 + y, one
| }
= $080D LDX #$00
= $080F LDY #$00
= $0811 LDA #$09
= $0813 STA $084C
= $0816 LDA #$03
= $0818 STA $084D
= $081B LDA $084C
= $081E STA $084F,X
= $0821 LDA $084D
= $0824 STA $094F,X
= $0827 LDA $084C
= $082A STA $0850,Y
= $082D LDA $084D
= $0830 STA $0950,Y
= $0833 LDA $0851,X
= $0836 STA $084C
= $0839 LDA $0951,X
= $083C STA $084D
= $083F LDA $0852,Y
= $0842 STA $084C
= $0845 LDA $0952,Y
= $0848 STA $084D
= $084B RTS
Indirect call. TODO: we don't need the final RTS here, omit it.
| vector routine
| outputs x
@ -939,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`.
@ -1002,28 +1170,77 @@ 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
= $0816 STA $0846
= $0819 LDA #$3E
= $081B STA $0847,X
= $081E LDA #$08
= $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.
| vector routine
| outputs x
| trashes a, z, n
| one
| vector routine
| outputs x
| trashes a, z, n
| table[256] many
|
| define bar routine outputs x trashes a, z, n {
| ld x, 200
| }
|
| define main routine
| inputs one, many
| outputs one, many
| trashes a, x, n, z
| {
| ld x, 0
| copy bar, one
| copy bar, many + 1 + x
| copy one, many + 2 + x
| copy many + 3 + x, one
| call one
| }
= $080D LDX #$00
= $080F LDA #$3E
= $0811 STA $0845
= $0814 LDA #$08
= $0816 STA $0846
= $0819 LDA #$3E
= $081B STA $0848,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
= $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
@ -1207,20 +1424,39 @@ Subtracting a word memory location from another word memory location.
= $081D STA $0822
= $0820 RTS
### Buffers and Pointers
### Tables and Pointers
Load address into pointer.
Associate pointer with table. Does nothing by itself.
| buffer[2048] buf
| byte table[256] tab
| pointer ptr @ 254
|
| define main routine
| inputs buf
| outputs buf, y
| inputs tab
| outputs tab, y
| trashes a, z, n, ptr
| {
| ld y, 0
| copy ^buf, ptr
| point ptr into tab {
| }
| }
= $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
@ -1231,17 +1467,19 @@ Load address into pointer.
Write literal through a pointer.
| buffer[2048] buf
| byte table[256] tab
| pointer ptr @ 254
|
| define main routine
| inputs buf
| outputs buf, y
| inputs tab
| outputs tab, y
| trashes a, z, n, ptr
| {
| ld y, 0
| copy ^buf, ptr
| copy 123, [ptr] + y
| point ptr into tab {
| reset ptr 0
| copy 123, [ptr] + y
| }
| }
= $080D LDY #$00
= $080F LDA #$1C
@ -1254,43 +1492,47 @@ Write literal through a pointer.
Write stored value through a pointer.
| buffer[2048] buf
| byte table[256] tab
| pointer ptr @ 254
| byte foo
|
| define main routine
| inputs foo, buf
| outputs y, buf
| inputs foo, tab
| outputs y, tab
| trashes a, z, n, ptr
| {
| ld y, 0
| copy ^buf, ptr
| copy foo, [ptr] + y
| point ptr into tab {
| reset ptr 0
| copy foo, [ptr] + y
| }
| }
= $080D LDY #$00
= $080F LDA #$1D
= $0811 STA $FE
= $0813 LDA #$08
= $0815 STA $FF
= $0817 LDA $101D
= $0817 LDA $091D
= $081A STA ($FE),Y
= $081C RTS
Read through a pointer, into a byte storage location, or the `a` register.
| buffer[2048] buf
| byte table[256] tab
| pointer ptr @ 254
| byte foo
|
| define main routine
| inputs buf
| inputs tab
| outputs y, foo
| trashes a, z, n, ptr
| {
| ld y, 0
| copy ^buf, ptr
| copy [ptr] + y, foo
| ld a, [ptr] + y
| point ptr into tab {
| reset ptr 0
| copy [ptr] + y, foo
| ld a, [ptr] + y
| }
| }
= $080D LDY #$00
= $080F LDA #$1F
@ -1298,25 +1540,62 @@ Read through a pointer, into a byte storage location, or the `a` register.
= $0813 LDA #$08
= $0815 STA $FF
= $0817 LDA ($FE),Y
= $0819 STA $101F
= $0819 STA $091F
= $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.
| buffer[2048] buf
| byte table[256] tab
| pointer ptra @ 252
| pointer ptrb @ 254
|
| define main routine
| inputs buf
| outputs buf
| inputs tab
| outputs tab
| trashes a, y, z, n, ptra, ptrb
| {
| ld y, 0
| copy ^buf, ptra
| copy ^buf, ptrb
| copy [ptra] + y, [ptrb] + y
| point ptra into tab {
| reset ptra 0
| point ptrb into tab {
| reset ptrb 0
| copy [ptra] + y, [ptrb] + y
| }
| }
| }
= $080D LDY #$00
= $080F LDA #$24
@ -1333,19 +1612,21 @@ Read and write through two pointers.
Write the `a` register through a pointer.
| buffer[2048] buf
| byte table[256] tab
| pointer ptr @ 254
| byte foo
|
| define main routine
| inputs buf
| outputs buf
| inputs tab
| outputs tab
| trashes a, y, z, n, ptr
| {
| ld y, 0
| copy ^buf, ptr
| ld a, 255
| st a, [ptr] + y
| point ptr into tab {
| reset ptr 0
| ld a, 255
| st a, [ptr] + y
| }
| }
= $080D LDY #$00
= $080F LDA #$1C
@ -1359,28 +1640,30 @@ Write the `a` register through a pointer.
Add a word memory location, and a literal word, to a pointer, and then read through it.
Note that this is *not* range-checked. (Yet.)
| buffer[2048] buf
| byte table[256] tab
| pointer ptr @ 254
| byte foo
| word delta
|
| define main routine
| inputs buf
| inputs tab
| outputs y, foo, delta
| trashes a, c, v, z, n, ptr
| {
| copy 619, delta
| ld y, 0
| st off, c
| copy ^buf, ptr
| add ptr, delta
| add ptr, word 1
| copy [ptr] + y, foo
| point ptr into tab {
| reset ptr 0
| add ptr, delta
| add ptr, word 1
| copy [ptr] + y, foo
| }
| }
= $080D LDA #$6B
= $080F STA $1043
= $080F STA $0943
= $0812 LDA #$02
= $0814 STA $1044
= $0814 STA $0944
= $0817 LDY #$00
= $0819 CLC
= $081A LDA #$42
@ -1388,10 +1671,10 @@ Note that this is *not* range-checked. (Yet.)
= $081E LDA #$08
= $0820 STA $FF
= $0822 LDA $FE
= $0824 ADC $1043
= $0824 ADC $0943
= $0827 STA $FE
= $0829 LDA $FF
= $082B ADC $1044
= $082B ADC $0944
= $082E STA $FF
= $0830 LDA $FE
= $0832 ADC #$01
@ -1400,7 +1683,7 @@ Note that this is *not* range-checked. (Yet.)
= $0838 ADC #$00
= $083A STA $FF
= $083C LDA ($FE),Y
= $083E STA $1042
= $083E STA $0942
= $0841 RTS
### Trash
@ -1420,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
@ -1443,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 {
@ -163,6 +178,9 @@ Basic "open-faced for" loops, up and down.
Other blocks.
| byte table[256] tab
| pointer ptr
|
| define main routine trashes a, x, c, z, v {
| with interrupts off {
| save a, x, c {
@ -172,6 +190,10 @@ Other blocks.
| save a, x, c {
| ld a, 0
| }
| point ptr into tab {
| reset ptr 0
| ld a, [ptr] + y
| }
| }
= ok
@ -180,7 +202,7 @@ User-defined memory addresses of different types.
| byte byt
| word wor
| vector routine trashes a vec
| buffer[2048] buf
| byte table[2048] buf
| pointer ptr
|
| define main routine {
@ -192,6 +214,8 @@ Tables of different types and some operations on them.
| byte table[256] many
| word table[256] wmany
| vector (routine trashes a) table[256] vmany
| byte bval
| word wval
|
| define main routine {
| ld x, 0
@ -207,11 +231,48 @@ Tables of different types and some operations on them.
| shr many + x
| inc many + x
| dec many + x
| ld a, many + x
| st a, many + x
| copy wval, wmany + x
| copy wmany + x, wval
| }
= ok
Indexing with an offset in some tables.
| byte table[256] many
| word table[256] wmany
| byte bval
| word wval
|
| define main routine {
| ld x, 0
| ld a, 0
| st off, c
| add a, many + 100 + x
| sub a, many + 100 + x
| cmp a, many + 100 + x
| and a, many + 100 + x
| or a, many + 100 + x
| xor a, many + 100 + x
| shl many + 100 + x
| shr many + 100 + x
| inc many + 100 + x
| dec many + 100 + x
| ld a, many + 100 + x
| st a, many + 100 + x
| copy wval, wmany + 100 + x
| copy wmany + 100 + x, wval
| }
= ok
The number of entries in a table must be
greater than 0 and less than or equal to 256.
greater than 0 and less than or equal to 65536.
(In previous versions, a table could have at
most 256 entries. They can now have more, however
the offset-access syntax can only access the
first 256. To access more, a pointer is required.)
| word table[512] many
|
@ -223,6 +284,30 @@ greater than 0 and less than or equal to 256.
| ld x, 0
| copy 9999, many + x
| }
= ok
| byte table[65536] many
|
| define main routine
| inputs many
| outputs many
| trashes a, x, n, z
| {
| ld x, 0
| copy 99, many + x
| }
= ok
| byte table[65537] many
|
| define main routine
| inputs many
| outputs many
| trashes a, x, n, z
| {
| ld x, 0
| copy 99, many + x
| }
? SyntaxError
| word table[0] many
@ -285,6 +370,19 @@ Constants.
| }
= ok
Named constants can be used as offsets.
| const lives 3
| const w1 1000
|
| byte table[w1] those
|
| define main routine {
| ld y, 0
| ld a, those + lives + y
| }
= ok
Can't have two constants with the same name.
| const w1 1000
@ -590,18 +688,20 @@ But you can't `goto` a label that never gets defined.
| }
? Expected '}', but found 'ld'
Buffers and pointers.
Tables and pointers.
| buffer[2048] buf
| byte table[2048] buf
| pointer ptr
| pointer ptrb
| byte foo
|
| define main routine {
| copy ^buf, ptr
| copy 123, [ptr] + y
| copy [ptr] + y, foo
| copy [ptr] + y, [ptrb] + y
| point ptr into buf {
| reset ptr 0
| copy 123, [ptr] + y
| copy [ptr] + y, foo
| copy [ptr] + y, [ptrb] + y
| }
| }
= ok
@ -663,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
@ -679,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
@ -699,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
|
@ -707,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
@ -719,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"