Compare commits

...

100 Commits

Author SHA1 Message Date
A2 Geek
5981f65db1
Merge pull request #30 from AppleCommander/dependabot/gradle/net.sf.applecommander-AppleCommander-1.9.0
Bump net.sf.applecommander:AppleCommander from 1.8.0 to 1.9.0
2023-11-08 23:57:41 -06:00
dependabot[bot]
459394d1a9
Bump net.sf.applecommander:AppleCommander from 1.8.0 to 1.9.0
Bumps [net.sf.applecommander:AppleCommander](https://github.com/AppleCommander/AppleCommander) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/AppleCommander/AppleCommander/releases)
- [Commits](https://github.com/AppleCommander/AppleCommander/compare/1.8.0...1.9.0)

---
updated-dependencies:
- dependency-name: net.sf.applecommander:AppleCommander
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-06 00:13:48 +00:00
Rob Greene
3d936a435c Fixing Maven publish artifactId. 2023-10-31 18:27:37 -05:00
Rob Greene
797224c65a Bumping minor version since there is an enhancement. 2023-10-30 12:59:39 -05:00
Rob Greene
87336e7d97 Fixing typo. 2023-10-30 12:59:09 -05:00
Rob Greene
c55b15b505 Merge remote-tracking branch 'origin/master' 2023-10-30 12:53:57 -05:00
Rob Greene
5e72a68c42 Adding a "wrapper" capability to help with DOS rewriting application. #24 2023-10-30 12:53:33 -05:00
Rob Greene
778c094b7b Adding a "wrapper" capability to help with DOS rewriting application. #124 2023-10-29 22:43:32 -05:00
Rob Greene
089356a3a2 Adding note regarding DOS vs ProDOS for '$embed'. #24 2023-10-29 14:38:15 -05:00
Rob Greene
02269e8abb Using the same version of Gradle across all AppleCommander projects. #21 2023-10-29 14:35:21 -05:00
Rob Greene
ca87be4733 Updating version of Picocli. 2023-10-29 13:55:46 -05:00
A2 Geek
848468ed21
Merge pull request #26 from AppleCommander/dependabot/gradle/net.sf.applecommander-AppleCommander-1.8.0
Bump net.sf.applecommander:AppleCommander from 1.4.0 to 1.8.0
2023-10-29 13:48:36 -05:00
A2 Geek
f61fdbac27
Create gradle.yml 2023-10-29 13:45:29 -05:00
Rob Greene
d84d4ca1e8 Correcting call when moving to call the move code instead of the embedded code. Related to #24. 2023-10-29 13:41:44 -05:00
Rob Greene
81e1fa0ce3 Bumping to next snapshot version. 2023-10-29 13:41:08 -05:00
dependabot[bot]
8e8bf72618
Bump net.sf.applecommander:AppleCommander from 1.4.0 to 1.8.0
Bumps [net.sf.applecommander:AppleCommander](https://github.com/AppleCommander/AppleCommander) from 1.4.0 to 1.8.0.
- [Release notes](https://github.com/AppleCommander/AppleCommander/releases)
- [Commits](https://github.com/AppleCommander/AppleCommander/commits/1.8.0)

---
updated-dependencies:
- dependency-name: net.sf.applecommander:AppleCommander
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-28 17:54:35 +00:00
A2 Geek
baabfead2c
Merge pull request #25 from AppleCommander/dependabot/gradle/net.sf.applecommander-applesingle-api-1.2.2
Bump net.sf.applecommander:applesingle-api from 1.2.1 to 1.2.2
2023-10-28 12:53:45 -05:00
A2 Geek
d59a033550
Merge pull request #29 from AppleCommander/dependabot/gradle/junit-junit-4.13.2
Bump junit:junit from 4.12 to 4.13.2
2023-10-28 12:53:19 -05:00
dependabot[bot]
013790a610
Bump junit:junit from 4.12 to 4.13.2
Bumps [junit:junit](https://github.com/junit-team/junit4) from 4.12 to 4.13.2.
- [Release notes](https://github.com/junit-team/junit4/releases)
- [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.12.md)
- [Commits](https://github.com/junit-team/junit4/compare/r4.12...r4.13.2)

---
updated-dependencies:
- dependency-name: junit:junit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-28 17:51:54 +00:00
dependabot[bot]
9f657dc531
Bump net.sf.applecommander:applesingle-api from 1.2.1 to 1.2.2
Bumps [net.sf.applecommander:applesingle-api](https://github.com/AppleCommander/applesingle) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/AppleCommander/applesingle/releases)
- [Commits](https://github.com/AppleCommander/applesingle/compare/v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: net.sf.applecommander:applesingle-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-28 17:51:37 +00:00
A2 Geek
06eddc96c6
Create dependabot.yml 2023-10-28 12:51:19 -05:00
A2 Geek
814f199782
Merge pull request #22 from ryandesign/patch-1
Fix typo in README-TOKENIZER.md
2021-10-29 23:06:31 -05:00
Ryan Schmidt
9fbbcc5b81
Fix typo in README-TOKENIZER.md 2021-10-01 18:08:36 -05:00
Rob Greene
bcfa634f7b Marking most of this class as private to prevent API poisoning.
Preparation for #43.
2020-11-25 12:38:31 -06:00
Rob Greene
71f28fccc0 First round of updates. Leaving publication and the mainClassName
reference for later. #21
2020-11-24 19:36:34 -06:00
Rob Greene
983d8d06f1 Bumping version. 2019-09-02 21:12:43 -05:00
Rob Greene
1471521271 Bumping version. 2019-09-02 21:08:05 -05:00
Rob Greene
f729ef7c6d Displaying a more intelligent error message with invalid tokens in the
input stream. #20
2019-09-02 21:07:52 -05:00
A2 Geek
cc12f7ccba
Merge pull request #19 from KrisKennaway/master
Support parsing '?' tokens, which is a shorthand for 'PRINT'.
2019-09-01 14:58:42 -05:00
kris
7939985f89 Add a comment explaining what is going on 2019-07-20 15:54:34 -05:00
kris
f7b5b050dd Support parsing '?' tokens, which is a shorthand for 'PRINT'. 2019-07-20 15:52:01 -05:00
Rob Greene
801ce68525 Updating README to include current help text. 2018-11-21 15:15:23 -06:00
Rob Greene
ceecbd49fa Tweaking to handle empty lines a bit better for #18. 2018-11-21 14:34:03 -06:00
Rob Greene
c43baf9c15 Updating version referenced in README. 2018-11-21 14:28:56 -06:00
Rob Greene
6989a186e6 Testing a few other types of input. 2018-11-21 14:28:27 -06:00
Rob Greene
722fee2b6d Reordered the erase to do the sub before the depthcharge. 2018-07-16 22:21:15 -05:00
Rob Greene
4c779f45e9 Restructured source code: long variable names, multi-line, a bit of
commenting.
2018-07-16 22:17:38 -05:00
Rob Greene
9078361a18 Adding an optimization to shorten variable names to 1 or 2 characters.
This allows more useful variable names in the program.
2018-07-16 22:16:59 -05:00
Rob Greene
b84896c8cf Allowing a line continuation character of "\" with a newline immediately
after the "\" character.
2018-07-16 22:16:03 -05:00
Rob Greene
070b9371ec Allowing a ' to begin a full-line comment; cannot be used within a
continued line.
2018-07-16 22:14:27 -05:00
Rob Greene
1ae29b4488 Fixed a couple of bugs. 2018-07-15 19:46:27 -05:00
Rob Greene
e20a9c03e0 Decided that exempting "A=150" wasn't really getting anywhere. If that's
in a loop, that 150 should be pre-assigned to a "constant".
2018-07-15 18:03:13 -05:00
Rob Greene
adc60b9d41 Renamed code, finished it up with an intro screen. 2018-07-15 18:02:29 -05:00
Rob Greene
35b02d96dc Adding ability to import external shape files via shape generator. 2018-07-14 23:25:55 -05:00
Rob Greene
53b8575d3b Adding sub explosions. 2018-07-14 16:48:12 -05:00
Rob Greene
fde074ccdc Dealing with a circular pattern in the computed mark address. 2018-07-14 16:47:16 -05:00
Rob Greene
87c5930b0c Added subs and collision detection. 2018-07-13 23:16:19 -05:00
Rob Greene
a35ab37419 Only 1 depth charge redraws per loop. 2018-07-13 22:22:10 -05:00
Rob Greene
ba55fc2aaf Multiple depth charges. 2018-07-13 21:22:22 -05:00
Rob Greene
486016d7d6 Tweaking logic (again) to more correctly reassign line numbers. 2018-07-13 21:21:59 -05:00
Rob Greene
442cb1cdeb Working on a sample of 'bt' and 'st' together with 'ac'. 2018-07-13 18:59:23 -05:00
Rob Greene
f0c27bcacf Fixup for renumbering; THEN should not trigger multiple number alterations. 2018-07-13 18:58:13 -05:00
Rob Greene
9dbbfdd663 Fixing some directive parsing snafus. 2018-07-13 00:00:12 -05:00
Rob Greene
446d06af1d Updating README. 2018-07-12 23:41:35 -05:00
Rob Greene
aa29fb8399 Extensive remodeling for slightly more complex directives. Adding
$shape. #16.
2018-07-12 23:37:19 -05:00
Rob Greene
b9f2eb28d1 Updating README. #16. 2018-06-24 15:45:27 -05:00
Rob Greene
15953e9d0d Updating 'st' README. #16. 2018-06-24 15:38:21 -05:00
Rob Greene
f77a945c7e Adding ability to attach labels to a shape. #16. 2018-06-24 15:29:31 -05:00
Rob Greene
89262427bb Changing shape demo code to (hopefully) be a bit more generic. #16. 2018-06-24 15:13:27 -05:00
Rob Greene
33289a18fb Adding shape optimization flag. #16. 2018-06-24 15:13:04 -05:00
Rob Greene
c16f9c674a Making shape demo disk autostart. #16. 2018-06-24 15:12:27 -05:00
Rob Greene
580af51716 Rewrote the zero vector handling logic. #16. 2018-06-24 12:21:47 -05:00
Rob Greene
965d6be891 Adding a robot shape that got mangled to the test. #16. 2018-06-24 12:21:25 -05:00
Rob Greene
cb85e065c3 Fixing isEmpty method. #16. 2018-06-24 12:20:44 -05:00
Rob Greene
1ee7cc0d00 Handling multiple up plots more appropriately. #16. 2018-06-23 23:14:43 -05:00
Rob Greene
a4a96e04f4 Reading shape source was skipping line 1. #16. 2018-06-23 23:13:56 -05:00
Rob Greene
f025fef9bb Extract command now supports various ranges for shape numbers as well as
exporting to code. #16.
2018-06-23 23:13:17 -05:00
Rob Greene
09fd3a4bca Adding width of shape to calculations. #16. 2018-06-23 23:10:56 -05:00
Rob Greene
efbf0973bb Adding a bitmap vectorization based on distance between two points;
fixed a bug in the tracking of origin when generating a bitmap. #16.
2018-06-23 16:50:05 -05:00
Rob Greene
13a6270723 Expanded sweep optimization. #16. 2018-06-22 17:04:52 -05:00
Rob Greene
834e708356 Early copy of bitmap -> vector transformation. #16. 2018-06-21 20:55:04 -05:00
Rob Greene
208eeb8b51 Generate subcommand can now generate a demo disk for shapes. #16. 2018-06-19 21:47:29 -05:00
Rob Greene
b9ce5e434d Allow generating to AppleSingle format. #16. 2018-06-19 20:39:39 -05:00
Rob Greene
be6670d05a Updating READMEs. 2018-06-19 20:09:53 -05:00
Rob Greene
707b567b96 Adding generate subcommand. #16. 2018-06-19 20:09:39 -05:00
Rob Greene
590314877b Switching around the extract command a bit to match others. 2018-06-19 20:09:03 -05:00
Rob Greene
47ea9e41de Starting shape table generation. #16. 2018-06-19 15:27:19 -05:00
Rob Greene
ed665de0a6 Clean up... 2018-06-19 12:13:54 -05:00
Rob Greene
0e7fedde33 Updating README. 2018-06-19 11:58:05 -05:00
Rob Greene
6a26e37330 Allowing image extraction of shapes. #16. 2018-06-19 11:57:58 -05:00
Rob Greene
55434b6f53 Using spaces to show unused area in shape. 2018-06-18 20:47:51 -05:00
Rob Greene
37683e1852 Early work on shape tools and 'st' command. 2018-06-17 19:03:48 -05:00
Rob Greene
6f3c68a0ee Updating Gradle. 2018-06-12 22:31:50 -05:00
Rob Greene
e4145576cc Quick fixes for name change. 2018-06-12 19:41:40 -05:00
Rob Greene
88ce03e970 Renaming project from bastokenizer to bastools to capture the ... expansion of its capabilities! 2018-06-12 19:39:33 -05:00
Rob Greene
68709f8ca1 Adding $hex directive as a hack to allow hex numbers. 2018-06-10 22:09:00 -05:00
Rob Greene
0a31aee7e9 Fix a glitch where an empty line 0 could be created if there are no
constants.
2018-06-10 22:08:39 -05:00
Rob Greene
d49a604412 Altering errors to not print a stack trace unless --debug is specified. 2018-06-10 19:17:19 -05:00
Rob Greene
1fd8afefcd Updating optimization docs for ticket #4. 2018-06-10 10:46:26 -05:00
Rob Greene
4d6e608f59 Optimization to move constant values into variables. Closes #4. 2018-06-10 10:39:14 -05:00
Rob Greene
187c926017 Utility to generate all Applesoft variable names. 2018-06-10 10:38:08 -05:00
Rob Greene
edb91e9f96 Adding VariableCollectorVisitor. 2018-06-10 10:37:34 -05:00
Rob Greene
d17aa69d8c Ignoring PO and DO disk orders. 2018-06-10 10:36:33 -05:00
Rob Greene
54370109b2 Fixing the "IF expr THEN [GOTO|GOSUB] n" case. 2018-06-10 10:35:50 -05:00
Rob Greene
d45c17dcaf A bit of documentation and making constructors uniform. 2018-06-09 11:24:29 -05:00
Rob Greene
4fb2cfe558 Bumping AppleSingle version. 2018-06-08 22:42:34 -05:00
Rob Greene
001cb40c9f Adding version to the BT API. 2018-06-08 22:42:25 -05:00
Rob Greene
9446fe4126 Adding a version to the BT API. 2018-06-08 22:42:11 -05:00
Rob Greene
0a6c669907 Adding a bit more documentation about the $embed directive. 2018-06-08 22:41:35 -05:00
Rob Greene
9f4528bd50 Bumping version. 2018-06-08 22:41:11 -05:00
115 changed files with 5784 additions and 749 deletions

11
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gradle" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

44
.github/workflows/gradle.yml vendored Normal file
View File

@ -0,0 +1,44 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
name: Build bt and st
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
with:
arguments: build
- name: Collect artifacts
run: |
mkdir artifacts
cp $(find tools/ -name "bastools-*.jar" -not -name "bastools-*-plain.jar") artifacts/
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: build-artifacts
path: artifacts/
if-no-files-found: error

8
.gitignore vendored
View File

@ -14,8 +14,12 @@ gradle-app.setting
bin/
# Misc. Apple II remnants
*.bin
*.dsk
/*.bin
/*.dsk
/*.po
/*.do
/*.bas
/*.st
# Gradle extras
.gradle/

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"files.exclude": {
"**/.classpath": true,
"**/.project": true,
"**/.settings": true,
"**/.factorypath": true
}
}

View File

@ -631,7 +631,7 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
bastokenizer
bastools
Copyright (C) 2018 Rob Greene
This program is free software: you can redistribute it and/or modify
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
bastokenizer Copyright (C) 2018 Rob Greene
bastools Copyright (C) 2018 Rob Greene
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

View File

@ -1,10 +1,11 @@
# BASIC Tokenizer
# BASIC Tools
This project is an offshoot of AppleCommander's import of a BASIC program. The tooling will have more capabilities than what is available in AppleCommander.
# Subprojects
This project is split into two sub-projects:
This project is split into multiple sub-projects:
* [api](api) is the Java API
* [bt](tools/bt) is a command-line tool for tokenizing AppleSoft BASIC programs.
* [bt](tools/bt) is a command-line tool for tokenizing Applesoft BASIC programs, including extensions for new directives (such as embedding a shapetable into the application).
* [st](tools/st) is a command-line tool for working with Applesoft Shape Tables.

92
api/README-SHAPES.md Normal file
View File

@ -0,0 +1,92 @@
# Shape Tooling
The Shape API allows:
* Shape tables to be read in the standard binary format;
* Shape tables to be generated from "source" in three formats;
* Shape tables to be written to the standard binary format;
* Shapes and shape tables can be written to a text or image graphical representation.
## API Notes
The shape table is represented by the `ShapeTable` class which has static `read` methods.
To generate a shape table from "source" use the `ShapeGenerator` class.
The `ShapeTable` object holds a list of `Shape`s. A `Shape` can be converted to a `VectorShape`
(up, down, left, right, plot/no plot) or to a `BitmapShape` with the `Shape#toVector()` and
`Shape#toBitmap()` methods.
## Shape source
These samples define the same shape as given by Applesoft BASIC Programmer's Reference Manual - a box.
## Shape source - bitmap format
To introduce a bitmap shape, use the `.bitmap` directive.
The bitmap defines an XY grid of plot/no-plot zones. An origin may be specified and if not specified defaults to (0,0).
Notes:
* `x` = plot
* `.` = no plot; used to clarify image regions
* `+` = origin, no plot (assumed to be upper-left if unspecified)
* `*` = origin. plot
* whitespace is ignored
Sample:
```
.bitmap
.xxx.
x...x
x.+.x
x...x
.xxx.
```
## Shape source - long vector format
To introduce a long vector shape, use the `.long` directive.
Notes:
* `move`[`up`|`down`|`left`|`right`] = move vector
* `plot`[`up`|`down`|`left`|`right`] = plot vector
* whitespace is ignored
* case insensitive
* accepts a numerical argument for repetition
```
.long
movedown 2
plotleft 2
moveup
plotup 3
moveright
plotright 3
movedown
plotdown 3
moveleft
plotleft
```
## Shape source - short vector format
To introduce a short vector shape, use the `.short` directive.
Notes:
* `u`, `d`, `l`, `r` = move vector
* `U`, `D`, `L`, `R` = plot vector
* whitespace is ignored
* case sensitive
```
.short
dd
LL
uUUU
rRRR
dDDD
lL
```

169
api/README-TOKENIZER.md Normal file
View File

@ -0,0 +1,169 @@
# Tokenizer Overview
Generally, the usage pattern is:
1. Setup the `Configuration`.
2. Read the tokens.
3. Parse the tokens into a `Program`.
4. Apply transformations, if applicable.
## Code snippets
```java
Queue<Token> tokens = TokenReader.tokenize(config.sourceFile);
```
The list of tokens is a loose interpretation. It includes more of a compiler sense of tokens -- numbers, end of line markers (they're significant), AppleSoft tokens, strings, comments, identifiers, etc.
```java
Parser parser = new Parser(tokens);
Program program = parser.parse();
```
The `Program` is now the parsed version of the BASIC program. Various `Visitor`s may be used to report, gather information, or manipulate the tree in various ways.
```java
Configuration config = Configuration.builder()
.sourceFile(this.sourceFile)
.build();
```
The `Configuration` class also allows the BASIC start address to be set (defaults to `0x801`), set the maximum line length (this is in bytes, and defaults to `255`, but feel free to experiment). Some of the classes report output via the debug stream, which defaults to a simple null stream (no output) - replace with `System.out` or another `PrintStream`.
```java
ByteVisitor byteVisitor = Visitors.byteVisitor(config);
byte[] programData = byteVisitor.dump(program);
```
Finally, the ByteVisitor will transform the program into the tokenized form.
## Directives
The framework allows embedding of directives.
### `$embed`
> NOTE: It appears that DOS 3.3 _rewrites_ the resulting application and messes up the linked list of lines. ProDOS does not.
`$embed` will allow a binary to be embedded within the resulting application and can move it to a destination in memory. Please note that once the application is loaded on the Apple II, the program cannot be altered as the computer will crash.
Options:
* `file=<string>`, required. Specifies the file to load.
* `moveto=<addr>`, optional. If provided, generates code to move binary to destination. Automatically `CALL`ed.
* `var=<variable>`, optional. If provided, address is assigned to variable specified.
> Note that the current parser does not handle hex formats (_at all_). You may provide a string as well that starts with a `$` or `0x` prefix.
Usage example:
```
5 $embed file="read.time.bin", moveto="0x0260"
```
The `$embed` directive _must_ be last on the line (if there are comments, be sure to use the `REMOVE_REM_STATEMENTS` optimization.
From the `circles-timing.bas` sample, this is the beginning of the program:
```
0801:9A 09 00 00 8C 32 30 36 32 3A AB 31 00 A9 2B 85
\___/ \___/ \____________/ \___/ \_______...
Ptr, Line 0, CALL 2062, :, GOTO 1, Assembly code...
```
The move code is based on what Beagle Bros put into their [Peeks, Pokes, and Pointers](https://beagle.applearchives.com/Posters/Poster%202.pdf) poster. (See _Memory Move_ under the *Useful Calls*; the `CALL -468` entry.)
```
LDA #<embeddedStart
STA $3C
LDA #>embeddedStart
STA $3D
LDA #<embeddedEnd
STA $3E
LDA #>embeddedEnd
STA $3F
LDA #<targetAddress
STA $42
LDA #>targetAddress
STA $43
LDY #0
JMP $FE2C
```
### `$shape`
`$shape` will generate a shape table based either on the source (`src=`) or binary (`bin=`) shape table provided. Source shape table generation is based on the shape table `st` tool support and is described [here in more detail](README-SHAPES.md).
Overall format is as follows:
```
$shape ( src="path" [ ,label=variable | ,assign=(varname1="label1" [,varname2="label2"]* ] )
| bin="path" )
[,poke=yes(default)|no]
[,address=<variable>]
[,init=yes|no ]
```
#### Shape from source
By using the `src=` option, the source code will be generated on the fly. For example the following shape source will insert a shape named "mouse" into the BASIC program:
```
; extracted from NEW MOUSE
.bitmap mouse
..........*X..
....XXXX.XX...
...XXXXXXXX...
.XXXXXXXXXXX..
XX.XXXXXXX.XX.
X...XXXXXXXXXX
XX............
.XXX.XX.......
...XXX........
```
Options on the source include:
* `label=variable` which indicates a label is really a variable name; in the example, the variable name would be "MOUSE".
* `assign=(...)` will define a mapping from the label in the source to the BASIC variable name. A `assign(m=mouse)` will define the variable `M` to be the shape number for the mouse.
#### Shape from binary
By using the `bin=` option, an already existing binary shape table can be inserted into the code. There are no additional options available in this case.
#### General options
* `poke=yes|no` (default=`yes`) will embed a `POKE 232,<lowAddr>:POKE 233,<highAddr>` into the line of code.
* `address=<variable>`, if supplied, will assign the address to a variable; therefore a `address=AD` will embed the variable `AD` into the line of code.
* `init=yes|no` (default=`yes`) will embed a simple `ROT=0:SCALE=1` into the line of code for simple shape initialization.
### `$hex`
If embedding hexadecimal addresses into an application makes sense, the `$hex` directive allows that to be done in a rudimentary manner.
Sample:
```
10 call $hex value="fc58"
```
Yields:
```
10 call -936
```
## Optimizations
Optimizations are mechanisms to rewrite the `Program`, typically making the program smaller. `Optimization` itself is an enum which has a `create` method to setup the `Visitor`.
Current optimizations are:
* _Remove empty statements_ will remove all extra colons. For example, if the application in question used `:` to indicate nesting. Or just accidents!
* _Remove REM statements_ will remove all comments.
* _Extract constant values_ will find all constant numerical references, insert a line `0` with assignments, and finally replace all the numbers with the approrpiate variable name. Hypothesis is that the BASIC interpreter only parses the number once.
* _Merge lines_ will identify all lines that are not a target of `GOTO`/`GOSUB`-type action and rewrite the line by merging it with others. The concept involved is that the BASIC program is just a linked list and shortening the list will shorten the search path. The default *max length* in bytes is set to `255`.
* _Renumber_ will renumber the application, beginning with line `0`. This makes the decoding a tiny bit more efficient in that the number to decode will be smaller in the token stream.
Sample use:
```java
program = program.accept(Optimization.REMOVE_REM_STATEMENTS.create(config));
```

View File

@ -1,6 +1,6 @@
# BT API
# BASIC Tools API
The BASIC Tokenizer API is a set of reusable code that can be used to parse a text-based AppleSoft BASIC program an generate the appropriate tokens. It also has multiple types of visitors that can re-write that parse tree to rearrange the code (calling them optimizations is a bit over-the-top).
The BASIC Tools API is a set of reusable code that can be used to parse a text-based Applesoft BASIC program an generate the appropriate tokens. It also has multiple types of visitors that can re-write that parse tree to rearrange the code (calling them optimizations is a bit over-the-top).
## Maven / Gradle
@ -9,8 +9,8 @@ To include in a Maven project:
```xml
<dependency>
<groupId>net.sf.applecommander</groupId>
<artifactId>bastokenizer-api</artifactId>
<version>0.2.0</version>
<artifactId>bastools-api</artifactId>
<version>0.3.1</version>
</dependency>
```
@ -19,76 +19,14 @@ To include in a Gradle project:
```
dependencies {
// ...
compile "net.sf.applecommander:bastokenizer-api:0.2.0"
compile "net.sf.applecommander:bastools-api:0.3.1"
// ...
}
```
## Overview
## API descriptions
Generally, the usage pattern is:
1. Setup the `Configuration`.
2. Read the tokens.
3. Parse the tokens into a `Program`.
4. Apply transformations, if applicable.
Currently the API is broken into the following sections:
## Code snippets
```java
Configuration config = Configuration.builder()
.sourceFile(this.sourceFile)
.build();
```
The `Configuration` class also allows the BASIC start address to be set (defaults to `0x801`), set the maximum line length (this is in bytes, and defaults to `255`, but feel free to experiment). Some of the classes report output via the debug stream, which defaults to a simple null stream (no output) - replace with `System.out` or another `PrintStream`.
```java
Queue<Token> tokens = TokenReader.tokenize(config.sourceFile);
```
The list of tokens is a loose interpretation. It includes more of a compiler sense of tokens -- numbers, end of line markers (they're significant), AppleSoft tokens, strings, comments, identifiers, etc.
```java
Parser parser = new Parser(tokens);
Program program = parser.parse();
```
The `Program` is now the parsed version of the BASIC program. Various `Visitor`s may be used to report, gather information, or manipulate the tree in various ways.
## Directives
The framework allows embedding of directives.
### `$embed`
`$embed` will allow a binary to be embedded within the resulting application. Please note that once the application is loaeded on the Apple II, the program cannot be altered as the computer will crash. Usage example:
```
5 $embed "read.time.bin", "0x0260"
```
The `$embed` directive _must_ be last on the line (if there are comments, be sure to use the `REMOVE_REM_STATEMENTS` optimization. It takes two parameters: file name and target address, both are strings.
From the `circles-timing.bas` sample, this is the beginning of the program:
```
0801:9A 09 00 00 8C 32 30 36 32 3A AB 31 00 A9 2B 85
\___/ \___/ \____________/ \___/ \_______...
Ptr, Line 0, CALL 3062, :, GOTO 1, Assembly code...
```
## Optimizations
Optimizations are mechanisms to rewrite the `Program`, typically making the program smaller. `Optimization` itself is an enum which has a `create` method to setup the `Visitor`.
Current optimizations are:
* _Remove empty statements_ will remove all extra colons. For example, if the application in question used `:` to indicate nesting. Or just accidents!
* _Remove REM statements_ will remove all comments.
* _Merge lines_ will identify all lines that are not a target of `GOTO`/`GOSUB`-type action and rewrite the line by merging it with others. The concept involved is that the BASIC program is just a linked list and shortening the list will shorten the search path. The default *max length* in bytes is set to `255`.
* _Renumber_ will renumber the application, beginning with line `0`. This makes the decoding a tiny bit more efficient in that the number to decode will be smaller in the token stream.
Sample use:
```java
program = program.accept(Optimization.REMOVE_REM_STATEMENTS.create(config));
```
* [BASIC Tokenizer](README-TOKENIZER.md)
* [Shape Tooling](README-SHAPES.md)

View File

@ -1,72 +1,95 @@
repositories {
jcenter()
plugins {
id 'java-library'
id 'maven-publish'
id 'signing'
}
apply plugin: 'java-library'
apply plugin: 'maven'
apply plugin: 'signing'
ext.isSnapshotVersion = version.endsWith("SNAPSHOT")
ext.isReleaseVersion = !ext.isSnapshotVersion
sourceCompatibility = 11
targetCompatibility = 11
repositories {
mavenCentral()
}
dependencies {
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13.2'
}
jar {
manifest {
attributes(
'Implementation-Title': 'B/BAS Tools API',
'Implementation-Version': "${project.version} (${new Date().format('yyyy-MM-dd HH:mm')})"
)
}
}
javadoc {
title = "bastools ${version}"
source = sourceSets.main.allJava
options.addStringOption('Xdoclint:none', '-quiet')
}
task javadocJar(type: Jar) {
classifier = 'javadoc'
archiveClassifier = 'javadoc'
from javadoc
}
task sourcesJar(type: Jar) {
classifier = 'sources'
archiveClassifier = 'sources'
from sourceSets.main.allSource
}
artifacts {
archives javadocJar, sourcesJar
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
artifact javadocJar
pom {
groupId = "net.sf.applecommander"
artifactId = "bastools-api"
name = "B/BAS Tools (bastools)"
description = 'An Applesoft BASIC tools library.'
url = 'https://applecommander.github.io/'
licenses {
license {
name = 'The GNU General Public License (GPL) Version 3, 29 June 2007'
url = 'https://www.gnu.org/licenses/gpl-3.0.html'
}
}
developers {
developer {
id = 'robgreene'
name = 'Rob Greene'
email = 'robgreene@gmail.com'
}
}
scm {
connection = 'scm:git:https://github.com/AppleCommander/bastools.git'
developerConnection = 'scm:git:git@github.com:AppleCommander/bastools.git'
url = 'https://github.com/AppleCommander/bastools'
}
}
repositories {
maven {
def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2"
def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/"
url = isSnapshotVersion ? snapshotsRepoUrl : releasesRepoUrl
credentials {
username = findProperty('ossrhUsername')
password = findProperty('ossrhPassword')
}
}
}
}
}
}
signing {
// Only sign if we're uploading...
required { gradle.taskGraph.hasTask("uploadArchives") }
sign configurations.archives
sign publishing.publications.mavenJava
}
uploadArchives {
repositories {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
authentication(userName: findProperty('ossrhUsername'), password: findProperty('ossrhPassword'))
}
snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") {
authentication(userName: findProperty('ossrhUsername'), password: findProperty('ossrhPassword'))
}
pom.project {
name archivesBaseName
packaging 'jar'
description 'Experiments with generating an AppleSoft B/BAS tokenized "binary".'
url 'https://applecommander.github.io/'
scm {
url 'https://github.com/AppleCommander/bastokenizer'
}
licenses {
license {
name 'The GNU General Public License (GPL) Version 3, 29 June 2007'
url 'https://www.gnu.org/licenses/gpl-3.0.html'
}
}
developers {
developer {
id 'robgreene'
email 'robgreene@gmail.com'
}
}
}
}
}
}

View File

@ -1,84 +0,0 @@
package io.github.applecommander.bastokenizer.api;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import io.github.applecommander.bastokenizer.api.model.Line;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastokenizer.api.model.Token.Type;
import io.github.applecommander.bastokenizer.api.utils.Converters;
public abstract class Directive {
protected Configuration config;
protected OutputStream outputStream;
protected List<Token> parameters = new ArrayList<>();
protected Directive(Configuration config, OutputStream outputStream) {
Objects.requireNonNull(config);
Objects.requireNonNull(outputStream);
this.config = config;
this.outputStream = outputStream;
}
public void append(Token token) {
// Skip the commas...
if (token.type == Type.SYNTAX && ",".equals(token.text)) return;
parameters.add(token);
}
protected Token require(Type... types) {
Token t = parameters.remove(0);
boolean matches = false;
for (Type type : types) {
matches |= type == t.type;
}
if (!matches) {
throw new IllegalArgumentException("Expecting a type of " + types);
}
return t;
}
protected String requiresString() {
Token t = require(Type.STRING);
return t.text;
}
protected int requiresInteger() {
Token t = require(Type.NUMBER, Type.STRING);
if (t.type == Type.NUMBER) {
return t.number.intValue();
}
return Converters.toInteger(t.text);
}
protected void ldy(int value) throws IOException {
outputStream.write(0xa0);
outputStream.write(value);
}
protected void jmp(int address) throws IOException {
outputStream.write(0x4c);
outputStream.write(address & 0xff);
outputStream.write(address >> 8);
}
protected void lda(int value) throws IOException {
outputStream.write(0xa9);
outputStream.write(value);
}
protected void sta(int address) throws IOException {
if ((address & 0xff00) == 0) {
outputStream.write(0x85);
outputStream.write(address);
} else {
throw new RuntimeException("sta does not handle 16 bit addresses yet!");
}
}
protected void setAddress(int value, int address) throws IOException {
lda(value & 0xff);
sta(address);
lda(value >> 8);
sta(address+1);
}
/** Write directive contents to output file. Note that address is adjusted for the line header already. */
public abstract void writeBytes(int startAddress, Line line) throws IOException;
}

View File

@ -1,26 +0,0 @@
package io.github.applecommander.bastokenizer.api;
import java.util.function.Function;
import io.github.applecommander.bastokenizer.api.optimizations.MergeLines;
import io.github.applecommander.bastokenizer.api.optimizations.RemoveEmptyStatements;
import io.github.applecommander.bastokenizer.api.optimizations.RemoveRemStatements;
import io.github.applecommander.bastokenizer.api.optimizations.Renumber;
public enum Optimization {
REMOVE_EMPTY_STATEMENTS(config -> new RemoveEmptyStatements()),
REMOVE_REM_STATEMENTS(config -> new RemoveRemStatements()),
MERGE_LINES(config -> new MergeLines(config)),
RENUMBER(config -> new Renumber());
private Function<Configuration,Visitor> factory;
private Optimization(Function<Configuration,Visitor> factory) {
this.factory = factory;
}
public Visitor create(Configuration config) {
return factory.apply(config);
}
}

View File

@ -1,71 +0,0 @@
package io.github.applecommander.bastokenizer.api;
import java.util.Objects;
import java.util.Queue;
import io.github.applecommander.bastokenizer.api.model.Line;
import io.github.applecommander.bastokenizer.api.model.Program;
import io.github.applecommander.bastokenizer.api.model.Statement;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastokenizer.api.model.Token.Type;
/**
* The Parser will read a series of Tokens and build a Program.
* Note that this is not a compiler and does not "understand" the program.
*/
public class Parser {
private final Queue<Token> tokens;
public Parser(Queue<Token> tokens) {
Objects.requireNonNull(tokens);
this.tokens = tokens;
}
public Program parse() {
Program program = new Program();
while (!tokens.isEmpty()) {
Line line = readLine(program);
program.lines.add(line);
}
return program;
}
public Line readLine(Program program) {
Line line = new Line(expectNumber(), program);
while (!tokens.isEmpty() && tokens.peek().type != Type.EOL) {
Statement statement = readStatement();
if (statement != null) {
line.statements.add(statement);
} else {
break;
}
}
if (!tokens.isEmpty() && tokens.peek().type == Type.EOL) {
tokens.remove(); // Skip that EOL
}
return line;
}
public Statement readStatement() {
Statement statement = new Statement();
while (!tokens.isEmpty()) {
if (tokens.peek().type == Type.EOL) break;
Token t = tokens.remove();
if (t.type == Type.SYNTAX && ":".equals(t.text)) break;
statement.tokens.add(t);
}
return statement;
}
public int expectNumber() {
Token c = tokens.remove();
while (c.type == Type.EOL) {
// Allow blank lines...
c = tokens.remove();
}
if (c.type != Type.NUMBER) {
throw new RuntimeException("Expected a number in line #" + c.line);
}
return c.number.intValue();
}
}

View File

@ -1,81 +0,0 @@
package io.github.applecommander.bastokenizer.api.directives;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Optional;
import io.github.applecommander.bastokenizer.api.Configuration;
import io.github.applecommander.bastokenizer.api.Directive;
import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword;
import io.github.applecommander.bastokenizer.api.model.Line;
public class EmbeddedBinaryDirective extends Directive {
public EmbeddedBinaryDirective(Configuration config, OutputStream outputStream) {
super(config, outputStream);
}
@Override
public void writeBytes(int startAddress, Line line) throws IOException {
if (parameters.size() != 2) {
throw new IllegalArgumentException("$embed requires a name and address parameter");
}
String filename = requiresString();
int targetAddress = requiresInteger();
File file = new File(config.sourceFile.getParentFile(), filename);
byte[] bin = Files.readAllBytes(file.toPath());
Optional<Line> nextLine = line.nextLine();
byte[] basicCode = nextLine.isPresent()
? callAndGoto(startAddress,nextLine.get())
: callAndReturn(startAddress);
final int moveLength = 8*3 + 2 + 3; // LDA/STA, LDY, JMP.
int embeddedStart = startAddress + basicCode.length + moveLength;
int embeddedEnd = embeddedStart + bin.length;
outputStream.write(basicCode);
setAddress(embeddedStart, 0x3c);
setAddress(embeddedEnd, 0x3e);
setAddress(targetAddress, 0x42);
ldy(0x00);
jmp(0xfe2c);
outputStream.write(bin);
}
// In program, "CALL <address>:GOTO line"
private byte[] callAndGoto(int startAddress, Line line) throws IOException {
// 3 for the tokens "CALL", ":", "GOTO", end of line (0x00)
final int tokenCount = 3 + 1;
int offset = Integer.toString(line.lineNumber).length() + tokenCount;
offset += Integer.toString(startAddress).length();
// Attempting to adjust if we bump from 4 digit address to a 5 digit address
if (startAddress < 10000 && startAddress + offset >= 10000) offset += 1;
ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write(ApplesoftKeyword.CALL.code);
os.write(Integer.toString(startAddress+offset).getBytes());
os.write(':');
os.write(ApplesoftKeyword.GOTO.code);
os.write(Integer.toString(line.lineNumber).getBytes());
os.write(0x00);
return os.toByteArray();
}
// At end of program, just "CALL <address>:RETURN"
private byte[] callAndReturn(int startAddress) throws IOException {
// 3 for the tokens "CALL", ":", "RETURN", end of line (0x00)
final int tokenCount = 3 + 1;
int offset = tokenCount;
offset += Integer.toString(startAddress).length();
// Attempting to adjust if we bump from 4 digit address to a 5 digit address
if (startAddress < 10000 && startAddress + offset >= 10000) offset += 1;
ByteArrayOutputStream os = new ByteArrayOutputStream();
os.write(ApplesoftKeyword.CALL.code);
os.write(Integer.toString(startAddress+offset).getBytes());
os.write(':');
os.write(ApplesoftKeyword.RETURN.code);
os.write(0x00);
return os.toByteArray();
}
}

View File

@ -1,10 +0,0 @@
package io.github.applecommander.bastokenizer.api.optimizations;
import io.github.applecommander.bastokenizer.api.model.Statement;
public class RemoveEmptyStatements extends BaseVisitor {
@Override
public Statement visit(Statement statement) {
return statement.tokens.isEmpty() ? null : statement;
}
}

View File

@ -1,11 +0,0 @@
package io.github.applecommander.bastokenizer.api.optimizations;
import io.github.applecommander.bastokenizer.api.model.Statement;
import io.github.applecommander.bastokenizer.api.model.Token.Type;
public class RemoveRemStatements extends BaseVisitor {
@Override
public Statement visit(Statement statement) {
return statement.tokens.get(0).type == Type.COMMENT ? null : statement;
}
}

View File

@ -1,22 +0,0 @@
package io.github.applecommander.bastokenizer.api.utils;
public class Converters {
private Converters() { /* Prevent construction */ }
/**
* Convert a string to an integer allowing multiple formats.
* Normal decimal, or hexadecimal with a <code>$</code> or <code>0x</code> prefix.
*/
public static Integer toInteger(String value) {
if (value == null) {
return null;
} else if (value.startsWith("$")) {
return Integer.valueOf(value.substring(1), 16);
} else if (value.startsWith("0x") || value.startsWith("0X")) {
return Integer.valueOf(value.substring(2), 16);
} else {
return Integer.valueOf(value);
}
}
}

View File

@ -0,0 +1,16 @@
package io.github.applecommander.bastools.api;
/**
* Since there are many pieces to bastools, the version information is just a small,
* dedicated class.
*/
public class BasTools {
public static final String VERSION;
public static final String TITLE;
static {
TITLE = BasTools.class.getPackage().getImplementationTitle();
VERSION = BasTools.class.getPackage().getImplementationVersion();
}
private BasTools() {}
}

View File

@ -1,9 +1,11 @@
package io.github.applecommander.bastokenizer.api;
package io.github.applecommander.bastools.api;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Configuration {
@ -11,6 +13,7 @@ public class Configuration {
public final int startAddress;
public final int maxLineLength;
public final PrintStream debugStream;
public final Map<String,String >variableReplacements = new HashMap<>();
private Configuration(Builder b) {
this.sourceFile = b.sourceFile;

View File

@ -0,0 +1,242 @@
package io.github.applecommander.bastools.api;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Predicate;
import io.github.applecommander.bastools.api.model.ApplesoftKeyword;
import io.github.applecommander.bastools.api.model.Line;
import io.github.applecommander.bastools.api.model.Token;
import io.github.applecommander.bastools.api.model.Token.Type;
import io.github.applecommander.bastools.api.utils.Converters;
public abstract class Directive {
private String directiveName;
protected Configuration config;
protected OutputStream outputStream;
private List<Token> paramTokens = new ArrayList<>();
private Map<String,Expression> parameters = new TreeMap<>(String::compareToIgnoreCase);
private Set<String> parameterNames;
protected Directive(String directiveName, Configuration config, OutputStream outputStream, String... parameterNames) {
Objects.requireNonNull(directiveName);
Objects.requireNonNull(config);
Objects.requireNonNull(outputStream);
this.directiveName = directiveName;
this.config = config;
this.outputStream = outputStream;
this.parameterNames = new TreeSet<>(String::compareToIgnoreCase);
this.parameterNames.addAll(Arrays.asList(parameterNames));
}
/** Resolve the given variable name with any variable replacements that should occur. */
public String resolve(String originalVariableName) {
if (config.variableReplacements.containsKey(originalVariableName)) {
String replacementVariableName = config.variableReplacements.get(originalVariableName);
config.debugStream.printf("Replacing '%s' with '%s'\n", originalVariableName, replacementVariableName);
return replacementVariableName;
}
return originalVariableName;
}
public Optional<Expression> optionalExpression(String paramName) {
return Optional.ofNullable(parameters.get(paramName));
}
// public Expression requiredExpression(String paramName, String errorMessage) {
// return optionalExpression(paramName).orElseThrow(() -> new RuntimeException(errorMessage));
// }
public Optional<Integer> optionalIntegerExpression(String paramName) {
return optionalExpression(paramName)
.flatMap(Expression::toSimpleExpression)
.map(SimpleExpression::asInteger);
}
public Integer requiredIntegerExpression(String paramName, String errorMessage) {
return optionalIntegerExpression(paramName).orElseThrow(() -> new RuntimeException(errorMessage));
}
public Optional<String> optionalStringExpression(String paramName) {
return optionalExpression(paramName)
.flatMap(Expression::toSimpleExpression)
.map(SimpleExpression::asString);
}
public boolean defaultBooleanExpression(String paramName, boolean defaultValue) {
return optionalExpression(paramName)
.flatMap(Expression::toSimpleExpression)
.map(SimpleExpression::asBoolean)
.orElse(defaultValue);
}
public String requiredStringExpression(String paramName, String errorMessage) {
return optionalStringExpression(paramName).orElseThrow(() -> new RuntimeException(errorMessage));
}
public Optional<MapExpression> optionalMapExpression(String paramName) {
return optionalExpression(paramName)
.flatMap(Expression::toMapExpression);
}
public static final Predicate<Integer> ONLY_ONE = (n) -> n == 1;
public static final Predicate<Integer> ZERO_OR_ONE = (n) -> n <= 1;
public static final Predicate<Integer> ZERO = (n) -> n == 0;
/** Validate a set of optionals with the given validator. If it fails, throw an exception with the message. */
public void validateSet(Predicate<Integer> validator, String message, Optional<?>... opts) {
int count = 0;
for (Optional<?> opt : opts) {
if (opt.isPresent()) count += 1;
}
if (!validator.test(count)) {
throw new RuntimeException(message);
}
}
/**
* Append directive tokens. Note that this MUST be terminated by a termination token
* (probably EOL) to prevent loss of information.
*/
public void append(Token token) {
if (token.type == Type.EOL) {
while (!paramTokens.isEmpty()) {
String name = requireIdentToken();
if (!parameterNames.contains(name)) {
String message = String.format("Parameter '%s' is invalid for %s directive", name, directiveName);
throw new RuntimeException(message);
}
requireSyntaxToken("=");
Expression expr = buildExpression();
parameters.put(name, expr);
if (!paramTokens.isEmpty()) {
requireSyntaxToken(",");
}
}
} else {
paramTokens.add(token);
}
}
private Expression buildExpression() {
Token t = paramTokens.get(0);
if ("(".equals(t.text)) {
requireSyntaxToken("(");
Expression expr = buildMapExpression();
requireSyntaxToken(")");
return expr;
} else {
return buildSimpleExpression();
}
}
private Expression buildSimpleExpression() {
Token t = paramTokens.remove(0);
return new SimpleExpression(t.asString());
}
private Expression buildMapExpression() {
MapExpression mapex = new MapExpression();
boolean more = true;
while (more) {
String key = requireIdentToken();
requireSyntaxToken("=");
Expression expr = buildExpression();
mapex.expressions.put(key, expr);
more = checkSyntaxToken(",");
if (more) {
// Still need to consume it
requireSyntaxToken(",");
}
}
return mapex;
}
private Token requireToken(Type... types) {
Token t = paramTokens.remove(0);
boolean matches = false;
for (Type type : types) {
matches |= type == t.type;
}
if (!matches) {
String message = String.format("Expecting a token type of %s but found %s instead",
Arrays.asList(types), t.type);
throw new IllegalArgumentException(message);
}
return t;
}
private String requireIdentToken() {
Token t = requireToken(Type.IDENT, Type.KEYWORD);
return t.text;
}
private void requireSyntaxToken(String syntax) {
try {
Type tokenType = ApplesoftKeyword.find(syntax).map(t -> Type.KEYWORD).orElse(Type.SYNTAX);
Token token = requireToken(tokenType);
if (!syntax.equals(token.text)) {
String message = String.format("Expecting '%s' but found '%s' instead", syntax, token.text);
throw new RuntimeException(message);
}
} catch (IllegalArgumentException ex) {
throw new RuntimeException(String.format("Failed when token of '%s' was required", syntax));
}
}
private boolean checkSyntaxToken(String syntax) {
if (paramTokens.isEmpty()) return false;
Type tokenType = ApplesoftKeyword.find(syntax).map(t -> Type.KEYWORD).orElse(Type.SYNTAX);
Token token = paramTokens.get(0);
return tokenType == token.type && syntax.equals(token.text);
}
/** Write directive contents to output file. Note that address is adjusted for the line header already. */
public abstract void writeBytes(int startAddress, Line line) throws IOException;
public static class Variable {
public final String name;
public final Expression expr;
private Variable(String name, Expression expr) {
this.name = name;
this.expr = expr;
}
}
public interface Expression {
public Optional<SimpleExpression> toSimpleExpression();
public Optional<MapExpression> toMapExpression();
}
public static class SimpleExpression implements Expression {
private final String value;
public SimpleExpression(String value) {
this.value = value;
}
public String asString() {
return value;
}
public Boolean asBoolean() {
return Converters.toBoolean(value);
}
public Integer asInteger() {
return Converters.toInteger(value);
}
public Optional<SimpleExpression> toSimpleExpression() {
return Optional.of(this);
}
public Optional<MapExpression> toMapExpression() {
return Optional.empty();
}
}
public static class MapExpression implements Expression {
private final Map<String,Expression> expressions = new HashMap<>();
public Optional<Expression> get(String key) {
return Optional.ofNullable(expressions.get(key));
}
public Set<Map.Entry<String,Expression>> entrySet() {
return expressions.entrySet();
}
public Optional<SimpleExpression> toSimpleExpression() {
return Optional.empty();
}
public Optional<MapExpression> toMapExpression() {
return Optional.of(this);
}
}
}

View File

@ -1,11 +1,13 @@
package io.github.applecommander.bastokenizer.api;
package io.github.applecommander.bastools.api;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.TreeMap;
import io.github.applecommander.bastokenizer.api.directives.EmbeddedBinaryDirective;
import io.github.applecommander.bastools.api.directives.EmbeddedBinaryDirective;
import io.github.applecommander.bastools.api.directives.EmbeddedShapeTable;
import io.github.applecommander.bastools.api.directives.HexDirective;
public class Directives {
private Directives() { /* Prevent construction. */ }
@ -15,7 +17,9 @@ public class Directives {
private static final long serialVersionUID = -8111460701487331592L;
{
put("$embed", EmbeddedBinaryDirective.class);
put(EmbeddedBinaryDirective.NAME, EmbeddedBinaryDirective.class);
put(HexDirective.NAME, HexDirective.class);
put(EmbeddedShapeTable.NAME, EmbeddedShapeTable.class);
}
};

View File

@ -0,0 +1,34 @@
package io.github.applecommander.bastools.api;
import java.util.function.Function;
import io.github.applecommander.bastools.api.optimizations.ExtractConstantValues;
import io.github.applecommander.bastools.api.optimizations.ShortenVariableNames;
import io.github.applecommander.bastools.api.optimizations.MergeLines;
import io.github.applecommander.bastools.api.optimizations.RemoveEmptyStatements;
import io.github.applecommander.bastools.api.optimizations.RemoveRemStatements;
import io.github.applecommander.bastools.api.optimizations.Renumber;
/**
* All optimization capabilities are definined here in the "best" manner of execution.
* Essentially, the goal is to prioritize the optimizations to manage dependencies.
*/
public enum Optimization {
REMOVE_EMPTY_STATEMENTS(RemoveEmptyStatements::new),
REMOVE_REM_STATEMENTS(RemoveRemStatements::new),
SHORTEN_VARIABLE_NAMES(ShortenVariableNames::new),
EXTRACT_CONSTANT_VALUES(ExtractConstantValues::new),
MERGE_LINES(MergeLines::new),
RENUMBER(Renumber::new);
private Function<Configuration,Visitor> factory;
private Optimization(Function<Configuration,Visitor> factory) {
this.factory = factory;
}
public Visitor create(Configuration config) {
return factory.apply(config);
}
}

View File

@ -0,0 +1,72 @@
package io.github.applecommander.bastools.api;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import io.github.applecommander.bastools.api.model.Line;
import io.github.applecommander.bastools.api.model.Program;
import io.github.applecommander.bastools.api.model.Statement;
import io.github.applecommander.bastools.api.model.Token;
import io.github.applecommander.bastools.api.model.Token.Type;
/**
* The Parser will read a series of Tokens and build a Program.
* Note that this is not a compiler and does not "understand" the program.
*/
public class Parser {
private final Queue<Token> tokens;
public Parser(Queue<Token> tokens) {
Objects.requireNonNull(tokens);
this.tokens = tokens;
}
public Program parse() {
Program program = new Program();
while (!tokens.isEmpty()) {
readLine(program).ifPresent(program.lines::add);
}
return program;
}
public Optional<Line> readLine(Program program) {
return expectNumber().map(lineNumber -> {
Line line = new Line(lineNumber, program);
while (!tokens.isEmpty() && tokens.peek().type != Type.EOL) {
Statement statement = readStatement();
if (statement != null) {
line.statements.add(statement);
} else {
break;
}
}
if (!tokens.isEmpty() && tokens.peek().type == Type.EOL) {
tokens.remove(); // Skip that EOL
}
return line;
});
}
public Statement readStatement() {
Statement statement = new Statement();
while (!tokens.isEmpty()) {
if (tokens.peek().type == Type.EOL) break;
Token t = tokens.remove();
if (t.type == Type.SYNTAX && ":".equals(t.text)) break;
statement.tokens.add(t);
}
return statement;
}
public Optional<Integer> expectNumber() {
Token c = tokens.remove();
while (c.type == Type.EOL) {
return Optional.empty();
}
if (c.type != Type.NUMBER) {
throw new RuntimeException("Expected a number in line #" + c.line);
}
return Optional.of(c.number.intValue());
}
}

View File

@ -1,4 +1,4 @@
package io.github.applecommander.bastokenizer.api;
package io.github.applecommander.bastools.api;
import java.io.File;
import java.io.FileNotFoundException;
@ -12,13 +12,18 @@ import java.util.LinkedList;
import java.util.Optional;
import java.util.Queue;
import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastools.api.model.ApplesoftKeyword;
import io.github.applecommander.bastools.api.model.Token;
/**
* The TokenReader, given a text file, generates a series of Tokens (in the compiler sense,
* not AppleSoft) for the AppleSoft program.
*
* <p/>
* Note that this relies on the Java StreamTokenizer. The goal is to provide a more modern
* parsing of tokens (in which it succeeds), however, AppleSoft itself is dated and mixes with
* what a modern syntax would consist of. Specifically, the '#' is out of place as part of a
* token and not a syntax element. Hence there is some funny business embedded in the code.
*
* @author rob
*/
public class TokenReader {
@ -57,16 +62,16 @@ public class TokenReader {
return tokens;
}
public TokenReader(Reader reader) {
private TokenReader(Reader reader) {
this.reader = reader;
this.tokenizer = ApplesoftKeyword.tokenizer(reader);
}
public boolean hasMore() {
private boolean hasMore() {
return hasMore;
}
public Optional<Token> next(int depth) throws IOException {
private Optional<Token> next(int depth) throws IOException {
// A cheesy attempt to prevent too much looping...
if (depth > 0) {
if (this.needSyntheticEol) {
@ -108,7 +113,12 @@ public class TokenReader {
.filter(t -> opt.get().parts.get(1).equals(t.text))
.orElseThrow(() -> new IOException("Expecting: " + opt.get().parts));
}
return Optional.of(Token.keyword(line, opt.get()));
ApplesoftKeyword outKeyword = opt.get();
// Special case - canonicalize '?' alternate form of 'PRINT'
if (opt.filter(kw -> kw == ApplesoftKeyword.questionmark).isPresent()) {
outKeyword = ApplesoftKeyword.PRINT;
}
return Optional.of(Token.keyword(line, outKeyword));
}
// Check if we found a directive
if (tokenizer.sval.startsWith("$")) {
@ -145,9 +155,26 @@ public class TokenReader {
ApplesoftKeyword.find(String.format("%c", tokenizer.ttype))
.map(kw -> Token.keyword(line, kw))
.orElse(Token.syntax(line, tokenizer.ttype)));
case '\\':
// Special case: introducing a backslash to ignore the IMMEDIATELY following EOL
// If this does not occur, we simply fall through and fail. That is intentional!
if (tokenizer.nextToken() == StreamTokenizer.TT_EOL) {
// Consume the EOL and continue on our merry way
break;
}
default:
throw new IOException(String.format(
"Unknown! ttype=%d, nval=%f, sval=%s\n", tokenizer.ttype, tokenizer.nval, tokenizer.sval));
String message = String.format("Unknown or unexpected character '%c'", tokenizer.ttype);
if (tokenizer.ttype == StreamTokenizer.TT_WORD) {
message = String.format("Unknown or unexpected string '%s'", tokenizer.sval);
} else if (tokenizer.ttype == StreamTokenizer.TT_NUMBER) {
message = String.format("Unknown or unexpected number %f", tokenizer.nval);
} else if (tokenizer.ttype == StreamTokenizer.TT_EOF) {
message = "Unexpected EOF";
} else if (tokenizer.ttype == StreamTokenizer.TT_EOL) {
message = "Unexpected EOL";
}
message += String.format(" found on line #%d", tokenizer.lineno());
throw new IOException(message);
}
}
}

View File

@ -1,9 +1,9 @@
package io.github.applecommander.bastokenizer.api;
package io.github.applecommander.bastools.api;
import io.github.applecommander.bastokenizer.api.model.Line;
import io.github.applecommander.bastokenizer.api.model.Program;
import io.github.applecommander.bastokenizer.api.model.Statement;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastools.api.model.Line;
import io.github.applecommander.bastools.api.model.Program;
import io.github.applecommander.bastools.api.model.Statement;
import io.github.applecommander.bastools.api.model.Token;
/**
* The Visitor interface allows some flexibility in what can be done with the

View File

@ -1,16 +1,17 @@
package io.github.applecommander.bastokenizer.api;
package io.github.applecommander.bastools.api;
import java.io.PrintStream;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import io.github.applecommander.bastokenizer.api.visitors.ByteVisitor;
import io.github.applecommander.bastokenizer.api.visitors.LineNumberTargetCollector;
import io.github.applecommander.bastokenizer.api.visitors.PrettyPrintVisitor;
import io.github.applecommander.bastokenizer.api.visitors.PrintVisitor;
import io.github.applecommander.bastokenizer.api.visitors.ReassignmentVisitor;
import io.github.applecommander.bastokenizer.api.visitors.VariableReportVisitor;
import io.github.applecommander.bastools.api.visitors.ByteVisitor;
import io.github.applecommander.bastools.api.visitors.LineNumberTargetCollector;
import io.github.applecommander.bastools.api.visitors.PrettyPrintVisitor;
import io.github.applecommander.bastools.api.visitors.PrintVisitor;
import io.github.applecommander.bastools.api.visitors.ReassignmentVisitor;
import io.github.applecommander.bastools.api.visitors.VariableCollectorVisitor;
import io.github.applecommander.bastools.api.visitors.VariableReportVisitor;
/**
* This class presents all of the common Visitor implementations via builder patterns.
@ -67,6 +68,10 @@ public class Visitors {
return new LineNumberTargetCollector();
}
public static VariableCollectorVisitor variableCollectorVisitor() {
return new VariableCollectorVisitor();
}
public static Visitor variableReportVisitor() {
return new VariableReportVisitor();
}

View File

@ -0,0 +1,101 @@
package io.github.applecommander.bastools.api.code;
import java.io.IOException;
import java.util.Objects;
/**
* {@code AsmBuilder} allows generation of assembly code to embed into the output stream.
* <p>
* By no means is this complete, but is being built out as the need arises.
*/
public class AsmBuilder {
private CodeBuilder builder;
public AsmBuilder(CodeBuilder builder) {
Objects.requireNonNull(builder);
this.builder = builder;
}
public CodeBuilder end() {
return this.builder;
}
/** Generate a "LDY #value" in the output stream. */
public AsmBuilder ldy(int value) {
builder.add(state -> internalLDY(state, value));
return this;
}
/** Generate a "JMP address" in the output stream. */
public AsmBuilder jmp(int address) {
builder.add(state -> internalJMP(state, address));
return this;
}
/** Generate a "LDA #value" in the output stream. */
public AsmBuilder lda(int value) throws IOException {
builder.add(state -> internalLDA(state, value));
return this;
}
/** Generate a "STA address" in the output stream. */
public AsmBuilder sta(int address) throws IOException {
builder.add(state -> internalSTA(state, address));
return this;
}
/**
* Generate an address setup in the output stream of the format:
* <pre>
* LDA #low(value)
* STA address
* LDA #high(value)
* STA address+1
* </pre>
*/
public AsmBuilder setAddress(int value, int address) {
builder.add(state -> {
internalLDA(state, value & 0xff);
internalSTA(state, address);
internalLDA(state, value >> 8);
internalSTA(state, address+1);
});
return this;
}
/**
* Generate an address setup for a mark in the output stream of the format:
* <pre>
* LDA #low(mark)
* STA address
* LDA #high(mark)
* STA address+1
* </pre>
*/
public AsmBuilder setAddress(CodeMark mark, int address) {
builder.add(state -> {
int value = mark.getAddress();
internalLDA(state, value & 0xff);
internalSTA(state, address);
internalLDA(state, value >> 8);
internalSTA(state, address+1);
});
return this;
}
private void internalJMP(GeneratorState state, int address) {
state.write(0x4c);
state.write(address & 0xff);
state.write(address >> 8);
}
private void internalLDY(GeneratorState state, int value) {
state.write(0xa0);
state.write(value);
}
private void internalLDA(GeneratorState state, int value) {
state.write(0xa9);
state.write(value);
}
private void internalSTA(GeneratorState state, int address) {
if ((address & 0xff00) == 0) {
state.write(0x85);
state.write(address);
} else {
throw new RuntimeException("sta does not handle 16 bit addresses yet!");
}
}
}

View File

@ -0,0 +1,120 @@
package io.github.applecommander.bastools.api.code;
import java.util.Objects;
import io.github.applecommander.bastools.api.model.ApplesoftKeyword;
/**
* {@code BasicBuilder} allows BASIC commands to be built. Note that {@code #endLine()} and {{@link #endStatement()}
* are items that need to be invoked by hand.
* <p>
* By no means is this complete, but is being built out as the need arises.
*/
public class BasicBuilder {
private CodeBuilder builder;
public BasicBuilder(CodeBuilder builder) {
Objects.requireNonNull(builder);
this.builder = builder;
}
public CodeBuilder end() {
return this.builder;
}
/** Generate a "RETURN" statement. */
public BasicBuilder RETURN() {
builder.add(state -> state.write(ApplesoftKeyword.RETURN.code));
return this;
}
/** Generate a "GOTO <lineNumber>" statement. */
public BasicBuilder GOTO(int lineNumber) {
builder.add(state -> {
state.write(ApplesoftKeyword.GOTO.code);
state.write(Integer.toString(lineNumber).getBytes());
});
return this;
}
/** Generate a "GOSUB <lineNumber>" statement. */
public BasicBuilder GOSUB(int lineNumber) {
builder.add(state -> {
state.write(ApplesoftKeyword.GOSUB.code);
state.write(Integer.toString(lineNumber).getBytes());
});
return this;
}
/** Generate a "CALL <markAddress>" statement. */
public BasicBuilder CALL(CodeMark mark) {
builder.add(state -> {
int address = mark.getAddress();
state.write(ApplesoftKeyword.CALL.code);
state.write(Integer.toString(address).getBytes());
});
return this;
}
/** Generate a "POKE <address>,<lowMarkAddress>:POKE <address+1>,<highMarkAddress>" set of statements. */
public BasicBuilder POKEW(int address, CodeMark mark) {
builder.add(state -> {
int value = mark.getAddress();
state.write(ApplesoftKeyword.POKE.code);
state.write(Integer.toString(address).getBytes());
state.write(',');
state.write(Integer.toString(value & 0xff).getBytes());
state.write(':');
state.write(ApplesoftKeyword.POKE.code);
state.write(Integer.toString(address+1).getBytes());
state.write(',');
state.write(Integer.toString(value >> 8).getBytes());
});
return this;
}
/** Generate a statement separator. */
public BasicBuilder endStatement() {
builder.add(state -> state.write(':'));
return this;
}
/** Generate an assignment statement. */
public BasicBuilder assign(String varName, CodeMark mark) {
builder.add(state -> {
state.write(varName.getBytes());
state.write(ApplesoftKeyword.eq.code);
state.write(Integer.toString(mark.getAddress()).getBytes());
});
return this;
}
/** Generate an assignment statement. */
public BasicBuilder assign(String varName, int value) {
builder.add(state -> {
state.write(varName.getBytes());
state.write(ApplesoftKeyword.eq.code);
state.write(Integer.toString(value).getBytes());
});
return this;
}
/** End the current line. No more BASIC after this point! */
public CodeBuilder endLine() {
builder.add(state -> state.write(0x00));
return builder;
}
/** Generate a "ROT=<0-64>" statement. */
public BasicBuilder ROT(int lineNumber) {
builder.add(state -> {
state.write(ApplesoftKeyword.ROT.code);
state.write(Integer.toString(lineNumber).getBytes());
});
return this;
}
/** Generate a "SCALE=<1-255>" statement. */
public BasicBuilder SCALE(int lineNumber) {
builder.add(state -> {
state.write(ApplesoftKeyword.SCALE.code);
state.write(Integer.toString(lineNumber).getBytes());
});
return this;
}
/** Generate a "HCOLOR=<0-7>" statement. */
public BasicBuilder HCOLOR(int lineNumber) {
builder.add(state -> {
state.write(ApplesoftKeyword.HCOLOR.code);
state.write(Integer.toString(lineNumber).getBytes());
});
return this;
}
}

View File

@ -0,0 +1,55 @@
package io.github.applecommander.bastools.api.code;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* {@code CodeBuilder} allows dynamic generation of combined BASIC and Assembly code with dynamic
* {@code CodeMark} capability. This allows forward references to unknown address in a (mostly) safe
* manner.
*/
public class CodeBuilder {
private CodeGenerator generatorChain = (os) -> {};
/**
* Generate this set of code beginning at the starting address.
* @return ByteArrayOutputStream which allows {@code ByteArrayOutputStream#writeTo(java.io.OutputStream)}
* and {@code ByteArrayOutputStream#toByteArray()}
*/
public ByteArrayOutputStream generate(int startAddress) throws IOException {
GeneratorState state = new GeneratorState(startAddress);
do {
state.reset();
generatorChain.generate(state);
} while (state.hasMarkMoved());
return state.outputStream();
}
/** Start generating BASIC code. */
public BasicBuilder basic() {
return new BasicBuilder(this);
}
/** Start generating Assembly code. */
public AsmBuilder asm() {
return new AsmBuilder(this);
}
/** Helper method to chain in a {@code CodeGenerator}. */
public CodeBuilder add(CodeGenerator generator) {
generatorChain = generatorChain.andThen(generator);
return this;
}
/** Set a {@code CodeMark}'s value. */
public CodeBuilder set(CodeMark mark) {
return add(state -> {
// A bit twisted, but this allows the GeneratorState and CodeMark to interact without our intervention.
state.update(mark);
});
}
/** Add a {@code byte[]} to this stream. */
public CodeBuilder addBinary(byte[] data) {
return add(state -> {
state.write(data);
});
}
}

View File

@ -0,0 +1,32 @@
package io.github.applecommander.bastools.api.code;
import java.io.IOException;
import java.util.Objects;
/**
* Represents a code generation operation that accepts the current {@code GeneratorState}
* and performs operations against that state.
*/
@FunctionalInterface
public interface CodeGenerator {
/**
* Generates code and writes the bytes into the given {@code OutputStream}.
*/
public void generate(GeneratorState state) throws IOException;
/**
* Returns a composed {@code CodeGenerator} that performs, in sequence, this
* operation followed by the {@code after} operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation. If performing this operation throws an exception,
* the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code Consumer} that performs in sequence this
* operation followed by the {@code after} operation
* @throws NullPointerException if {@code after} is null
*/
public default CodeGenerator andThen(CodeGenerator after) {
Objects.requireNonNull(after);
return (GeneratorState state) -> { generate(state); after.generate(state); };
}
}

View File

@ -0,0 +1,58 @@
package io.github.applecommander.bastools.api.code;
import java.util.HashMap;
/**
* A {@code CodeMark} marks a dynamic address within the output stream. When referenced, it will report the
* most recent address is knows, forcing the generation to run multiple times until it "settles".
* <p>
* Multiple passes occur for the following reasons:<ul>
* <li>an assembly address can be calculated on the second pass (1st is to actually calculate address and 2nd pass is to
* use that address).</li>
* <li>Applesoft BASIC encodes the address as text; that means first pass, the address is "0" but 2nd pass the address is
* likely to be 4 digits "8123" (for example) which, in turn moves anything after that point, requiring a 3rd pass
* to push everything out (and likely making that number become "8126" since going from 1 digit to 4 digits adds
* 3 bytes to the preceding bytes</li>
* </ul>
*
* @author rob
*/
public class CodeMark {
private static final int LOOP_MAX = 10;
private HashMap<Integer,Integer> loopCounter = new HashMap<>();
private int address;
public int getAddress() {
return address;
}
/**
* Update the current address based on the {@code GeneratorState}.
* @return boolean indicating if the address changed
*/
public boolean update(GeneratorState state) {
int currentAddress = state.currentAddress();
loopCounter.merge(currentAddress, 1, (a,b) -> a+b);
if (loopCounter.get(currentAddress) > LOOP_MAX) {
StringBuilder sb = new StringBuilder();
sb.append("A circular pattern in a dynamic address was discovered!\n");
sb.append("This usually indicates that an address was computed to be just below a page boundary.\n");
sb.append("For example, the 0x1000 mark. However, code using that address then pushed the\n");
sb.append("address over the 0x1000 mark, but that triggered the address to be recomputed below\n");
sb.append("the 0x1000 mark. Rinse and repeat.\n");
sb.append("\n");
sb.append("For example, shape tables have a POKE 232,255 when the shape table is at 0xFFF, but the\n");
sb.append("address gets recomputed (due to the 3 digit low address) to be 0x1001, which changes the\n");
sb.append("low address byte to be a single digit. This starts the cascade.\n");
sb.append("\n");
sb.append("Generally, stick a little bit of extra code into the program bypasses this issue.\n");
sb.append("(Sorry, there is no elegant solution at this time. Pull requests are welcome!).\n");
throw new IllegalStateException(sb.toString());
}
try {
return currentAddress != address;
} finally {
this.address = currentAddress;
}
}
}

View File

@ -0,0 +1,52 @@
package io.github.applecommander.bastools.api.code;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* Track current state of the code generation. This class proxies a number of objects and can be extended
* for those objects as required.
*/
public class GeneratorState {
private final int startAddress;
private boolean markMoved = false;
private ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
public GeneratorState(int startAddress) {
this.startAddress = startAddress;
}
/** Clear current state for another pass. Used while the generation is "settling down". */
public void reset() {
this.markMoved = false;
this.outputStream.reset();
}
/** Indicates if a CodeMark has moved. */
public boolean hasMarkMoved() {
return this.markMoved;
}
/** Hook for the CodeMark to be updated and to capture if a change occurred. */
public void update(CodeMark mark) {
markMoved |= mark.update(this);
}
/** Grab the {@code ByteArrayOutputStream}. Only valid once generation is complete. */
public ByteArrayOutputStream outputStream() {
return this.outputStream;
}
/** This is the current address as defined by the start address + number of bytes generated. */
public int currentAddress() {
return startAddress + outputStream.size();
}
/** Write a byte to the output stream. */
public void write(int b) {
outputStream.write(b);
}
/** Write entire byte array to the output stream. */
public void write(byte[] b) throws IOException {
outputStream.write(b);
}
}

View File

@ -0,0 +1,87 @@
package io.github.applecommander.bastools.api.directives;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Optional;
import io.github.applecommander.bastools.api.Configuration;
import io.github.applecommander.bastools.api.Directive;
import io.github.applecommander.bastools.api.code.CodeBuilder;
import io.github.applecommander.bastools.api.code.CodeMark;
import io.github.applecommander.bastools.api.model.Line;
/**
* Embed an binary file into a BASIC program. See writeup in the README-TOKENIZER.md file.
*/
public class EmbeddedBinaryDirective extends Directive {
public static final String NAME = "$embed";
public static final String PARAM_FILE = "file";
public static final String PARAM_MOVETO = "moveto";
public static final String PARAM_VAR = "var";
public EmbeddedBinaryDirective(Configuration config, OutputStream outputStream) {
super(NAME, config, outputStream, PARAM_FILE, PARAM_MOVETO, PARAM_VAR);
}
@Override
public void writeBytes(int startAddress, Line line) throws IOException {
String filename = requiredStringExpression(PARAM_FILE, "$embed requires a 'name=<string>' parameter");
Optional<Integer> targetAddress = optionalIntegerExpression(PARAM_MOVETO);
Optional<String> variableName = optionalStringExpression(PARAM_VAR);
validateSet(ONLY_ONE, "$embed requires either a 'var' assignment or a 'moveto' parameter", targetAddress, variableName);
File file = new File(config.sourceFile.getParentFile(), filename);
byte[] bin = Files.readAllBytes(file.toPath());
CodeBuilder builder = new CodeBuilder();
CodeMark moveStart = new CodeMark();
CodeMark embeddedStart = new CodeMark();
CodeMark embeddedEnd = new CodeMark();
variableName.ifPresent(var -> {
builder.basic()
.assign(resolve(var), embeddedStart)
.endStatement();
});
targetAddress.ifPresent(address -> {
builder.basic()
.CALL(moveStart)
.endStatement();
Optional<Line> nextLine = line.nextLine();
if (nextLine.isPresent()) {
builder.basic()
.GOTO(nextLine.get().lineNumber);
} else {
builder.basic()
.RETURN();
}
});
builder.basic()
.endLine();
targetAddress.ifPresent(address -> {
builder.set(moveStart)
.asm()
.setAddress(embeddedStart, 0x3c)
.setAddress(embeddedEnd, 0x3e)
.setAddress(address, 0x42)
.ldy(0x00)
.jmp(0xfe2c)
.end();
});
builder.set(embeddedStart)
.addBinary(bin)
.set(embeddedEnd);
builder.generate(startAddress)
.writeTo(super.outputStream);
}
}

View File

@ -0,0 +1,136 @@
package io.github.applecommander.bastools.api.directives;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.util.Optional;
import io.github.applecommander.bastools.api.Configuration;
import io.github.applecommander.bastools.api.Directive;
import io.github.applecommander.bastools.api.code.BasicBuilder;
import io.github.applecommander.bastools.api.code.CodeBuilder;
import io.github.applecommander.bastools.api.code.CodeMark;
import io.github.applecommander.bastools.api.model.Line;
import io.github.applecommander.bastools.api.shapes.Shape;
import io.github.applecommander.bastools.api.shapes.ShapeGenerator;
import io.github.applecommander.bastools.api.shapes.ShapeTable;
/**
* Embed an Applesoft shape table into a BASIC program. See writeup in the README-TOKENIZER.md file.
*/
public class EmbeddedShapeTable extends Directive {
public static final String NAME = "$shape";
public static final String PARAM_SRC = "src";
public static final String PARAM_LABEL = "label";
public static final String VALUE_VARIABLE = "variable";
public static final String PARAM_BIN = "bin";
public static final String PARAM_POKE = "poke";
public static final String PARAM_ASSIGN = "assign";
public static final String PARAM_INIT = "init";
public static final String PARAM_ADDRESS = "address";
public EmbeddedShapeTable(Configuration config, OutputStream outputStream) {
super(NAME, config, outputStream, PARAM_SRC, PARAM_LABEL, PARAM_BIN, PARAM_POKE,
PARAM_ASSIGN, PARAM_INIT, PARAM_ADDRESS);
}
/**
* Parse the given parameters, generating code and embedding shape table as directed.
*/
@Override
public void writeBytes(int startAddress, Line line) throws IOException {
Optional<String> src = optionalStringExpression(PARAM_SRC);
Optional<String> label = optionalStringExpression(PARAM_LABEL);
Optional<MapExpression> assign = optionalMapExpression(PARAM_ASSIGN);
Optional<String> bin = optionalStringExpression(PARAM_BIN);
boolean poke = defaultBooleanExpression(PARAM_POKE, true);
boolean init = defaultBooleanExpression(PARAM_INIT, true);
Optional<String> address = optionalStringExpression(PARAM_ADDRESS);
// Validation
validateSet(ONLY_ONE, "Please include a 'src' or a 'bin' as part $shape directive, but not both", src, bin);
validateSet(ZERO_OR_ONE, "Cannot specify both 'label' and 'assign' in $shape directive", label, assign);
bin.ifPresent(x -> validateSet(ZERO, "'bin' does not support 'label' or 'assign'", label, assign));
// Load in specified data file
Optional<byte[]> binData = bin.map(this::readBin);
Optional<ShapeTable> shapeTable = src.map(this::readSrc);
// Setup code builders
CodeMark shapeTableStart = new CodeMark();
CodeBuilder builder = new CodeBuilder();
BasicBuilder basic = builder.basic();
// Setup common code
if (poke) basic.POKEW(232, shapeTableStart).endStatement();
if (init) basic.ROT(0).endStatement().SCALE(1).endStatement();
address.ifPresent(var -> basic.assign(resolve(var), shapeTableStart).endStatement());
// Inject src options
assign.ifPresent(expr -> setupVariables(expr, basic, shapeTable));
label.ifPresent(opt -> setupLabels(opt, basic, shapeTable));
// We need to terminate a binary embedded line with some mechanism of skipping the binary content.
Optional<Line> nextLineOpt = line.nextLine();
nextLineOpt.ifPresent(nextLine -> basic.GOTO(nextLine.lineNumber));
if (!nextLineOpt.isPresent()) basic.RETURN();
// End line and inject binary content
basic.endLine().set(shapeTableStart);
binData.ifPresent(builder::addBinary);
shapeTable.map(this::mapShapeTableToBin).ifPresent(builder::addBinary);
builder.generate(startAddress).writeTo(this.outputStream);
}
public void setupVariables(MapExpression expr, BasicBuilder basic, Optional<ShapeTable> shapeTableOptional) {
ShapeTable st = shapeTableOptional.orElseThrow(() -> new RuntimeException("ShapeTable source not supplied"));
expr.entrySet().forEach(e -> {
String label = e.getValue().toSimpleExpression()
.map(SimpleExpression::asString)
.orElseThrow(() -> new RuntimeException(
String.format("Unexpected format of asignments for variable '%s'", e.getKey())));
basic.assign(resolve(e.getKey()), st.findPositionByLabel(label)).endStatement();
});
}
public void setupLabels(String labelOption, BasicBuilder basic, Optional<ShapeTable> shapeTableOptional) {
if (!"variable".equalsIgnoreCase(labelOption)) {
throw new RuntimeException(String.format("Unexpected label option of '%s'", labelOption));
}
ShapeTable st = shapeTableOptional.orElseThrow(() -> new RuntimeException("ShapeTable source not supplied"));
for (int i=0; i<st.shapes.size(); i++) {
Shape s = st.shapes.get(i);
basic.assign(resolve(s.getLabel()), i+1);
}
}
public byte[] readBin(String filename) {
try {
File file = new File(config.sourceFile.getParentFile(), filename);
return Files.readAllBytes(file.toPath());
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
public ShapeTable readSrc(String filename) {
try {
File file = new File(config.sourceFile.getParentFile(), filename);
return ShapeGenerator.generate(file);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
public byte[] mapShapeTableToBin(ShapeTable shapeTable) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
shapeTable.write(outputStream);
return outputStream.toByteArray();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}

View File

@ -0,0 +1,41 @@
package io.github.applecommander.bastools.api.directives;
import java.io.IOException;
import java.io.OutputStream;
import io.github.applecommander.bastools.api.Configuration;
import io.github.applecommander.bastools.api.Directive;
import io.github.applecommander.bastools.api.model.ApplesoftKeyword;
import io.github.applecommander.bastools.api.model.Line;
/**
* A simple directive to introduce hexadecimal capabilities. StreamTokenizer does not
* appear to support syntax, so using a directive to introduce the capability.
*/
public class HexDirective extends Directive {
public static final String NAME = "$hex";
public HexDirective(Configuration config, OutputStream outputStream) {
super(NAME, config, outputStream);
}
@Override
public void writeBytes(int startAddress, Line line) throws IOException {
int value = requiredIntegerExpression("value", "$hex directive requires 'value' parameter");
if (value < 0 || value > 65535) {
throw new RuntimeException("$hex address out of range");
}
String value1 = Integer.toString(value);
String value2 = Integer.toString(value - 65536);
String shortestValue = value1.length() < value2.length() ? value1 : value2;
if (shortestValue.startsWith("-")) {
outputStream.write(ApplesoftKeyword.sub.code);
shortestValue = shortestValue.substring(1);
}
outputStream.write(shortestValue.getBytes());
}
}

View File

@ -1,4 +1,4 @@
package io.github.applecommander.bastokenizer.api.model;
package io.github.applecommander.bastools.api.model;
import java.io.IOException;
import java.io.Reader;
@ -71,6 +71,7 @@ public enum ApplesoftKeyword {
DEF(0xB8, "DEF"),
POKE(0xB9, "POKE"),
PRINT(0xBA, "PRINT"),
questionmark(0xBA, "?"), // Alternate form of PRINT
CONT(0xBB, "CONT"),
LIST(0xBC, "LIST"),
CLEAR(0xBD, "CLEAR"),
@ -178,6 +179,7 @@ public enum ApplesoftKeyword {
tokenizer.resetSyntax();
tokenizer.wordChars('a', 'z');
tokenizer.wordChars('A', 'Z');
tokenizer.wordChars('?', '?'); // For '?' form of PRINT
tokenizer.wordChars('$', '$'); // Experiment to pull in string marker
tokenizer.wordChars('%', '%'); // Experiment to pull in integer marker
tokenizer.wordChars(128 + 32, 255);
@ -187,6 +189,8 @@ public enum ApplesoftKeyword {
// This resets part of parseNumbers to match AppleSoft tokenization!
tokenizer.ordinaryChar('-');
tokenizer.eolIsSignificant(true);
// Special case: Allow a single line comment with ' as that is not part of language
tokenizer.commentChar('\'');
return tokenizer;
}

View File

@ -1,11 +1,11 @@
package io.github.applecommander.bastokenizer.api.model;
package io.github.applecommander.bastools.api.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import io.github.applecommander.bastokenizer.api.Visitor;
import io.github.applecommander.bastools.api.Visitor;
/** An AppleSoft BASIC Line representation. */
public class Line {
@ -19,6 +19,10 @@ public class Line {
this.program = program;
}
public int getLineNumber() {
return lineNumber;
}
public Optional<Line> nextLine() {
int i = program.lines.indexOf(this);
if (i == -1 || i+1 >= program.lines.size()) {

View File

@ -1,9 +1,9 @@
package io.github.applecommander.bastokenizer.api.model;
package io.github.applecommander.bastools.api.model;
import java.util.ArrayList;
import java.util.List;
import io.github.applecommander.bastokenizer.api.Visitor;
import io.github.applecommander.bastools.api.Visitor;
/** A Program is a series of lines. */
public class Program {

View File

@ -1,9 +1,9 @@
package io.github.applecommander.bastokenizer.api.model;
package io.github.applecommander.bastools.api.model;
import java.util.ArrayList;
import java.util.List;
import io.github.applecommander.bastokenizer.api.Visitor;
import io.github.applecommander.bastools.api.Visitor;
/** A Statement is simply a series of Tokens. */
public class Statement {

View File

@ -1,6 +1,6 @@
package io.github.applecommander.bastokenizer.api.model;
package io.github.applecommander.bastools.api.model;
import io.github.applecommander.bastokenizer.api.Visitor;
import io.github.applecommander.bastools.api.Visitor;
/**
* A Token in the classic compiler sense, in that this represents a component of the application.
@ -37,6 +37,18 @@ public class Token {
return String.format("%s(%s)", type, text);
}
}
public String asString() {
switch (type) {
case EOL:
return "\n";
case KEYWORD:
return keyword.toString();
case NUMBER:
return number.toString();
default:
return text;
}
}
public static Token eol(int line) {
return new Token(line, Type.EOL, null, null, null);

View File

@ -1,16 +1,20 @@
package io.github.applecommander.bastokenizer.api.optimizations;
package io.github.applecommander.bastools.api.optimizations;
import java.util.HashMap;
import java.util.Map;
import io.github.applecommander.bastokenizer.api.Visitor;
import io.github.applecommander.bastokenizer.api.Visitors;
import io.github.applecommander.bastokenizer.api.model.Line;
import io.github.applecommander.bastokenizer.api.model.Program;
import io.github.applecommander.bastokenizer.api.model.Statement;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastools.api.Visitor;
import io.github.applecommander.bastools.api.Visitors;
import io.github.applecommander.bastools.api.model.Line;
import io.github.applecommander.bastools.api.model.Program;
import io.github.applecommander.bastools.api.model.Statement;
import io.github.applecommander.bastools.api.model.Token;
/** Common base class for optimization visitors that allow the program tree to be rewritten. */
/**
* Common base class for optimization visitors that allow the program tree to be rewritten.
* Note that {@code #reassignments} is used to track line number movement and is <em>automatically</em>
* applied at the end of the program visit.
*/
public class BaseVisitor implements Visitor {
protected Map<Integer,Integer> reassignments = new HashMap<>();
protected Program newProgram;

View File

@ -0,0 +1,150 @@
package io.github.applecommander.bastools.api.optimizations;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import io.github.applecommander.bastools.api.Configuration;
import io.github.applecommander.bastools.api.Visitors;
import io.github.applecommander.bastools.api.model.ApplesoftKeyword;
import io.github.applecommander.bastools.api.model.Line;
import io.github.applecommander.bastools.api.model.Program;
import io.github.applecommander.bastools.api.model.Statement;
import io.github.applecommander.bastools.api.model.Token;
import io.github.applecommander.bastools.api.utils.VariableNameGenerator;
import io.github.applecommander.bastools.api.visitors.VariableCollectorVisitor;
/**
* Find constants and extract to variables in order to have the number parsed only once.
*/
public class ExtractConstantValues extends BaseVisitor {
/** These trigger the start of a replacement range. Note the special logic for assignments. */
public static List<ApplesoftKeyword> TARGET_STARTS = Arrays.asList(
ApplesoftKeyword.FOR, ApplesoftKeyword.CALL, ApplesoftKeyword.PLOT, ApplesoftKeyword.HLIN,
ApplesoftKeyword.VLIN, ApplesoftKeyword.HCOLOR, ApplesoftKeyword.HPLOT, ApplesoftKeyword.DRAW,
ApplesoftKeyword.XDRAW, ApplesoftKeyword.HTAB, ApplesoftKeyword.SCALE, ApplesoftKeyword.COLOR,
ApplesoftKeyword.VTAB, ApplesoftKeyword.HIMEM, ApplesoftKeyword.LOMEM, ApplesoftKeyword.SPEED,
ApplesoftKeyword.LET, ApplesoftKeyword.IF, ApplesoftKeyword.ON, ApplesoftKeyword.WAIT,
ApplesoftKeyword.POKE);
/** These trigger the end of a replacement range. End of statement is always an end. */
public static List<ApplesoftKeyword> TARGET_ENDS = Arrays.asList(
ApplesoftKeyword.GOTO, ApplesoftKeyword.GOSUB, ApplesoftKeyword.THEN);
// Map keyed by value (Double isn't a good key, using a String of the number) and pointing to replacement variable name
private Map<String,String> map = new HashMap<>();
private VariableNameGenerator variableGenerator = new VariableNameGenerator();
private Set<String> existingVariables;
private Function<Token,Token> consumer = this::nullTransformation;
public ExtractConstantValues(Configuration config) {
// ignored
}
public Token nullTransformation(Token token) {
return token;
}
/** Collect a map of constant values and the new variable name to be used. */
public Token numberToIdentTransformation(Token token) {
String key = token.number.toString();
// New entry, create it
if (!map.containsKey(key)) {
String varName = null;
do {
varName = variableGenerator.get()
.orElseThrow(() -> new RuntimeException("Ran out of variable names to assign"));
} while (existingVariables.contains(varName));
map.put(key, varName);
}
// Existing (or NEW!) entry, swap to that variable.
if (map.containsKey(key)) {
return Token.ident(token.line, map.get(key));
}
return token;
}
@Override
public Program visit(Program program) {
VariableCollectorVisitor collector = Visitors.variableCollectorVisitor();
program.accept(collector);
this.existingVariables = collector.getVariableNames();
program = super.visit(program);
injectLine0(program);
return program;
}
private void injectLine0(Program program) {
Line line = generateLine0(program);
// Bypass if there were no constants
if (line.statements.isEmpty()) return;
// setup a renumber of lines that interfere if we have any
if (program.lines.get(0).lineNumber == 0) {
// start with line #0 should become line #1
super.reassignments.put(0, 1);
// chase it to the end!
program.lines.stream()
.map(Line::getLineNumber)
.filter(super.reassignments::containsValue)
.forEach(n -> { super.reassignments.put(n, n+1); });
}
program.lines.add(0, line);
}
private Line generateLine0(Program program) {
Line line = new Line(0, program);
map.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.map(this::toStatement)
.forEach(line.statements::add);
return line;
}
private Statement toStatement(Map.Entry<String,String> variable) {
Statement statement = new Statement();
statement.tokens.add(Token.ident(-1, variable.getValue()));
statement.tokens.add(Token.syntax(-1, '='));
statement.tokens.add(Token.number(-1, Double.valueOf(variable.getKey())));
return statement;
}
@Override
public Statement visit(Statement statement) {
try {
if (!statement.tokens.isEmpty()) {
Token t = statement.tokens.get(0);
// Assignment
if (t.type == Token.Type.IDENT) {
this.consumer = this::numberToIdentTransformation;
}
// Assignment with LET
if (t.type == Token.Type.KEYWORD && t.keyword == ApplesoftKeyword.LET) {
this.consumer = this::numberToIdentTransformation;
}
}
return super.visit(statement);
} finally {
this.consumer = this::nullTransformation;
}
}
@Override
public Token visit(Token token) {
switch (token.type) {
case KEYWORD:
if (TARGET_STARTS.contains(token.keyword)) {
this.consumer = this::numberToIdentTransformation;
} else if (TARGET_ENDS.contains(token.keyword)) {
this.consumer = this::nullTransformation;
}
break;
case NUMBER:
return this.consumer.apply(token);
default:
break;
}
return super.visit(token);
}
}

View File

@ -1,18 +1,18 @@
package io.github.applecommander.bastokenizer.api.optimizations;
package io.github.applecommander.bastools.api.optimizations;
import java.io.PrintStream;
import java.util.Set;
import io.github.applecommander.bastokenizer.api.Configuration;
import io.github.applecommander.bastokenizer.api.Visitors;
import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword;
import io.github.applecommander.bastokenizer.api.model.Line;
import io.github.applecommander.bastokenizer.api.model.Program;
import io.github.applecommander.bastokenizer.api.model.Statement;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastokenizer.api.model.Token.Type;
import io.github.applecommander.bastokenizer.api.visitors.ByteVisitor;
import io.github.applecommander.bastokenizer.api.visitors.LineNumberTargetCollector;
import io.github.applecommander.bastools.api.Configuration;
import io.github.applecommander.bastools.api.Visitors;
import io.github.applecommander.bastools.api.model.ApplesoftKeyword;
import io.github.applecommander.bastools.api.model.Line;
import io.github.applecommander.bastools.api.model.Program;
import io.github.applecommander.bastools.api.model.Statement;
import io.github.applecommander.bastools.api.model.Token;
import io.github.applecommander.bastools.api.model.Token.Type;
import io.github.applecommander.bastools.api.visitors.ByteVisitor;
import io.github.applecommander.bastools.api.visitors.LineNumberTargetCollector;
public class MergeLines extends BaseVisitor {
private Set<Integer> targets;

View File

@ -0,0 +1,16 @@
package io.github.applecommander.bastools.api.optimizations;
import io.github.applecommander.bastools.api.Configuration;
import io.github.applecommander.bastools.api.model.Statement;
/** Remove any empty statements during the tree walk. Effective removes double "::"'s. */
public class RemoveEmptyStatements extends BaseVisitor {
public RemoveEmptyStatements(Configuration config) {
// ignored
}
@Override
public Statement visit(Statement statement) {
return statement.tokens.isEmpty() ? null : statement;
}
}

View File

@ -0,0 +1,17 @@
package io.github.applecommander.bastools.api.optimizations;
import io.github.applecommander.bastools.api.Configuration;
import io.github.applecommander.bastools.api.model.Statement;
import io.github.applecommander.bastools.api.model.Token.Type;
/** Drop all REM statements as they are encountered in the tree walk. */
public class RemoveRemStatements extends BaseVisitor {
public RemoveRemStatements(Configuration config) {
// ignored
}
@Override
public Statement visit(Statement statement) {
return statement.tokens.get(0).type == Type.COMMENT ? null : statement;
}
}

View File

@ -1,9 +1,19 @@
package io.github.applecommander.bastokenizer.api.optimizations;
package io.github.applecommander.bastools.api.optimizations;
import io.github.applecommander.bastokenizer.api.model.Line;
import io.github.applecommander.bastools.api.Configuration;
import io.github.applecommander.bastools.api.model.Line;
/**
* A simple renumbering algorithm that maps the reassignments and lets {@code BaseVisitor}
* perform the actual renumbering!
*/
public class Renumber extends BaseVisitor {
protected int lineNumber = 0;
public Renumber(Configuration config) {
// ignored
}
@Override
public Line visit(Line line) {
Line newLine = new Line(lineNumber++, this.newProgram);

View File

@ -0,0 +1,68 @@
package io.github.applecommander.bastools.api.optimizations;
import java.util.Set;
import io.github.applecommander.bastools.api.Configuration;
import io.github.applecommander.bastools.api.Visitors;
import io.github.applecommander.bastools.api.model.Program;
import io.github.applecommander.bastools.api.model.Statement;
import io.github.applecommander.bastools.api.model.Token;
import io.github.applecommander.bastools.api.model.Token.Type;
import io.github.applecommander.bastools.api.utils.VariableNameGenerator;
import io.github.applecommander.bastools.api.visitors.VariableCollectorVisitor;
/**
* Ensure all variable names are 1 or 2 characters long.
* This allows the source to use more descriptive variable names, which may
* crossover ("PLAYERX" and "PLAYERY" become "PL" as far Applesoft BASIC is
* concerned). Somewhat hampers running without this optimization being used,
* however.
*/
public class ShortenVariableNames extends BaseVisitor {
private Configuration config;
private VariableNameGenerator variableGenerator = new VariableNameGenerator();
public ShortenVariableNames(Configuration config) {
this.config = config;
}
@Override
public Program visit(Program program) {
// Find existing variable names so we don't clobber already existing names
VariableCollectorVisitor collector = Visitors.variableCollectorVisitor();
program.accept(collector);
Set<String> existingVariables = collector.getVariableNames();
// Preassign all variable names
for (String originalName : existingVariables) {
String newName = originalName;
if (newName.replaceAll("[^\\p{Alnum}]","").length() > 2) {
String varType = newName.replaceAll("[\\p{Alnum}]","");
do {
newName = variableGenerator.get().orElseThrow(() -> new RuntimeException("Ran out of variable names to assign"));
newName += varType;
} while (existingVariables.contains(newName));
config.debugStream.printf("Replacing '%s' with '%s'\n", originalName, newName);
}
config.variableReplacements.put(originalName, newName);
}
// Continue walking the tree to replace the variables!
return super.visit(program);
}
@Override
public Statement visit(Statement statement) {
if (statement.tokens.get(0).type == Type.DIRECTIVE) {
return statement;
}
return super.visit(statement);
}
@Override
public Token visit(Token token) {
if (token.type == Type.IDENT) {
return Token.ident(token.line, config.variableReplacements.get(token.text));
}
return super.visit(token);
}
}

View File

@ -0,0 +1,357 @@
package io.github.applecommander.bastools.api.shapes;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Represents a bitmap copy of the shape.
* This may be useful for displaying the shape or for defining shapes as a bitmap is
* easier to understand than vectors.
*/
public class BitmapShape implements Shape {
public final String label;
public final List<List<Boolean>> grid = new ArrayList<>();
public final Point origin = new Point();
public BitmapShape() {
this(0, 0, null);
}
public BitmapShape(String label) {
this(0, 0, label);
}
public BitmapShape(int height, int width) {
this(height, width, null);
}
public BitmapShape(int height, int width, String label) {
this.label = label;
while (this.grid.size() < height) {
this.grid.add(newRow(width));
}
}
private List<Boolean> newRow(int width) {
List<Boolean> row = new ArrayList<>();
while (row.size() < width) {
row.add(Boolean.FALSE);
}
return row;
}
public void insertColumn() {
origin.x++;
for (List<Boolean> row : grid) {
row.add(0, Boolean.FALSE);
}
}
public void addColumn() {
for (List<Boolean> row : grid) {
row.add(Boolean.FALSE);
}
}
public void insertRow() {
origin.y++;
grid.add(0, newRow(getWidth()));
}
public void addRow() {
grid.add(newRow(getWidth()));
}
public void appendBitmapRow(String line) {
line = line.trim();
List<Boolean> row = new ArrayList<>();
Runnable setOrigin = () -> {
// Share origin logic for '+' and '*'
origin.x = row.size();
origin.y = grid.size();
};
for (char pixel : line.toCharArray()) {
switch (Character.toLowerCase(pixel)) {
case '+':
setOrigin.run();
// fall through to '.'
case '.':
row.add(Boolean.FALSE);
break;
case '*':
setOrigin.run();
// fall through to 'x'
case 'x':
row.add(Boolean.TRUE);
break;
default:
throw new RuntimeException("Unexpected bitmap pixel type: " + pixel);
}
}
grid.add(row);
}
public int getHeight() {
return grid.size();
}
public int getWidth() {
return grid.isEmpty() ? 0 : grid.get(0).size();
}
public void plot(int x, int y) {
plot(x, y, Boolean.TRUE);
}
public void plot(int x, int y, Boolean pixel) {
if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) {
return;
}
grid.get(y).set(x, pixel);
}
public Boolean get(int x, int y) {
if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) {
return Boolean.FALSE;
}
return grid.get(y).get(x);
}
public Boolean get(Point point) {
return get(point.x, point.y);
}
@Override
public boolean isEmpty() {
boolean hasData = false;
for (List<Boolean> row : grid) {
for (Boolean plot : row) {
hasData |= plot;
}
}
return !hasData;
}
@Override
public String getLabel() {
return label;
}
@Override
public BitmapShape toBitmap() {
return this;
}
/**
* Convert this bitmap shape to a vector shape. The shape chosen encodes to the least number of bytes
* in the resulting file.
*/
@Override
public VectorShape toVector() {
List<Supplier<VectorShape>> scans = Arrays.asList(
new SweepVectorization(this, VectorCommand.MOVE_RIGHT, VectorCommand.MOVE_UP),
new SweepVectorization(this, VectorCommand.MOVE_RIGHT, VectorCommand.MOVE_DOWN),
new SweepVectorization(this, VectorCommand.MOVE_LEFT, VectorCommand.MOVE_UP),
new SweepVectorization(this, VectorCommand.MOVE_LEFT, VectorCommand.MOVE_DOWN),
new SweepVectorization(this, VectorCommand.MOVE_DOWN, VectorCommand.MOVE_RIGHT),
new SweepVectorization(this, VectorCommand.MOVE_DOWN, VectorCommand.MOVE_LEFT),
new SweepVectorization(this, VectorCommand.MOVE_UP, VectorCommand.MOVE_RIGHT),
new SweepVectorization(this, VectorCommand.MOVE_UP, VectorCommand.MOVE_LEFT),
new EuclidianDistanceVectorization(this)
);
int byteLength = Integer.MAX_VALUE;
VectorShape vshape = null;
for (Supplier<VectorShape> scan : scans) {
VectorShape candidate = scan.get();
int length = candidate.toBytes().length;
if (vshape == null || byteLength >= length) {
vshape = candidate;
byteLength = length;
}
}
return vshape;
}
/**
* Encode a bitmap shape by going to a corner and sweeping back-and-forth across the image.
* The resulting shape is not optimal, so the {@link VectorShape#optimize()} should be used.
* Note that this class is setup to be dynamic in the chosen corner.
*/
public static class SweepVectorization implements Supplier<VectorShape> {
private VectorCommand[] toOrigin;
private VectorCommand movement;
private VectorCommand next;
private Point point;
private BitmapShape bitmapShape;
private VectorShape vectorShape;
private int width;
private int height;
/**
* Create an instance of the sweep method.
*
* @param bitmapShape is the shape to be converted
* @param initialMovement is the initial sweep movement
* @param next is the direction to advance for each line
*/
public SweepVectorization(BitmapShape bitmapShape, VectorCommand initialMovement, VectorCommand next) {
Objects.requireNonNull(bitmapShape);
Objects.requireNonNull(initialMovement);
Objects.requireNonNull(next);
if (initialMovement.horizontal == next.horizontal || initialMovement.vertical == next.vertical) {
throw new IllegalArgumentException("One vector must be horizontal and the other vector must be vertical");
}
this.toOrigin = new VectorCommand[] { next.opposite(), initialMovement.opposite() };
this.movement = initialMovement;
this.next = next;
this.bitmapShape = bitmapShape;
this.width = bitmapShape.getWidth();
this.height = bitmapShape.getHeight();
this.point = new Point(bitmapShape.origin);
this.vectorShape = new VectorShape();
}
public VectorShape get() {
findStartPosition();
while (!onOrAtEdge(next)) {
scanRow();
plotOrMove(next);
movement = movement.opposite();
point.translate(next.xmove, next.ymove);
}
return vectorShape;
}
public void findStartPosition() {
for (VectorCommand vector : toOrigin) {
while (!onOrAtEdge(vector)) {
vectorShape.vectors.add(vector);
point.translate(vector.xmove, vector.ymove);
}
}
}
public void scanRow() {
while (!onOrAtEdge(movement)) {
plotOrMove(movement);
point.translate(movement.xmove, movement.ymove);
}
}
public void plotOrMove(VectorCommand vector) {
if (bitmapShape.get(point)) {
vectorShape.appendShortCommand(Character.toUpperCase(vector.shortCommand));
} else {
vectorShape.appendShortCommand(Character.toLowerCase(vector.shortCommand));
}
}
public boolean onOrAtEdge(VectorCommand vector) {
// No clever way to do this?
switch (vector) {
case MOVE_DOWN:
case PLOT_DOWN:
return point.y >= height;
case MOVE_UP:
case PLOT_UP:
return point.y < 0;
case MOVE_LEFT:
case PLOT_LEFT:
return point.x < 0;
case MOVE_RIGHT:
case PLOT_RIGHT:
return point.x >= width;
default:
throw new RuntimeException("Unexpected vector: " + vector);
}
}
}
/**
* Encode a bitmap shape by using the Euclidean distance between plotted points to determine
* which vectors take precedence. Most of the math is captured in the Point class, which makes
* this much more straight-forward.
*/
public static class EuclidianDistanceVectorization implements Supplier<VectorShape> {
private List<Point> points;
private BitmapShape bitmapShape;
private VectorShape vshape;
public EuclidianDistanceVectorization(BitmapShape bitmapShape) {
this.bitmapShape = bitmapShape;
this.points = new ArrayList<>();
this.vshape = new VectorShape();
// Collect vector targets
for (int y=0; y<bitmapShape.getHeight(); y++) {
for (int x=0; x<bitmapShape.getWidth(); x++) {
if (bitmapShape.get(x,y)) {
points.add(new Point(x,y));
}
}
}
}
@Override
public VectorShape get() {
Point point = new Point(bitmapShape.origin);
boolean plotFirst = false;
while (!points.isEmpty()) {
Point target = null;
double distance = Double.MAX_VALUE;
for (Point candidate : points) {
double candidateDistance = point.distance(candidate);
if (distance >= candidateDistance) {
distance = candidateDistance;
target = candidate;
}
}
moveTo(plotFirst, point, target);
points.remove(target);
point = target;
plotFirst = true;
}
// Need to plot that last pixel
vshape.plotUp();
return vshape;
}
/**
* Move from origin to target. General strategy is to work from distance between points and bias the one that "loses"
* each time while resetting the one that "won". Hopefully this draws more of a jagged line than something entirely
* straight. (The goal being to prevent a bunch of plot up's in the worst case that doesn't encode nicely.)
* @param plotFirst Indicates if the first vector needs to plot; otherwise all other vectors will be moves
* @param origin Is the point of origin (which can be a plotted pixel)
* @param target Is the target point
*/
public void moveTo(boolean plotFirst, Point origin, Point target) {
if (origin.equals(target)) return;
VectorCommand xvector = origin.x < target.x ? VectorCommand.MOVE_RIGHT : VectorCommand.MOVE_LEFT;
VectorCommand yvector = origin.y < target.y ? VectorCommand.MOVE_DOWN : VectorCommand.MOVE_UP;
if (plotFirst) {
xvector = xvector.plot();
yvector = yvector.plot();
}
Point point = new Point(origin);
int xdist = Math.abs(point.x - target.x);
int ydist = Math.abs(point.y - target.y);
while (!point.equals(target)) {
if (xdist > ydist) {
xdist = Math.abs(point.x - target.x);
ydist += 1;
if (point.x != target.x) {
point.translate(xvector.xmove, xvector.ymove);
vshape.append(xvector);
xvector = xvector.move();
yvector = yvector.move();
}
} else {
xdist += 1;
ydist = Math.abs(point.y - target.y);
if (point.y != target.y) {
point.translate(yvector.xmove, yvector.ymove);
vshape.append(yvector);
xvector = xvector.move();
yvector = yvector.move();
}
}
}
}
}
}

View File

@ -0,0 +1,98 @@
package io.github.applecommander.bastools.api.shapes;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.IntStream;
import io.github.applecommander.bastools.api.utils.Converters;
/**
* Allow the import of an external shape. Processing is very dependent on
* being invoked in the "correct" manner!
* <p>
* Prototype code:
* <pre>
* ; Read in external shape table: configure first and then "import" processes file.
* .external characters
* type=bin
* shapes=1-96
* import=imperator.bin
* </pre>
*/
public class ExternalShapeImporter {
private ShapeTable destination;
private String firstShapeLabel;
private Function<String,ShapeTable> importer = this::importShapeTableFromBinary;
private IntStream intStream = null;
public ExternalShapeImporter(ShapeTable destination, String firstShapeLabel) {
this.destination = destination;
this.firstShapeLabel = firstShapeLabel;
}
public void process(String line) {
Objects.requireNonNull(line);
String[] parts = line.split("=");
if (parts.length != 2) {
throw new RuntimeException(String.format(".external fields require an assignment for '%s'", line));
}
switch (parts[0].toLowerCase()) {
case "type":
switch (parts[1].toLowerCase()) {
case "bin":
importer = this::importShapeTableFromBinary;
break;
case "src":
importer = this::importShapeTableFromSource;
break;
default:
throw new RuntimeException(String.format("Unknown import type specified: '%s'", line));
}
break;
case "shapes":
intStream = Converters.toIntStream(parts[1]);
break;
case "import":
ShapeTable temp = importer.apply(parts[1]);
// Shapes in Applesoft are 1 based but Java List object is 0 based...
intStream.map(n -> n-1).mapToObj(temp.shapes::get).forEach(this::importShape);
break;
default:
throw new RuntimeException(String.format("Unknown assignment '%s' for .external", line));
}
}
public ShapeTable importShapeTableFromBinary(String filename) {
// FIXME May need access to Configuration for these nested files?
try {
Objects.requireNonNull(intStream, ".external requires that 'shapes' is specified");
return ShapeTable.read(Paths.get(filename));
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
public ShapeTable importShapeTableFromSource(String filename) {
// FIXME May need access to Configuration for these nested files?
try {
Objects.requireNonNull(intStream, ".external requires that 'shapes' is specified");
return ShapeGenerator.generate(Paths.get(filename));
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
public void importShape(Shape shape) {
if (firstShapeLabel != null) {
VectorShape vshape = new VectorShape(firstShapeLabel);
vshape.vectors.addAll(shape.toVector().vectors);
destination.shapes.add(vshape);
firstShapeLabel = null;
} else {
destination.shapes.add(shape);
}
}
}

View File

@ -0,0 +1,20 @@
package io.github.applecommander.bastools.api.shapes;
/**
* Represents a single Applesoft shape. Note that the interface is mostly useful to get at the
* bitmap or vector shapes. This also implies that these implementations need to transform between
* eachother!
*
* @see BitmapShape
* @see VectorShape
*/
public interface Shape {
/** Indicates if this shape is empty. */
public boolean isEmpty();
/** Get the label of this shape. */
public String getLabel();
/** Transform to a BitmapShape. */
public BitmapShape toBitmap();
/** Transform to a VectorShape. */
public VectorShape toVector();
}

View File

@ -0,0 +1,58 @@
package io.github.applecommander.bastools.api.shapes;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import io.github.applecommander.bastools.api.shapes.exporters.ImageShapeExporter;
import io.github.applecommander.bastools.api.shapes.exporters.SourceShapeExporter;
import io.github.applecommander.bastools.api.shapes.exporters.TextShapeExporter;
public interface ShapeExporter {
/** Export a single shape to the OutputStream. */
public void export(Shape shape, OutputStream outputStream) throws IOException;
/** Export a single shape to the File. */
public default void export(Shape shape, File file) throws IOException {
Objects.requireNonNull(shape);
Objects.requireNonNull(file);
export(shape, file.toPath());
}
/** Export a single shape to the Path. */
public default void export(Shape shape, Path path) throws IOException {
Objects.requireNonNull(shape);
Objects.requireNonNull(path);
try (OutputStream outputStream = Files.newOutputStream(path)) {
export(shape, outputStream);
}
}
/** Export the entire shape table to the OutputStream. */
public void export(ShapeTable shapeTable, OutputStream outputStream) throws IOException;
/** Export the entire shape table to the File. */
public default void export(ShapeTable shapeTable, File file) throws IOException {
Objects.requireNonNull(shapeTable);
Objects.requireNonNull(file);
export(shapeTable, file.toPath());
}
/** Export the entire shape table to the Path. */
public default void export(ShapeTable shapeTable, Path path) throws IOException {
Objects.requireNonNull(shapeTable);
Objects.requireNonNull(path);
try (OutputStream outputStream = Files.newOutputStream(path)) {
export(shapeTable, outputStream);
}
}
public static TextShapeExporter.Builder text() {
return new TextShapeExporter.Builder();
}
public static ImageShapeExporter.Builder image() {
return new ImageShapeExporter.Builder();
}
public static SourceShapeExporter.Builder source() {
return new SourceShapeExporter.Builder();
}
}

View File

@ -0,0 +1,86 @@
package io.github.applecommander.bastools.api.shapes;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.Reader;
import java.nio.file.Path;
import java.util.Objects;
import java.util.function.Consumer;
public class ShapeGenerator {
public static ShapeTable generate(Reader sourceReader) throws IOException {
Objects.requireNonNull(sourceReader);
ShapeTable st = new ShapeTable();
LineNumberReader reader = new LineNumberReader(sourceReader);
String line = reader.readLine();
Consumer<String> shapeConsumer = null;
while (line != null) {
int comment = line.indexOf(';');
if (comment > -1) line = line.substring(0, comment);
line = line.trim();
String[] parts = line.split("\\s+");
String command = parts[0];
String label = parts.length > 1 ? parts[1] : null;
switch (command.toLowerCase()) {
case ".short":
VectorShape shortShape = new VectorShape(label);
st.shapes.add(shortShape);
shapeConsumer = shortShape::appendShortCommands;
break;
case ".long":
VectorShape longShape = new VectorShape(label);
st.shapes.add(longShape);
shapeConsumer = longShape::appendLongCommands;
break;
case ".bitmap":
BitmapShape bitmapShape = new BitmapShape(label);
st.shapes.add(bitmapShape);
shapeConsumer = bitmapShape::appendBitmapRow;
break;
case ".external":
ExternalShapeImporter importer = new ExternalShapeImporter(st, label);
shapeConsumer = importer::process;
break;
default:
if (line.length() == 0) {
// do nothing
} else if (shapeConsumer != null) {
try {
shapeConsumer.accept(line);
} catch (Throwable t) {
String message = String.format("Error at line #%d - %s", reader.getLineNumber(), t.getMessage());
throw new IOException(message, t);
}
} else {
throw new IOException("Unexpected command: " + line);
}
break;
}
line = reader.readLine();
}
return st;
}
public static ShapeTable generate(InputStream inputStream) throws IOException {
Objects.requireNonNull(inputStream);
try (Reader reader = new InputStreamReader(inputStream)) {
return generate(reader);
}
}
public static ShapeTable generate(File file) throws IOException {
Objects.requireNonNull(file);
try (Reader reader = new FileReader(file)) {
return generate(reader);
}
}
public static ShapeTable generate(Path path) throws IOException {
Objects.requireNonNull(path);
return generate(path.toFile());
}
}

View File

@ -0,0 +1,108 @@
package io.github.applecommander.bastools.api.shapes;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import io.github.applecommander.bastools.api.utils.Streams;
/**
* Represents an Applesoft shape table. Note that this direct class is somewhat useless,
* except for the I/O routines. Access the individual shapes via the {@code #shapes} list.
*/
public class ShapeTable {
/** Read an existing Applesoft shape table binary file. */
public static ShapeTable read(byte[] data) {
Objects.requireNonNull(data);
ShapeTable shapeTable = new ShapeTable();
ByteBuffer buf = ByteBuffer.wrap(data)
.order(ByteOrder.LITTLE_ENDIAN);
int count = Byte.toUnsignedInt(buf.get());
// unused:
buf.get();
for (int i = 0; i < count; i++) {
int offset = buf.getShort();
// load empty shapes as empty...
if (offset == 0) {
shapeTable.shapes.add(new VectorShape());
continue;
}
// defer to VectorShape to process bits
buf.mark();
buf.position(offset);
shapeTable.shapes.add(VectorShape.from(buf));
buf.reset();
}
return shapeTable;
}
public static ShapeTable read(InputStream inputStream) throws IOException {
Objects.requireNonNull(inputStream);
return read(Streams.toByteArray(inputStream));
}
public static ShapeTable read(File file) throws IOException {
Objects.requireNonNull(file);
return read(file.toPath());
}
public static ShapeTable read(Path path) throws IOException {
Objects.requireNonNull(path);
return read(Files.readAllBytes(path));
}
public final List<Shape> shapes = new ArrayList<>();
public int findPositionByLabel(String label) {
for (int i=0; i<shapes.size(); i++) {
if (label.equalsIgnoreCase(shapes.get(i).getLabel())) {
// Applesoft shape tables are 1-based, not 0-based.
return i+1;
}
}
throw new RuntimeException(String.format("Unable to locate shape with label of '%s'", label));
}
public void write(OutputStream outputStream) throws IOException {
Objects.requireNonNull(outputStream);
// Header
outputStream.write(shapes.size());
outputStream.write(0);
// Collect each shape
List<byte[]> data = this.shapes.stream()
.map(Shape::toVector)
.map(VectorShape::toBytes)
.collect(Collectors.toList());
// Build offset table
int offset = 2 + 2*data.size();
for (byte[] d : data) {
outputStream.write(offset & 0xff);
outputStream.write(offset >> 8);
offset += d.length;
}
// Write shape data
for (byte[] d : data) {
outputStream.write(d);
}
}
public void write(File file) throws IOException {
Objects.requireNonNull(file);
try (OutputStream outputStream = new FileOutputStream(file)) {
write(file);
}
}
public void write(Path path) throws IOException {
Objects.requireNonNull(path);
write(path.toFile());
}
}

View File

@ -0,0 +1,55 @@
package io.github.applecommander.bastools.api.shapes;
/**
* Represents all "plot vectors" available in an Applesoft shape table.
*
* @see <a href="https://archive.org/stream/Applesoft_BASIC_Programming_Reference_Manual_Apple_Computer#page/n103/mode/2up">
* Applesoft BASIC Programming Reference Manual</a>
*/
public enum VectorCommand {
// Order here is specific to the encoding within the shape itself
MOVE_UP, MOVE_RIGHT, MOVE_DOWN, MOVE_LEFT,
PLOT_UP, PLOT_RIGHT, PLOT_DOWN, PLOT_LEFT;
public final boolean plot;
public final int xmove;
public final int ymove;
public final char shortCommand;
public final String longCommand;
public final boolean vertical;
public final boolean horizontal;
private VectorCommand() {
this.plot = (this.ordinal() & 0b100) != 0;
// up 0b00
// right 0b01
// down 0b10
// left 0b11
if ((this.ordinal() & 0b001) == 1) {
this.xmove = 2 - (this.ordinal() & 0b011);
this.ymove = 0;
} else {
this.xmove = 0;
this.ymove = (this.ordinal() & 0b011) - 1;
}
this.vertical = xmove == 0;
this.horizontal = ymove == 0;
char shortCommand = "urdl".charAt(this.ordinal() & 0b011);
this.shortCommand = plot ? Character.toUpperCase(shortCommand) : shortCommand;
this.longCommand = this.name().replaceAll("_", "").toLowerCase();
}
public VectorCommand opposite() {
int newDirection = this.ordinal() ^ 0b010;
return VectorCommand.values()[newDirection];
}
public VectorCommand plot() {
return VectorCommand.values()[this.ordinal() | 0b100];
}
public VectorCommand move() {
return VectorCommand.values()[this.ordinal() & 0b011];
}
}

View File

@ -0,0 +1,266 @@
package io.github.applecommander.bastools.api.shapes;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class VectorShape implements Shape {
public static VectorShape from(ByteBuffer buf) {
Objects.requireNonNull(buf);
VectorShape shape = new VectorShape();
VectorCommand[] commands = VectorCommand.values();
while (buf.hasRemaining()) {
int code = Byte.toUnsignedInt(buf.get());
if (code == 0) break;
int vector1 = code & 0b111;
int vector2 = (code >> 3) & 0b111;
int vector3 = (code >> 6) & 0b011; // Cannot plot
shape.vectors.add(commands[vector1]);
if (vector2 != 0 || vector3 != 0) {
shape.vectors.add(commands[vector2]);
if (vector3 != 0) {
shape.vectors.add(commands[vector3]);
}
}
}
return shape;
}
public final String label;
public final List<VectorCommand> vectors = new ArrayList<>();
public VectorShape() {
this.label = null;
}
public VectorShape(String label) {
this.label = label;
}
public VectorShape moveUp() { return append(VectorCommand.MOVE_UP); }
public VectorShape moveRight() { return append(VectorCommand.MOVE_RIGHT); }
public VectorShape moveDown() { return append(VectorCommand.MOVE_DOWN); }
public VectorShape moveLeft() { return append(VectorCommand.MOVE_LEFT); }
public VectorShape plotUp() { return append(VectorCommand.PLOT_UP); }
public VectorShape plotRight() { return append(VectorCommand.PLOT_RIGHT); }
public VectorShape plotDown() { return append(VectorCommand.PLOT_DOWN); }
public VectorShape plotLeft() { return append(VectorCommand.PLOT_LEFT); }
public VectorShape append(VectorCommand vectorCommand) {
this.vectors.add(vectorCommand);
return this;
}
/**
* Optimize the vectors by removing useless vectors or replacing a series with a shorter series.
* At this point, everything is based off of a regex with a potential modification.
*/
public VectorShape optimize() {
String commands = toShortCommands();
Function<String,String> opts =
// Unused moves (left followed by a right with no plotting in between, for instance).
VectorRegexOptimization.of("l([ud]*)r")
.andThen(VectorRegexOptimization.of("r([ud]*)l"))
.andThen(VectorRegexOptimization.of("u([rl]*)d"))
.andThen(VectorRegexOptimization.of("d([rl]*)u"))
// These are plot/move combinations, such as LEFT>up>right that can be replaced by just UP.
.andThen(VectorRegexOptimization.of("L([ud])r", String::toUpperCase))
.andThen(VectorRegexOptimization.of("R([ud])l", String::toUpperCase))
.andThen(VectorRegexOptimization.of("U([rl])d", String::toUpperCase))
.andThen(VectorRegexOptimization.of("D([rl])u", String::toUpperCase))
// Base assumption is that any tail moves can be removed as they don't lead to a plot.
.andThen(VectorRegexOptimization.of("()[udlr]+$"));
String oldCommands = null;
do {
oldCommands = commands;
commands = opts.apply(commands);
} while (!oldCommands.equals(commands));
VectorShape newShape = new VectorShape();
newShape.appendShortCommands(commands);
return newShape;
}
/**
* A vector optimization based on regex. Transformation is optional to (for instance) change a {@code move}
* to a {@code plot} command. Note that the regex requires a matcher group; also be aware that an empty group
* "{@code ()}" is a viable solution.
*/
public static class VectorRegexOptimization implements Function<String,String> {
public static Function<String,String> of(String regex, Function<String,String> transformation) {
VectorRegexOptimization opt = new VectorRegexOptimization();
opt.pattern = Pattern.compile(regex);
opt.fn = transformation;
return opt;
}
public static Function<String,String> of(String regex) {
return of(regex, (s) -> s);
}
private Pattern pattern;
private Function<String,String> fn;
private VectorRegexOptimization() { /* Prevent construction */ }
@Override
public String apply(String shortCommands) {
Matcher matcher = pattern.matcher(shortCommands);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, fn.apply(matcher.group(1)));
}
matcher.appendTail(sb);
return sb.toString();
}
}
public String toShortCommands() {
StringBuilder sb = new StringBuilder();
vectors.stream().map(v -> v.shortCommand).forEach(sb::append);
return sb.toString();
}
public void appendShortCommands(String line) {
for (char cmd : line.trim().toCharArray()) {
appendShortCommand(cmd);
}
}
public void appendShortCommand(char cmd) {
switch (cmd) {
case 'u': moveUp(); break;
case 'd': moveDown(); break;
case 'l': moveLeft(); break;
case 'r': moveRight(); break;
case 'U': plotUp(); break;
case 'D': plotDown(); break;
case 'L': plotLeft(); break;
case 'R': plotRight(); break;
default:
// whitespace is allowed
if (!Character.isWhitespace(cmd)) {
throw new RuntimeException("Unknown command: " + cmd);
}
}
}
public void appendLongCommands(String line) {
Queue<String> tokens = new LinkedList<>(Arrays.asList(line.split("\\s+")));
while (!tokens.isEmpty()) {
String command = tokens.remove();
int count = 1;
String checkNumber = tokens.peek();
if (checkNumber != null && checkNumber.matches("\\d+")) count = Integer.parseInt(tokens.remove());
for (int i=0; i<count; i++) {
switch (command.toLowerCase()) {
case "moveup": moveUp(); break;
case "movedown": moveDown(); break;
case "moveleft": moveLeft(); break;
case "moveright": moveRight(); break;
case "plotup": plotUp(); break;
case "plotdown": plotDown(); break;
case "plotleft": plotLeft(); break;
case "plotright": plotRight(); break;
default:
throw new RuntimeException("Unknown command: " + command);
}
}
}
}
public byte[] toBytes() {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
LinkedList<VectorCommand> work = new LinkedList<>(vectors);
while (!work.isEmpty()) {
VectorCommand vector1 = work.remove();
int section1 = vector1.ordinal();
VectorCommand vector2 = work.poll();
int section2 = Optional.ofNullable(vector2).map(VectorCommand::ordinal).orElse(0);
VectorCommand vector3 = work.poll();
// Remove all invalid encodings 100, 101, 110, 111, and 000.
if (vector3 != null && (vector3.plot || vector3 == VectorCommand.MOVE_UP)) {
work.addFirst(vector3);
vector3 = null;
}
int section3 = Optional.ofNullable(vector3).map(VectorCommand::ordinal).orElse(0);
if (section3 == 0 && section2 == 0 && !work.isEmpty()) {
vector3 = VectorCommand.MOVE_LEFT;
section3 = vector3.ordinal();
// If we have a series of MOVE_UP, we'll end up with a "uul", "rul", "rul", etc.
// It can be compressed a bit by stretching that right out a bit to get "uul", "uur", "uul, "uur", etc.
int moveUpCount = 0;
for (VectorCommand test : work) {
if (test == VectorCommand.MOVE_UP) {
moveUpCount += 1;
} else {
break;
}
}
work.add(Math.min(moveUpCount,2), VectorCommand.MOVE_RIGHT);
}
outputStream.write(section3 << 6 | section2 << 3 | section1);
}
outputStream.write(0);
return outputStream.toByteArray();
}
@Override
public boolean isEmpty() {
return vectors.isEmpty();
}
@Override
public String getLabel() {
return label;
}
@Override
public BitmapShape toBitmap() {
BitmapShape shape = new BitmapShape();
int x = 0;
int y = 0;
for (VectorCommand command : vectors) {
if (command.plot) {
while (y < 0) {
shape.insertRow();
y += 1;
}
while (y >= shape.getHeight()) {
shape.addRow();
}
while (x < 0) {
shape.insertColumn();
x += 1;
}
while (x >= shape.getWidth()) {
shape.addColumn();
}
shape.plot(x,y);
}
x += command.xmove;
y += command.ymove;
}
return shape;
}
@Override
public VectorShape toVector() {
return this;
}
}

View File

@ -0,0 +1,160 @@
package io.github.applecommander.bastools.api.shapes.exporters;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.stream.Collectors;
import javax.imageio.ImageIO;
import io.github.applecommander.bastools.api.shapes.BitmapShape;
import io.github.applecommander.bastools.api.shapes.Shape;
import io.github.applecommander.bastools.api.shapes.ShapeExporter;
import io.github.applecommander.bastools.api.shapes.ShapeTable;
public class ImageShapeExporter implements ShapeExporter {
private int maxWidth = 1024;
private int pixelSize = 4;
private int padding = 2;
private boolean border = true;
private boolean skipEmptyShapes;
private String imageFormat = "PNG";
/** Use the {@code Builder} to create a ImageShapeExporter. */
private ImageShapeExporter() { }
@Override
public void export(Shape shape, OutputStream outputStream) throws IOException {
Objects.requireNonNull(shape);
Objects.requireNonNull(outputStream);
export(Arrays.asList(shape.toBitmap()), outputStream);
}
@Override
public void export(ShapeTable shapeTable, OutputStream outputStream) throws IOException {
Objects.requireNonNull(shapeTable);
Objects.requireNonNull(outputStream);
List<BitmapShape> blist = shapeTable.shapes.stream()
.filter(this::displayThisShape)
.map(Shape::toBitmap)
.collect(Collectors.toList());
export(blist, outputStream);
}
public void export(List<BitmapShape> blist, OutputStream outputStream) throws IOException {
Objects.requireNonNull(blist);
Objects.requireNonNull(outputStream);
int shapeWidth = pixelSize * blist.stream().mapToInt(BitmapShape::getWidth).max().getAsInt();
int shapeHeight = pixelSize * blist.stream().mapToInt(BitmapShape::getHeight).max().getAsInt();
int borderDividerWidth = border ? 1+padding*2 : 0;
int borderEdgeWidth = border ? 1+padding : 0;
int columns = Math.min(blist.size(), Math.max(1, this.maxWidth / shapeWidth));
int rows = (blist.size() + columns - 1) / columns;
int imageWidth = borderEdgeWidth*2 + columns*shapeWidth + (columns-1)*borderDividerWidth;
int imageHeight = borderEdgeWidth*2 + rows*shapeHeight + (rows-1)*borderDividerWidth;
BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
Queue<BitmapShape> bqueue = new LinkedList<>(blist);
Graphics g = image.createGraphics();
if (border) drawBorders(g, shapeWidth, shapeHeight, imageWidth, imageHeight);
Point pt = new Point(borderEdgeWidth, borderEdgeWidth);
while (!bqueue.isEmpty()) {
BitmapShape bshape = bqueue.remove();
drawShapeAt(g, bshape, pt);
pt.x += shapeWidth + borderDividerWidth;
if (pt.x > imageWidth) {
pt.y += shapeHeight + borderDividerWidth;
pt.x = borderEdgeWidth;
}
}
g.dispose();
ImageIO.write(image, imageFormat, outputStream);
}
private boolean displayThisShape(Shape shape) {
return !(skipEmptyShapes && shape.isEmpty());
}
public void drawBorders(Graphics g, int shapeWidth, int shapeHeight, int imageWidth, int imageHeight) {
g.setColor(Color.white);
int paddingWidth = border ? padding : 0;
for (int x=0; x<imageWidth; x+=shapeWidth + paddingWidth*2 + 1) {
for (int y=0; y<imageHeight; y+=shapeHeight + paddingWidth*2 + 1) {
g.drawLine(x, 0, x, imageHeight);
g.drawLine(0, y, imageWidth, y);
}
}
}
public void drawShapeAt(Graphics g, BitmapShape shape, Point origin) {
for (int x=0; x<shape.getWidth(); x++) {
for (int y=0; y<shape.getHeight(); y++) {
g.setColor(shape.get(x, y) ? Color.white : Color.lightGray);
g.fillRect(origin.x + (x*pixelSize), origin.y + (y*pixelSize),
pixelSize, pixelSize);
}
}
}
public static class Builder {
private ImageShapeExporter shapeExporter = new ImageShapeExporter();
public Builder maxWidth(int maxWidth) {
shapeExporter.maxWidth = maxWidth;
return this;
}
public Builder pixelSize(int pixelSize) {
shapeExporter.pixelSize = pixelSize;
return this;
}
public Builder border(boolean border) {
shapeExporter.border = border;
return this;
}
public Builder jpeg() {
return imageFormat("JPEG");
}
public Builder png() {
return imageFormat("PNG");
}
public Builder bmp() {
return imageFormat("BMP");
}
public Builder wbmp() {
return imageFormat("WBMP");
}
public Builder gif() {
return imageFormat("GIF");
}
public Builder imageFormat(String imageFormat) {
shapeExporter.imageFormat = imageFormat;
return this;
}
public Builder skipEmptyShapes() {
return skipEmptyShapes(true);
}
public Builder skipEmptyShapes(boolean skipEmptyShapes) {
shapeExporter.skipEmptyShapes = skipEmptyShapes;
return this;
}
public ShapeExporter build() {
return shapeExporter;
}
}
}

View File

@ -0,0 +1,153 @@
package io.github.applecommander.bastools.api.shapes.exporters;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Queue;
import java.util.function.BiConsumer;
import io.github.applecommander.bastools.api.shapes.Shape;
import io.github.applecommander.bastools.api.shapes.ShapeExporter;
import io.github.applecommander.bastools.api.shapes.ShapeTable;
import io.github.applecommander.bastools.api.shapes.VectorCommand;
import io.github.applecommander.bastools.api.shapes.VectorShape;
public class SourceShapeExporter implements ShapeExporter {
private BiConsumer<Shape,PrintWriter> formatFunction = this::exportShapeAsBitmap;
private ShapeExporter textExporter;
private boolean skipEmptyShapes;
private boolean optimize;
/** Use the {@code Builder} to create a TextShapeExporter. */
private SourceShapeExporter() {
this.textExporter = ShapeExporter.text().noBorder().build();
}
@Override
public void export(Shape shape, OutputStream outputStream) throws IOException {
PrintWriter pw = new PrintWriter(outputStream);
formatFunction.accept(shape, pw);
pw.flush();
}
@Override
public void export(ShapeTable shapeTable, OutputStream outputStream) throws IOException {
PrintWriter pw = new PrintWriter(outputStream);
shapeTable.shapes.stream()
.filter(this::displayThisShape)
.forEach(shape -> formatFunction.accept(shape, pw));
pw.flush();
}
private boolean displayThisShape(Shape shape) {
return !(skipEmptyShapes && shape.isEmpty());
}
public void exportShapeAsBitmap(Shape shape, PrintWriter pw) {
try {
pw.printf(".bitmap\n");
ByteArrayOutputStream os = new ByteArrayOutputStream();
textExporter.export(shape, new PaddedOutputStream(os, " "));
pw.print(new String(os.toByteArray()));
pw.printf("\n");
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
public void exportShapeAsShortCommands(Shape shape, PrintWriter pw) {
VectorShape vshape = shape.toVector();
if (optimize) vshape = vshape.optimize();
if (displayThisShape(vshape)) {
pw.printf(".short\n");
pw.printf(" %s\n", vshape.toShortCommands());
pw.printf("\n");
}
}
public void exportShapeAsLongCommands(Shape shape, PrintWriter pw) {
VectorShape vshape = shape.toVector();
if (optimize) vshape = vshape.optimize();
if (displayThisShape(vshape)) {
pw.printf(".long\n");
Queue<VectorCommand> vectors = new LinkedList<>(vshape.vectors);
while (!vectors.isEmpty()) {
VectorCommand vector = vectors.remove();
int count = 1;
while (vectors.peek() == vector) {
vectors.remove();
count += 1;
}
if (count == 1) {
pw.printf(" %s\n", vector.longCommand);
} else {
pw.printf(" %s %d\n", vector.longCommand, count);
}
}
pw.printf("\n");
}
}
public static class Builder {
private SourceShapeExporter exporter = new SourceShapeExporter();
public Builder bitmap() {
exporter.formatFunction = exporter::exportShapeAsBitmap;
return this;
}
public Builder shortCommands() {
exporter.formatFunction = exporter::exportShapeAsShortCommands;
return this;
}
public Builder longCommands() {
exporter.formatFunction = exporter::exportShapeAsLongCommands;
return this;
}
public Builder skipEmptyShapes() {
return skipEmptyShapes(true);
}
public Builder skipEmptyShapes(boolean skipEmptyShapes) {
exporter.skipEmptyShapes = skipEmptyShapes;
return this;
}
public Builder optimize() {
return optimize(true);
}
public Builder optimize(boolean optimize) {
exporter.optimize = optimize;
return this;
}
public ShapeExporter build() {
return exporter;
}
}
public static class PaddedOutputStream extends OutputStream {
private OutputStream wrappedStream;
private boolean needPadding = true;
private byte[] padding;
public PaddedOutputStream(OutputStream outputStream, String padding) {
Objects.requireNonNull(outputStream);
Objects.requireNonNull(padding);
this.wrappedStream = outputStream;
this.padding = padding.getBytes();
}
@Override
public void write(int b) throws IOException {
if (needPadding) {
wrappedStream.write(padding);
}
needPadding = (b == '\n');
wrappedStream.write(b);
}
}
}

View File

@ -0,0 +1,263 @@
package io.github.applecommander.bastools.api.shapes.exporters;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import io.github.applecommander.bastools.api.shapes.BitmapShape;
import io.github.applecommander.bastools.api.shapes.Shape;
import io.github.applecommander.bastools.api.shapes.ShapeExporter;
import io.github.applecommander.bastools.api.shapes.ShapeTable;
public class TextShapeExporter implements ShapeExporter {
private int maxWidth = 80;
private BorderStrategy borderStrategy = BorderStrategy.BOX_DRAWING;
private boolean skipEmptyShapes;
/** Use the {@code Builder} to create a TextShapeExporter. */
private TextShapeExporter() { }
@Override
public void export(Shape shape, OutputStream outputStream) {
Objects.requireNonNull(shape);
Objects.requireNonNull(outputStream);
BitmapShape b = shape.toBitmap();
PrintWriter pw = new PrintWriter(outputStream);
drawTopLine(pw, 1, b.getWidth());
Queue<BitmapShape> bqueue = new LinkedList<>(Arrays.asList(b));
drawRow(pw, bqueue, 1, b.getHeight(), b.getWidth());
drawBottomLine(pw, 1, b.getWidth());
pw.flush();
}
@Override
public void export(ShapeTable shapeTable, OutputStream outputStream) {
Objects.requireNonNull(shapeTable);
Objects.requireNonNull(outputStream);
List<BitmapShape> blist = shapeTable.shapes.stream()
.filter(this::displayThisShape)
.map(Shape::toBitmap)
.collect(Collectors.toList());
int width = blist.stream().mapToInt(BitmapShape::getWidth).max().getAsInt();
int height = blist.stream().mapToInt(BitmapShape::getHeight).max().getAsInt();
int columns = Math.min(Math.max(1, this.maxWidth / width), blist.size());
PrintWriter pw = new PrintWriter(outputStream);
drawTopLine(pw, columns, width);
Queue<BitmapShape> bqueue = new LinkedList<>(blist);
drawRow(pw, bqueue, columns, height, width);
while (!bqueue.isEmpty()) {
drawDividerLine(pw, columns, width);
drawRow(pw, bqueue, columns, height, width);
}
drawBottomLine(pw, columns, width);
pw.flush();
}
private boolean displayThisShape(Shape shape) {
return !(skipEmptyShapes && shape.isEmpty());
}
private void drawTopLine(PrintWriter pw, int columns, int width) {
borderStrategy.topLeftCorner(pw);
borderStrategy.horizontalLine(pw, width);
for (int i=1; i<columns; i++) {
borderStrategy.topDivider(pw);
borderStrategy.horizontalLine(pw, width);
}
borderStrategy.topRightCorner(pw);
}
private void drawDividerLine(PrintWriter pw, int columns, int width) {
borderStrategy.dividerLeftEdge(pw);
borderStrategy.dividerHorizontalLine(pw, width);
for (int i=1; i<columns; i++) {
borderStrategy.dividerMiddle(pw);
borderStrategy.dividerHorizontalLine(pw, width);
}
borderStrategy.dividerRightEdge(pw);
}
private void drawBottomLine(PrintWriter pw, int columns, int width) {
borderStrategy.bottomLeftCorner(pw);
borderStrategy.horizontalLine(pw, width);
for (int i=1; i<columns; i++) {
borderStrategy.bottomDivider(pw);
borderStrategy.horizontalLine(pw, width);
}
borderStrategy.bottomRightCorner(pw);
}
private void drawRow(PrintWriter pw, Queue<BitmapShape> bqueue, int columns, int height, int width) {
BitmapShape[] bshapes = new BitmapShape[columns];
for (int i=0; i<bshapes.length; i++) {
bshapes[i] = bqueue.isEmpty() ? new BitmapShape() : bqueue.remove();
}
for (int y=0; y<height; y++) {
borderStrategy.verticalLine(pw);
drawRowLine(pw, bshapes[0], width, y);
for (int c=1; c<bshapes.length; c++) {
borderStrategy.dividerVerticalLine(pw);
drawRowLine(pw, bshapes[c], width, y);
}
borderStrategy.verticalLine(pw);
pw.println();
}
}
private void drawRowLine(PrintWriter pw, BitmapShape bshape, int width, int y) {
List<Boolean> row = bshape.grid.size() > y ? bshape.grid.get(y) : new ArrayList<>();
for (int x=0; x<width; x++) {
if (row.size() > x) {
Boolean plot = row.get(x);
if (bshape.origin.x == x && bshape.origin.y == y) {
pw.printf("%c", plot ? '*' : '+');
} else {
pw.printf("%c", plot ? 'X' : '.');
}
} else {
pw.print(" ");
}
}
}
public enum BorderStrategy {
/** No border but with spaces between shapes. Note the tricky newline in {@code dividerLeftEdge}. */
NONE('\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', ' ', '\0', '\n', '\0', '\0'),
/**
* A border comprised of the box drawing characters.
* @see <a href='https://en.wikipedia.org/wiki/Box-drawing_character'>Wikipedia article on box characters</a>
*/
BOX_DRAWING('\u2500', '\u2502', '\u250C', '\u2510', '\u2514', '\u2518', '\u252C', '\u2534',
'\u2502', '\u2500', '\u251C', '\u2524', '\u253C'),
/** A simple border based on plain ASCII characters. */
ASCII_TEXT('-', '|', '+', '+', '+', '+', '+', '+', '|', '-', '+', '+', '+');
private final char horizontalLine;
private final char verticalLine;
private final char topLeftCorner;
private final char topRightCorner;
private final char bottomLeftCorner;
private final char bottomRightCorner;
private final char topDivider;
private final char bottomDivider;
private final char dividerVerticalLine;
private final char dividerHorizontalLine;
private final char dividerLeftEdge;
private final char dividerRightEdge;
private final char dividerMiddle;
private BorderStrategy(char horizontalLine, char verticalLine, char topLeftCorner, char topRightCorner,
char bottomLeftCorner, char bottomRightCorner, char topDivider, char bottomDivider,
char dividerVerticalLine, char dividerHorizontalLine, char dividerLeftEdge, char dividerRightEdge,
char dividerMiddle) {
this.horizontalLine = horizontalLine;
this.verticalLine = verticalLine;
this.topLeftCorner = topLeftCorner;
this.topRightCorner = topRightCorner;
this.bottomLeftCorner = bottomLeftCorner;
this.bottomRightCorner = bottomRightCorner;
this.topDivider = topDivider;
this.bottomDivider = bottomDivider;
this.dividerVerticalLine = dividerVerticalLine;
this.dividerHorizontalLine = dividerHorizontalLine;
this.dividerLeftEdge = dividerLeftEdge;
this.dividerRightEdge = dividerRightEdge;
this.dividerMiddle = dividerMiddle;
}
private void print(Consumer<String> output, char ch) {
print(output, ch, 1);
}
private void print(Consumer<String> output, char ch, int width) {
if (ch != '\0') {
output.accept(new String(new char[width]).replace('\0', ch));
}
}
public void horizontalLine(PrintWriter pw, int width) {
print(pw::print, horizontalLine, width);
}
public void verticalLine(PrintWriter pw) {
print(pw::print, verticalLine);
}
public void topLeftCorner(PrintWriter pw) {
print(pw::print, topLeftCorner);
}
public void topRightCorner(PrintWriter pw) {
print(pw::println, topRightCorner);
}
public void bottomLeftCorner(PrintWriter pw) {
print(pw::print, bottomLeftCorner);
}
public void bottomRightCorner(PrintWriter pw) {
print(pw::println, bottomRightCorner);
}
public void topDivider(PrintWriter pw) {
print(pw::print, topDivider);
}
public void bottomDivider(PrintWriter pw) {
print(pw::print, bottomDivider);
}
public void dividerVerticalLine(PrintWriter pw) {
print(pw::print, dividerVerticalLine);
}
public void dividerHorizontalLine(PrintWriter pw, int width) {
print(pw::print, dividerHorizontalLine, width);
}
public void dividerLeftEdge(PrintWriter pw) {
print(pw::print, dividerLeftEdge);
}
public void dividerRightEdge(PrintWriter pw) {
print(pw::println, dividerRightEdge);
}
public void dividerMiddle(PrintWriter pw) {
print(pw::print, dividerMiddle);
}
}
public static class Builder {
private TextShapeExporter textShapeExporter = new TextShapeExporter();
public Builder maxWidth(int maxWidth) {
textShapeExporter.maxWidth = maxWidth;
return this;
}
public Builder noBorder() {
return borderStrategy(BorderStrategy.NONE);
}
public Builder asciiTextBorder() {
return borderStrategy(BorderStrategy.ASCII_TEXT);
}
public Builder boxDrawingBorder() {
return borderStrategy(BorderStrategy.BOX_DRAWING);
}
public Builder borderStrategy(BorderStrategy borderStrategy) {
textShapeExporter.borderStrategy = borderStrategy;
return this;
}
public Builder skipEmptyShapes() {
return skipEmptyShapes(true);
}
public Builder skipEmptyShapes(boolean skipEmptyShapes) {
textShapeExporter.skipEmptyShapes = skipEmptyShapes;
return this;
}
public ShapeExporter build() {
return textShapeExporter;
}
}
}

View File

@ -0,0 +1,60 @@
package io.github.applecommander.bastools.api.utils;
import java.util.Arrays;
import java.util.stream.IntStream;
public class Converters {
private Converters() { /* Prevent construction */ }
/**
* Convert a string to an integer allowing multiple formats.
* Normal decimal, or hexadecimal with a <code>$</code> or <code>0x</code> prefix.
*/
public static Integer toInteger(String value) {
if (value == null) {
return null;
} else if (value.startsWith("$")) {
return Integer.valueOf(value.substring(1), 16);
} else if (value.startsWith("0x") || value.startsWith("0X")) {
return Integer.valueOf(value.substring(2), 16);
} else {
return Integer.valueOf(value);
}
}
/**
* Convert a string to a boolean value allowing for "true" or "yes" to evaluate to Boolean.TRUE.
*/
public static Boolean toBoolean(String value) {
if (value == null) {
return null;
}
return "true".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value);
}
/**
* Supports entry of values in ranges or comma-separated lists and combinations thereof.
* <ul>
* <li>Range: <code>m-n</code> where m<n.</li>
* <li>Distinct values: <code>a,b,c,d</code>.</li>
* <li>Single value: <code>x</code></li>
* <li>Combination: <code>m-n;a,b,c,d;x</code>.</li>
* </ul>
*/
public static IntStream toIntStream(String values) {
IntStream stream = IntStream.empty();
for (String range : values.split(";")) {
if (range.contains("-")) {
String[] parts = range.split("-");
int low = Integer.parseInt(parts[0]);
int high = Integer.parseInt(parts[1]);
stream = IntStream.concat(stream, IntStream.rangeClosed(low, high));
} else {
stream = IntStream.concat(stream,
Arrays.asList(range.split(",")).stream().mapToInt(Integer::parseInt));
}
}
return stream;
}
}

View File

@ -0,0 +1,23 @@
package io.github.applecommander.bastools.api.utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class Streams {
private Streams() { /* Prevent construction */ }
/** Utility method to read all bytes from an InputStream. */
public static byte[] toByteArray(InputStream inputStream) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
while (true) {
byte[] buf = new byte[1024];
int len = inputStream.read(buf);
if (len == -1) break;
outputStream.write(buf, 0, len);
}
outputStream.flush();
return outputStream.toByteArray();
}
}

View File

@ -0,0 +1,28 @@
package io.github.applecommander.bastools.api.utils;
import java.util.Optional;
import java.util.function.Supplier;
/** Generate all Applesoft BASIC FP variable names. */
public class VariableNameGenerator implements Supplier<Optional<String>> {
public static final String CHAR1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static final String CHAR2 = " " + CHAR1 + "0123456789";
public static final int LENGTH = CHAR1.length() * CHAR2.length();
private int n = 0;
@Override
public Optional<String> get() {
try {
if (n >= 0 && n < LENGTH) {
return Optional.of(String.format("%s%s",
CHAR1.charAt(n % CHAR1.length()),
CHAR2.charAt(n / CHAR1.length())
).trim());
}
return Optional.empty();
} finally {
n += 1;
}
}
}

View File

@ -1,4 +1,4 @@
package io.github.applecommander.bastokenizer.api.visitors;
package io.github.applecommander.bastools.api.visitors;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -7,15 +7,15 @@ import java.util.Optional;
import java.util.Stack;
import java.util.TreeMap;
import io.github.applecommander.bastokenizer.api.Configuration;
import io.github.applecommander.bastokenizer.api.Directive;
import io.github.applecommander.bastokenizer.api.Directives;
import io.github.applecommander.bastokenizer.api.Visitor;
import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword;
import io.github.applecommander.bastokenizer.api.model.Line;
import io.github.applecommander.bastokenizer.api.model.Program;
import io.github.applecommander.bastokenizer.api.model.Statement;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastools.api.Configuration;
import io.github.applecommander.bastools.api.Directive;
import io.github.applecommander.bastools.api.Directives;
import io.github.applecommander.bastools.api.Visitor;
import io.github.applecommander.bastools.api.model.ApplesoftKeyword;
import io.github.applecommander.bastools.api.model.Line;
import io.github.applecommander.bastools.api.model.Program;
import io.github.applecommander.bastools.api.model.Statement;
import io.github.applecommander.bastools.api.model.Token;
public class ByteVisitor implements Visitor {
private Stack<ByteArrayOutputStream> stack;
@ -63,6 +63,7 @@ public class ByteVisitor implements Visitor {
ByteArrayOutputStream os = stack.peek();
os.write(0x00);
os.write(0x00);
this.address += 2;
return program;
}
@ -82,6 +83,8 @@ public class ByteVisitor implements Visitor {
statement.accept(this);
}
if (currentDirective != null) {
// Need to force the last set of parameters to be processed. Yeah, stinky. :-)
currentDirective.append(Token.eol(-1));
currentDirective.writeBytes(this.address+4, line);
currentDirective = null;
}

View File

@ -1,13 +1,13 @@
package io.github.applecommander.bastokenizer.api.visitors;
package io.github.applecommander.bastools.api.visitors;
import java.util.Set;
import java.util.TreeSet;
import io.github.applecommander.bastokenizer.api.Visitor;
import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword;
import io.github.applecommander.bastokenizer.api.model.Statement;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastokenizer.api.model.Token.Type;
import io.github.applecommander.bastools.api.Visitor;
import io.github.applecommander.bastools.api.model.ApplesoftKeyword;
import io.github.applecommander.bastools.api.model.Statement;
import io.github.applecommander.bastools.api.model.Token;
import io.github.applecommander.bastools.api.model.Token.Type;
public class LineNumberTargetCollector implements Visitor {
private Set<Integer> targets = new TreeSet<>();

View File

@ -1,12 +1,12 @@
package io.github.applecommander.bastokenizer.api.visitors;
package io.github.applecommander.bastools.api.visitors;
import java.io.PrintStream;
import io.github.applecommander.bastokenizer.api.Visitor;
import io.github.applecommander.bastokenizer.api.Visitors.PrintBuilder;
import io.github.applecommander.bastokenizer.api.model.Line;
import io.github.applecommander.bastokenizer.api.model.Statement;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastools.api.Visitor;
import io.github.applecommander.bastools.api.Visitors.PrintBuilder;
import io.github.applecommander.bastools.api.model.Line;
import io.github.applecommander.bastools.api.model.Statement;
import io.github.applecommander.bastools.api.model.Token;
public class PrettyPrintVisitor implements Visitor {
private PrintStream printStream;

View File

@ -1,12 +1,12 @@
package io.github.applecommander.bastokenizer.api.visitors;
package io.github.applecommander.bastools.api.visitors;
import java.io.PrintStream;
import io.github.applecommander.bastokenizer.api.Visitor;
import io.github.applecommander.bastokenizer.api.Visitors.PrintBuilder;
import io.github.applecommander.bastokenizer.api.model.Line;
import io.github.applecommander.bastokenizer.api.model.Statement;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastools.api.Visitor;
import io.github.applecommander.bastools.api.Visitors.PrintBuilder;
import io.github.applecommander.bastools.api.model.Line;
import io.github.applecommander.bastools.api.model.Statement;
import io.github.applecommander.bastools.api.model.Token;
public class PrintVisitor implements Visitor {
private PrintStream printStream;

View File

@ -1,14 +1,14 @@
package io.github.applecommander.bastokenizer.api.visitors;
package io.github.applecommander.bastools.api.visitors;
import java.util.Map;
import io.github.applecommander.bastokenizer.api.Visitor;
import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword;
import io.github.applecommander.bastokenizer.api.model.Line;
import io.github.applecommander.bastokenizer.api.model.Program;
import io.github.applecommander.bastokenizer.api.model.Statement;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastokenizer.api.model.Token.Type;
import io.github.applecommander.bastools.api.Visitor;
import io.github.applecommander.bastools.api.model.ApplesoftKeyword;
import io.github.applecommander.bastools.api.model.Line;
import io.github.applecommander.bastools.api.model.Program;
import io.github.applecommander.bastools.api.model.Statement;
import io.github.applecommander.bastools.api.model.Token;
import io.github.applecommander.bastools.api.model.Token.Type;
/** This is a mildly rewritable Visitor. */
public class ReassignmentVisitor implements Visitor {
@ -44,6 +44,7 @@ public class ReassignmentVisitor implements Visitor {
* - GOSUB n
* - GOTO n
* - IF ... THEN n
* - IF ... THEN [GOTO|GOSUB] n
* - LIST n [ ,m ]
* - ON x GOTO n, m, ...
* - ON x GOSUB n, m, ...
@ -52,21 +53,21 @@ public class ReassignmentVisitor implements Visitor {
*/
@Override
public Statement visit(Statement statement) {
boolean next = false;
boolean multiple = false;
boolean trigger = false;
boolean then = false; // Special case: Immediately after THEN, a number triggers reassignment.
Statement newStatement = new Statement();
for (Token t : statement.tokens) {
Token newToken = t;
if (next) {
if (trigger || then) {
if (t.type == Type.NUMBER && reassignments.containsKey(t.number.intValue())) {
newToken = Token.number(t.line, reassignments.get(t.number.intValue()).doubleValue());
}
next = multiple; // preserve next based on if we have multiple line numbers or not.
} else {
next = t.keyword == ApplesoftKeyword.GOSUB || t.keyword == ApplesoftKeyword.GOTO
|| t.keyword == ApplesoftKeyword.THEN || t.keyword == ApplesoftKeyword.RUN
|| t.keyword == ApplesoftKeyword.LIST;
multiple |= t.keyword == ApplesoftKeyword.LIST || t.keyword == ApplesoftKeyword.ON;
then = false;
}
if (!trigger) {
trigger = t.keyword == ApplesoftKeyword.GOSUB || t.keyword == ApplesoftKeyword.GOTO
|| t.keyword == ApplesoftKeyword.LIST || t.keyword == ApplesoftKeyword.RUN;
then = t.keyword == ApplesoftKeyword.THEN;
}
newStatement.tokens.add(newToken);
}

View File

@ -0,0 +1,24 @@
package io.github.applecommander.bastools.api.visitors;
import java.util.HashSet;
import java.util.Set;
import io.github.applecommander.bastools.api.Visitor;
import io.github.applecommander.bastools.api.model.Token;
import io.github.applecommander.bastools.api.model.Token.Type;
public class VariableCollectorVisitor implements Visitor {
private Set<String> variableNames = new HashSet<>();
public Set<String> getVariableNames() {
return this.variableNames;
}
@Override
public Token visit(Token token) {
if (token.type == Type.IDENT) {
variableNames.add(token.text);
}
return Visitor.super.visit(token);
}
}

View File

@ -1,4 +1,4 @@
package io.github.applecommander.bastokenizer.api.visitors;
package io.github.applecommander.bastools.api.visitors;
import java.util.Arrays;
import java.util.HashMap;
@ -6,11 +6,11 @@ import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import io.github.applecommander.bastokenizer.api.Visitor;
import io.github.applecommander.bastokenizer.api.model.Line;
import io.github.applecommander.bastokenizer.api.model.Program;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastokenizer.api.model.Token.Type;
import io.github.applecommander.bastools.api.Visitor;
import io.github.applecommander.bastools.api.model.Line;
import io.github.applecommander.bastools.api.model.Program;
import io.github.applecommander.bastools.api.model.Token;
import io.github.applecommander.bastools.api.model.Token.Type;
public class VariableReportVisitor implements Visitor {
private Map<String,SortedSet<Integer>> refs = new HashMap<>();

View File

@ -0,0 +1,25 @@
package io.github.applecommander.bastools.api;
import java.util.Queue;
import org.junit.Assert;
import org.junit.Test;
import io.github.applecommander.bastools.api.model.Program;
import io.github.applecommander.bastools.api.model.Token;
import io.github.applecommander.bastools.api.utils.TokenBuilder;
public class ParserTest {
@Test
public void testBlankLines() {
Queue<Token> tokens = TokenBuilder.builder()
.eol() // Blank line before
.number(10.0).ident("A").syntax('=').number(42.0).eol()
.eol() // Blank line after
.tokens();
Parser parser = new Parser(tokens);
Program program = parser.parse();
Assert.assertNotNull(program);
}
}

View File

@ -0,0 +1,43 @@
package io.github.applecommander.bastools.api.code;
import static org.junit.Assert.assertArrayEquals;
import java.io.IOException;
import org.junit.Test;
import io.github.applecommander.bastools.api.model.ApplesoftKeyword;
public class CodeBuilderTest {
@Test
public void testBasicRETURN() throws IOException {
final byte[] expected = { (byte)ApplesoftKeyword.RETURN.code, 0x00 };
CodeBuilder builder = new CodeBuilder();
builder.basic().RETURN().endLine();
assertArrayEquals(expected, builder.generate(0x0000).toByteArray());
}
@Test
public void testAsmWithMark() throws IOException {
final byte[] data = { 0x01, 0x02, 0x03 };
final byte[] expected = {
(byte)0xa9, 0x09, // 0x801: LDA #$09
(byte)0x85, (byte)0xad, // 0x803: STA $AD
(byte)0xa9, 0x08, // 0x805: LDA #$08
(byte)0x85, (byte)0xae, // 0x807: STA $AE
0x01, 0x02, 0x03 // 0x809: 01 02 03 ("data")
};
CodeBuilder builder = new CodeBuilder();
CodeMark mark = new CodeMark();
builder.asm()
.setAddress(mark, 0xad)
.end()
.set(mark)
.addBinary(data);
assertArrayEquals(expected, builder.generate(0x801).toByteArray());
}
}

View File

@ -0,0 +1,138 @@
package io.github.applecommander.bastools.api.shapes;
import static org.junit.Assert.*;
import static org.junit.Assert.assertNotNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.junit.Test;
public class ShapeGeneratorTest {
@Test
public void generateBoxShortformTest() throws IOException {
ShapeTable st = ShapeGenerator.generate(getClass().getResourceAsStream("/box-shortform.st"));
assertShapeIsBox(st);
assertShapeBoxVectors(st, "label-short");
}
@Test
public void generateBoxLongformTest() throws IOException {
ShapeTable st = ShapeGenerator.generate(getClass().getResourceAsStream("/box-longform.st"));
assertShapeIsBox(st);
assertShapeBoxVectors(st, "label-long");
}
@Test
public void generateBoxBitmapTest() throws IOException {
ShapeTable st = ShapeGenerator.generate(getClass().getResourceAsStream("/box-bitmap.st"));
assertShapeIsBox(st);
// Unable to test vectors for bitmaps
assertEquals("label-bitmap", st.shapes.get(0).toBitmap().label);
}
public void assertShapeIsBox(ShapeTable st) throws IOException {
assertNotNull(st);
assertEquals(1, st.shapes.size());
final String expected = "+-----+\n"
+ "|.XXX.|\n"
+ "|X...X|\n"
+ "|X.+.X|\n"
+ "|X...X|\n"
+ "|.XXX.|\n"
+ "+-----+\n";
assertShapeMatches(expected, st.shapes.get(0));
}
public void assertShapeBoxVectors(ShapeTable st, String label) {
assertNotNull(st);
assertEquals(1, st.shapes.size());
VectorShape expected = new VectorShape(label)
.moveDown().moveDown()
.plotLeft().plotLeft()
.moveUp().plotUp().plotUp().plotUp()
.moveRight().plotRight().plotRight().plotRight()
.moveDown().plotDown().plotDown().plotDown()
.moveLeft().plotLeft();
Shape shape = st.shapes.get(0);
assertNotNull(shape);
assertTrue(shape instanceof VectorShape);
VectorShape vshape = shape.toVector();
assertEquals(expected.vectors, vshape.vectors);
assertEquals(expected.label, vshape.label);
}
@Test
public void testMouseShape() throws IOException {
final String mouse = "+--------------+\n"
+ "|..........*X..|\n"
+ "|....XXXX.XX...|\n"
+ "|...XXXXXXXX...|\n"
+ "|.XXXXXXXXXXX..|\n"
+ "|XX.XXXXXXX.XX.|\n"
+ "|X...XXXXXXXXXX|\n"
+ "|XX............|\n"
+ "|.XXX.XX.......|\n"
+ "|...XXX........|\n"
+ "+--------------+\n";
ShapeTable st = ShapeGenerator.generate(getClass().getResourceAsStream("/mouse-bitmap.st"));
assertNotNull(st);
assertEquals(1, st.shapes.size());
// Verify we read the shape correctly...
Shape shape = st.shapes.get(0);
assertNotNull(shape);
assertShapeMatches(mouse, shape);
// Run vector transform to be certain we're ok
Shape vectorShape = shape.toVector();
assertNotNull(vectorShape);
assertShapeMatches(mouse, vectorShape);
}
@Test
public void testRobotShape() throws IOException {
final String robot = "+-------------+\n"
+ "|....XXXXX...+|\n"
+ "|XXXXX...XX...|\n"
+ "|....XXXXX....|\n"
+ "|.............|\n"
+ "|..XX..XXX....|\n"
+ "|...XX.XXX....|\n"
+ "|...XX.XXXX...|\n"
+ "|..XX.XXXXX...|\n"
+ "|....XXXXXX...|\n"
+ "|.XXXXXXXXXXX.|\n"
+ "|XX.........XX|\n"
+ "|XX.........XX|\n"
+ "|.XXXXXXXXXXX.|\n"
+ "+-------------+\n";
ShapeTable st = ShapeGenerator.generate(getClass().getResourceAsStream("/robot-bitmap.st"));
assertNotNull(st);
assertEquals(1, st.shapes.size());
// Verify we read the shape correctly...
Shape shape = st.shapes.get(0);
assertNotNull(shape);
assertShapeMatches(robot, shape);
// Run vector transform to be certain we're ok
Shape vectorShape = shape.toVector();
assertNotNull(vectorShape);
assertShapeMatches(robot, vectorShape);
}
public void assertShapeMatches(final String expected, Shape shape) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ShapeExporter exp = ShapeExporter.text().asciiTextBorder().build();
exp.export(shape, outputStream);
String actual = new String(outputStream.toByteArray());
assertEquals(expected, actual);
}
}

View File

@ -0,0 +1,59 @@
package io.github.applecommander.bastools.api.shapes;
import static org.junit.Assert.*;
import static org.junit.Assert.assertNotNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class ShapeWriteAndReadTests {
@Parameters(name = "{index}: file= {0}")
public static Collection<String> data() {
return Arrays.asList("/mouse-bitmap.st", "/robot-bitmap.st");
}
@Parameter
public String filename;
private ShapeExporter textExporter;
@Before
public void setup() {
textExporter = ShapeExporter.text().asciiTextBorder().skipEmptyShapes(true).build();
}
@Test
public void test() throws IOException {
ShapeTable stBefore = ShapeGenerator.generate(getClass().getResourceAsStream(filename));
assertNotNull(stBefore);
assertFalse(stBefore.shapes.isEmpty());
final String expected = format(stBefore);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
stBefore.write(outputStream);
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
ShapeTable stAfter = ShapeTable.read(inputStream);
String actual = format(stAfter);
assertEquals(expected, actual);
}
public String format(ShapeTable st) throws IOException {
ByteArrayOutputStream text = new ByteArrayOutputStream();
textExporter.export(st, text);
return new String(text.toByteArray());
}
}

View File

@ -0,0 +1,185 @@
package io.github.applecommander.bastools.api.shapes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.junit.Test;
public class ShapesTest {
/**
* This shape data is taken from the Applesoft BASIC Programmer's Reference Manual (1987), p146.
*/
public static final byte[] BOX_SAMPLE = { 0x01, 0x00, 0x04, 0x00, 0x12, 0x3F, 0x20, 0x64, 0x2d, 0x15, 0x36, 0x1e, 0x07, 0x00 };
/**
* These shape vectors are taken from the Applesoft BASIC Programmer's Reference Manual (1987), p146.
*/
public VectorShape drawStandardBoxShape() {
return new VectorShape()
.moveDown().moveDown()
.plotLeft().plotLeft()
.moveUp().plotUp().plotUp().plotUp()
.moveRight().plotRight().plotRight().plotRight()
.moveDown().plotDown().plotDown().plotDown()
.moveLeft().plotLeft();
}
public BitmapShape plotStandardBoxShape() {
BitmapShape boxShape = new BitmapShape(5, 5);
for (int i=1; i<=3; i++) {
boxShape.plot(i, 0);
boxShape.plot(i, 4);
boxShape.plot(0, i);
boxShape.plot(4, i);
}
boxShape.origin.setLocation(2, 2);
return boxShape;
}
public ShapeTable readStandardShapeTable() {
ShapeTable st = ShapeTable.read(BOX_SAMPLE);
assertNotNull(st);
assertNotNull(st.shapes);
assertEquals(1, st.shapes.size());
return st;
}
@Test
public void testWriteStandardShapeTable() throws IOException {
ShapeTable st = new ShapeTable();
st.shapes.add(drawStandardBoxShape());
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
st.write(outputStream);
assertArrayEquals(BOX_SAMPLE, outputStream.toByteArray());
}
@Test
public void testStandardShapeTableVectors() {
ShapeTable st = readStandardShapeTable();
VectorShape expected = new VectorShape()
.moveDown().moveDown()
.plotLeft().plotLeft()
.moveUp().plotUp().plotUp().plotUp()
.moveRight().plotRight().plotRight().plotRight()
.moveDown().plotDown().plotDown().plotDown()
.moveLeft().plotLeft();
Shape s = st.shapes.get(0);
assertNotNull(s);
assertEquals(expected.vectors, s.toVector().vectors);
}
@Test
public void testStandardShapeTableBitmap() {
ShapeTable st = readStandardShapeTable();
BitmapShape expected = plotStandardBoxShape();
Shape s = st.shapes.get(0);
assertEquals(expected.grid, s.toBitmap().grid);
}
@Test
public void testToVectorFromBitmap() throws IOException {
BitmapShape bitmapShape = plotStandardBoxShape();
VectorShape vectorShape = bitmapShape.toVector();
BitmapShape newBitmapShape = vectorShape.toBitmap();
assertEquals(bitmapShape.grid, newBitmapShape.grid);
}
@Test
public void testTextShapeExporterNoBorder() throws IOException {
ShapeTable st = readStandardShapeTable();
final String expected = ".XXX.\n"
+ "X...X\n"
+ "X.+.X\n"
+ "X...X\n"
+ ".XXX.\n";
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ShapeExporter exp = ShapeExporter.text().noBorder().build();
exp.export(st.shapes.get(0), outputStream);
String actual = new String(outputStream.toByteArray());
assertEquals(expected, actual);
}
@Test
public void testTextShapeExporterAsciiBorder() throws IOException {
ShapeTable st = readStandardShapeTable();
final String expected = "+-----+\n"
+ "|.XXX.|\n"
+ "|X...X|\n"
+ "|X.+.X|\n"
+ "|X...X|\n"
+ "|.XXX.|\n"
+ "+-----+\n";
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ShapeExporter exp = ShapeExporter.text().asciiTextBorder().build();
exp.export(st.shapes.get(0), outputStream);
String actual = new String(outputStream.toByteArray());
assertEquals(expected, actual);
}
@Test
public void testTextShapeTableExporterNoBorder() throws IOException {
ShapeTable st = readStandardShapeTable();
// Simulate 4 of these identical shapes by adding 3 more
st.shapes.add(st.shapes.get(0));
st.shapes.add(st.shapes.get(0));
st.shapes.add(st.shapes.get(0));
final String oneExpectedRow = ".XXX. .XXX.\n"
+ "X...X X...X\n"
+ "X.+.X X.+.X\n"
+ "X...X X...X\n"
+ ".XXX. .XXX.\n";
String expected = oneExpectedRow + "\n" + oneExpectedRow;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ShapeExporter exp = ShapeExporter.text().maxWidth(12).noBorder().build();
exp.export(st, outputStream);
String actual = new String(outputStream.toByteArray());
assertEquals(expected, actual);
}
@Test
public void testTextShapeTableExporterAsciiBorder() throws IOException {
ShapeTable st = readStandardShapeTable();
// Simulate 4 of these identical shapes by adding 3 more
st.shapes.add(st.shapes.get(0));
st.shapes.add(st.shapes.get(0));
st.shapes.add(st.shapes.get(0));
final String divider = "+-----+-----+\n";
final String oneExpectedRow = divider
+ "|.XXX.|.XXX.|\n"
+ "|X...X|X...X|\n"
+ "|X.+.X|X.+.X|\n"
+ "|X...X|X...X|\n"
+ "|.XXX.|.XXX.|\n";
String expected = oneExpectedRow + oneExpectedRow + divider;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ShapeExporter exp = ShapeExporter.text().maxWidth(12).asciiTextBorder().build();
exp.export(st, outputStream);
String actual = new String(outputStream.toByteArray());
assertEquals(expected, actual);
}
}

View File

@ -0,0 +1,44 @@
package io.github.applecommander.bastools.api.shapes;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class VectorCommandTest {
@Test
public void testDirections() {
test(0, 1, VectorCommand.MOVE_DOWN, VectorCommand.PLOT_DOWN);
test(0, -1, VectorCommand.MOVE_UP, VectorCommand.PLOT_UP);
test(-1, 0, VectorCommand.MOVE_LEFT, VectorCommand.PLOT_LEFT);
test(1, 0, VectorCommand.MOVE_RIGHT, VectorCommand.PLOT_RIGHT);
}
public void test(int xmove, int ymove, VectorCommand... commands) {
for (VectorCommand command : commands) {
assertEquals(xmove, command.xmove);
assertEquals(ymove, command.ymove);
}
}
@Test
public void testPlot() {
test(false, VectorCommand.MOVE_DOWN, VectorCommand.MOVE_LEFT, VectorCommand.MOVE_RIGHT, VectorCommand.MOVE_UP);
test(true, VectorCommand.PLOT_DOWN, VectorCommand.PLOT_LEFT, VectorCommand.PLOT_RIGHT, VectorCommand.PLOT_UP);
}
public void test(boolean plot, VectorCommand... commands) {
for (VectorCommand command : commands) {
assertEquals(plot, command.plot);
}
}
@Test
public void testOpposite() {
test(VectorCommand.MOVE_DOWN, VectorCommand.MOVE_UP);
test(VectorCommand.MOVE_LEFT, VectorCommand.MOVE_RIGHT);
test(VectorCommand.PLOT_DOWN, VectorCommand.PLOT_UP);
test(VectorCommand.PLOT_LEFT, VectorCommand.PLOT_RIGHT);
}
public void test(VectorCommand a, VectorCommand b) {
assertEquals(a, b.opposite());
assertEquals(b, a.opposite());
}
}

View File

@ -0,0 +1,48 @@
package io.github.applecommander.bastools.api.utils;
import static org.junit.Assert.*;
import org.junit.Test;
public class ConverterTest {
@Test
public void testToInteger() {
assertEquals(0x1000, Converters.toInteger("0x1000").intValue());
assertEquals(0x1000, Converters.toInteger("0X1000").intValue());
assertEquals(0x1000, Converters.toInteger("$1000").intValue());
assertEquals(1000, Converters.toInteger("1000").intValue());
assertNull(Converters.toInteger(null));
}
@Test
public void testToBoolean() {
assertTrue(Converters.toBoolean("true"));
assertTrue(Converters.toBoolean("True"));
assertTrue(Converters.toBoolean("YES"));
assertFalse(Converters.toBoolean("faLse"));
assertFalse(Converters.toBoolean("No"));
assertFalse(Converters.toBoolean("notreally"));
assertNull(Converters.toBoolean(null));
}
@Test
public void testToIntStream_Range() {
final int[] expected = { 4, 5, 6, 7, 8 };
assertArrayEquals(expected, Converters.toIntStream("4-8").toArray());
}
@Test
public void testToIntStream_List() {
final int[] expected314159 = { 3, 1, 4, 1, 5, 9 };
assertArrayEquals(expected314159, Converters.toIntStream("3,1,4,1,5,9").toArray());
final int[] expected7 = { 7 };
assertArrayEquals(expected7, Converters.toIntStream("7").toArray());
}
@Test
public void testToIntStream_Complex() {
final int[] expected = { 1, 5,6,7, 9, 2,3,4, 8 };
assertArrayEquals(expected, Converters.toIntStream("1;5-7;9;2-4;8").toArray());
}
}

View File

@ -0,0 +1,50 @@
package io.github.applecommander.bastools.api.utils;
import java.util.LinkedList;
import java.util.Queue;
import io.github.applecommander.bastools.api.model.ApplesoftKeyword;
import io.github.applecommander.bastools.api.model.Token;
public class TokenBuilder {
private int lineNumber;
private Queue<Token> tokens = new LinkedList<Token>();
public static TokenBuilder builder() {
return new TokenBuilder();
}
public TokenBuilder eol() {
add(Token.eol(lineNumber));
lineNumber += 1;
return this;
}
public TokenBuilder number(Double number) {
return add(Token.number(lineNumber, number));
}
public TokenBuilder ident(String text) {
return add(Token.ident(lineNumber, text));
}
public TokenBuilder comment(String text) {
return add(Token.comment(lineNumber, text));
}
public TokenBuilder string(String text) {
return add(Token.string(lineNumber, text));
}
public TokenBuilder keyword(ApplesoftKeyword keyword) {
return add(Token.keyword(lineNumber, keyword));
}
public TokenBuilder syntax(int ch) {
return add(Token.syntax(lineNumber, ch));
}
public TokenBuilder directive(String text) {
return add(Token.directive(lineNumber, text));
}
private TokenBuilder add(Token token) {
tokens.add(token);
return this;
}
public Queue<Token> tokens() {
return tokens;
}
}

View File

@ -0,0 +1,43 @@
package io.github.applecommander.bastools.api.utils;
import static org.junit.Assert.assertEquals;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import io.github.applecommander.bastools.api.utils.VariableNameGenerator;
public class VariableNameGeneratorTest {
@Test
public void testNameSequence() {
Map<Integer,String> expecteds = new HashMap<>();
expecteds.put(0, "A");
expecteds.put(25, "Z");
expecteds.put(26, "AA");
expecteds.put(51, "ZA");
expecteds.put(52, "AB");
expecteds.put(77, "ZB");
// very last name in sequence
expecteds.put(VariableNameGenerator.LENGTH-1, "Z9");
int lastCheck = expecteds.keySet().stream().max(Integer::compare).get();
VariableNameGenerator gen = new VariableNameGenerator();
for (int i = 0; i <= lastCheck; i++) {
String varName = gen.get().orElseThrow(() -> new RuntimeException("Ran out of variable names too early!"));
if (expecteds.containsKey(i)) {
assertEquals(expecteds.get(i), varName);
}
}
}
@Test
public void testSequenceLength() {
VariableNameGenerator gen = new VariableNameGenerator();
int count = 0;
while (gen.get().isPresent()) count++;
assertEquals(VariableNameGenerator.LENGTH, count);
}
}

View File

@ -0,0 +1,16 @@
; A bitmap that defines a box as given by Applesoft BASIC Programmer's Reference Manual
; The bitmap is transformed into a vector by BitmapShape#toVector.
; Notes:
; x = plot
; . = unplotted; used to clarify image regions
; + = origin, no plot (assumed to be upper-left if unspecified)
; * = origin. plot
; whitespace is ignored
.bitmap label-bitmap
.xxx.
x...x
x.+.x
x...x
.xxx.

View File

@ -0,0 +1,20 @@
; A vector box as given by Applesoft BASIC Programmer's Reference Manual
; Notes:
; move[up|down|left|right] = move vector
; plot[up|down|left|right] = plot vector
; whitespace is ignored
; case insensitive
.long label-long
movedown 2
plotleft 2
moveup
plotup 3
moveright
plotright 3
movedown
plotdown 3
moveleft
plotleft

View File

@ -0,0 +1,15 @@
; A vector box as given by Applesoft BASIC Programmer's Reference Manual
; Notes:
; udlr = move vector
; UDLR = plot vector
; whitespace is ignored
; case sensitive
.short label-short
dd
LL
uUUU
rRRR
dDDD
lL

View File

@ -0,0 +1,13 @@
; From Mouse Maze (ca. 1983)
; See https://github.com/a2geek/mouse-maze-2001/tree/master/doc/original
.bitmap
..........*X..
....XXXX.XX...
...XXXXXXXX...
.XXXXXXXXXXX..
XX.XXXXXXX.XX.
X...XXXXXXXXXX
XX............
.XXX.XX.......
...XXX........

View File

@ -0,0 +1,15 @@
.bitmap
....XXXXX...+
XXXXX...XX...
....XXXXX....
.............
..XX..XXX....
...XX.XXX....
...XX.XXXX...
..XX.XXXXX...
....XXXXXX...
.XXXXXXXXXXX.
XX.........XX
XX.........XX
.XXXXXXXXXXX.

View File

@ -1,8 +1,8 @@
# Universal applesingle version number. Used for:
# Universal bastools version number. Used for:
# - Naming JAR file.
# - The build will insert this into a file that is read at run time as well.
version=0.2.0
version=0.4.0
# Maven Central Repository G and A of GAV coordinate. :-)
group=net.sf.applecommander
archivesBaseName=bastokenizer
archivesBaseName=bastools

Binary file not shown.

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

286
gradlew vendored
View File

@ -1,78 +1,129 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -81,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@ -89,84 +140,95 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

43
gradlew.bat vendored
View File

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -35,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -45,28 +64,14 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

3
samples/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
setup.sh
*.po
!template.po

187
samples/destroyer.bas Normal file
View File

@ -0,0 +1,187 @@
' One-time initializations
10 gosub 50000: \
chargemax=5: \
dim chargex(chargemax),chargey(chargemax),chargeshape(chargemax),\
priorchargex(chargemax),priorchargey(chargemax),priorchargeshape(chargemax): \
ch=36:cv=37: \
charwidth=7:charheight=8: \
space=asc(" "):rubshape=95+charbase: \
waterline=40:shipy=waterline-3:chargestarty=waterline+2: \
goto 1000
' Search list of depth charges for open slot; if found, queue up the next depth charge!
20 for i=1 to chargemax: \
if chargex(i) then next i:return
30 chargex(i)=shipx: \
chargey(i)=chargestarty: \
chargeshape(i)=constdepthchargeshape: \
remainingcharges=remainingcharges-1: \
gosub 40: \
vtab 3:htab 37:qq=remainingcharges: \
return
' Display number in QQ zero-padded to 4 digits
40 a$="0000"+str$(qq): \
a$=mid$(a$,len(a$)-3)
' Display string at current HTAB,VTAB location. Note the erase done with the rub character; assumption is color is black!
50 ly=peek(cv)*charheight: \
lx=peek(ch)*charwidth
60 for c=1 to len(a$): \
ls=charbase+asc(mid$(a$,c,1))-space: \
draw rubshape at lx,ly: \
xdraw ls at lx,ly: \
lx=lx+charwidth: \
next c: \
return
' Update ALL states on the screen
90 vtab 1:htab 37:qq=hiscore:gosub 40: \
vtab 2:qq=score:gosub 40: \
vtab 3:qq=remainingcharges:gosub 40: \
return
' Game initialization
100 shipx=140:shipdirection=0:shipshape=constdestroyershape: \
score=0:remainingcharges=30:chargeindex=1
120 for i=1 to chargemax: \
chargex(i)=0: \
chargey(i)=0: \
chargeshape(i)=0: \
next i
130 subshape=0:subx=0:suby=0:subdirection=0
140 explosionshape=0:explosionx=0:explosiony=0
150 gosub 90
' Draw all active shapes (based on X coordinate)
200 xdraw shipshape at shipx,shipy: \
oldshipx=shipx:oldshipshape=shipshape
210 if subx then \
xdraw subshape at subx,suby: \
oldsubshape=subshape:oldsubx=subx:oldsuby=suby
220 priorchargex(chargeindex)=chargex(chargeindex): \
priorchargey(chargeindex)=chargey(chargeindex): \
priorchargeshape(chargeindex)=chargeshape(chargeindex)
230 if chargex(chargeindex) then \
xdraw chargeshape(chargeindex) at chargex(chargeindex),chargey(chargeindex)
240 if explosionx then \
xdraw explosionshape at explosionx,explosiony:zs=explosionshape:zx=explosionx:zy=explosiony
' Handle keyboard
300 keypress=peek(-16384): \
if keypress < 128 then 400
310 poke -16368,0
320 if keypress=136 then \
shipshape=constdestroyershape: \
shipdirection=shipdirection-1
330 if keypress=149 then \
shipshape=constdestroyershape+1: \
shipdirection=shipdirection+1
340 if shipdirection > 3 then shipdirection=3
350 if shipdirection < -3 then shipdirection=-3
360 if keypress=160 and remainingcharges > 0 then gosub 20
' Submarine. Only one at a time. If it doesn't exist, pick a random number to see if one shows up!
400 if not subx then 450
410 subx=subx+subdirection
420 if subx < 10 or subx > 270 then subshape=0:subx=0:suby=0:subdirection=0
430 goto 500
' Test for a new sub
450 rr=rnd(1):if rr < 0.10 then 480
460 if rr > 0.20 then 500
470 subx=10:suby=waterline+10+rnd(1)*80:subdirection=2:subshape=constsubshape+1:goto 500
480 subx=270:suby=waterline+10+rnd(1)*80:subdirection=-2:subshape=constsubshape
' Move the destroyer/ship
500 shipx=shipx+shipdirection
510 if shipx < 10 then shipx=10
520 if shipx > 270 then shipx=270
' Make the explosion all explody
550 if explosionshape then explosionshape=explosionshape+2:if explosionshape >= constdestroyershape then explosionx=0
' Move one of the depth charges
600 if not chargex(chargeindex) then 690
610 chargeshape(chargeindex)=chargeshape(chargeindex)+1: \
if chargeshape(chargeindex) > constdepthchargeshape+3 then \
chargeshape(chargeindex)=constdepthchargeshape
620 chargey(chargeindex)=chargey(chargeindex)+5: \
if chargey(chargeindex) > 155 then \
chargex(chargeindex)=0: \
chargey(chargeindex)=0: \
chargeshape(chargeindex)=0
' Setup for next depth charge
690 chargeindex=chargeindex+1: \
if chargeindex > chargemax then \
chargeindex = 1
' Erase shapes
700 if oldsubx then \
xdraw oldsubshape at oldsubx,oldsuby: \
a=oldsubx:oldsubx=0: \
if peek(234) then \
explosionshape=oldsubshape+2: \
explosionx=a: \
explosiony=oldsuby: \
subx=0: \
score=score+1: \
remainingcharges=remainingcharges+5: \
vtab 2:htab 37:qq=score:gosub 40: \
vtab 3:qq=remainingcharges:gosub 40
710 if priorchargex(chargeindex) then \
xdraw priorchargeshape(chargeindex) at priorchargex(chargeindex),priorchargey(chargeindex): \
if peek(234) then \
chargex(chargeindex)=0: \
chargey(chargeindex)=0: \
chargeshape(chargeindex)=0
720 if zx then xdraw zs at zx,explosiony:zx=0
730 xdraw oldshipshape at oldshipx,shipy
740 if remainingcharges > 0 then 200
' Stupidly, once we run out of charges, we immediately end the game :-/
' Check if a high score was set ...
800 if score > hiscore then hiscore = score
' Display the title screen!
1000 hgr:poke -16302,0
1010 hcolor=0: \
vtab 1:htab 1:a$="Destroyer!":gosub 50
1020 vtab 22:htab 4:a$="| Left, } Right, Spacebar to fire":gosub 50
1030 vtab 1:htab 28:a$="Hiscore:":gosub 50:vtab 2:a$="Score:":gosub 50:vtab 3:a$="Charges:":gosub 50:gosub 90
1050 hcolor=6: \
hplot 0,waterline to 279,waterline: \
hplot 0,159 to 279,159:hcolor=0
1060 ly=waterline+15: \
xdraw constdestroyershape at 45,ly+3: \
a$="Your destroyer":lx=70:gosub 60
1070 ly=ly+10: \
xdraw constsubshape at 45,ly+3: \
a$="Enemy submarine (1 point)":lx=70:gosub 60
1080 ly=ly+10:lx=40: \
for s=constdepthchargeshape to constdepthchargeshape+3: \
xdraw s at lx,ly+3: \
lx=lx+5: \
next s: \
a$="Depth charges (hit = +5!)":lx=70:gosub 60
1090 ly=ly+20:lx=40: \
a$="Demo code for 'bastools'":gosub 60
1100 ly=ly+10:lx=40: \
a$="Visit applecommander.github.io":gosub 60
1110 ly=ly+20:lx=40: \
a$="PRESS ANY KEY TO BEGIN!":gosub 60
1120 if peek(-16384)<128 then 1120
1130 poke -16368,0
1140 hcolor=0: \
for y=waterline+15 to 150: \
hplot 0,y to 279,y: \
next y
1150 goto 100
50000 $shape src="destroyer.st", \
poke=yes, \
init=yes, \
assign=(constsubshape="sub-left", \
constdestroyershape="ship-left", \
constdepthchargeshape="depthcharge-1", \
charbase="characters")

117
samples/destroyer.st Normal file
View File

@ -0,0 +1,117 @@
.bitmap sub-left
....xxxx.......
.xxxxxxxxxxx.xx
xxxxxxx*xxxxxxx
.xxxxxxxxxxx.xx
.bitmap sub-right
.......xxxx....
xx.xxxxxxxxxxx.
xxxxxxx*xxxxxxx
xx.xxxxxxxxxxx.
.bitmap sub-left-explosion-1
....xxxxx.......
.xxxx...xxxxxxxx
xx..........x..x
x......+.......x
xx..........x..x
.xxxxxxxxxxxxxxx
.bitmap sub-right-explosion-1
.......xxxxx....
xxxxxxxx...xxxx.
x..x..........xx
x.......+......x
x..x..........xx
xxxxxxxxxxxxxxx.
.bitmap sub-left-explosion-2
....xxx.xxx.......
.xx.x.....xxx.xxxx
xx...............x
x................x
........+.........
x................x
xx...............x
.xxx.xxxxxxxx.xxxx
.bitmap sub-right-explosion-2
.......xxx.xxx....
xxxx.xxx.....x.xx.
x...............xx
x................x
.........+........
x................x
x...............xx
xxxx.xxxxxxxx.xxx.
.bitmap sub-left-explosion-3
.....xx..xx..........
......xxxx......xx...
.x............xxx....
xx...................
.....................
...................xx
..........+........x.
...................xx
.....................
xx...................
.x............xxx....
......xxxx......xx...
.....xx..xx..........
.bitmap sub-right-explosion-3
..........xx..xx.....
...xx......xxxx......
....xxx............x.
...................xx
.....................
xx...................
.x........+..........
xx...................
.....................
...................xx
....xxx............x.
...xx......xxxx......
..........xx..xx.....
.bitmap ship-left
.....xxxx......
.....xxxx......
xxxxxxx*x..xxxx
.xxxxxxxxxxxxxx
..xxxxxxxxxxxxx
.bitmap ship-right
......xxxx.....
......xxxx.....
xxxx..x*xxxxxxx
xxxxxxxxxxxxxx.
xxxxxxxxxxxxx..
.bitmap depthcharge-1
xxx
x*x
...
.bitmap depthcharge-2
xx.
x*x
.xx
.bitmap depthcharge-3
xx.
x*.
xx.
.bitmap depthcharge-4
.xx
x*x
xx.
; "]IMPERATOR" font from Beagle Bros. "Apple Mechanic"
.external characters
type=bin
shapes=1-96
import=imperator.bin

BIN
samples/imperator.bin Normal file

Binary file not shown.

16
samples/mkdestroyer.sh Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
if [ ! -f ./setup.sh ]
then
echo "This script depends on a 'setup.sh' script in the local directory."
echo "'setup.sh' should define an alias for 'bt' and 'ac'."
echo "Alternatively, you may craft a shell script with those names and place them on"
echo "the PATH."
exit 1
fi
shopt -s expand_aliases
source ./setup.sh
cp template.po destroyer.po
bt --stdout --applesingle --optimize destroyer.bas | ac -as destroyer.po startup

BIN
samples/template.po Normal file

Binary file not shown.

View File

@ -1,7 +1,9 @@
include 'api'
include 'tools:bt'
include 'tools:st'
rootProject.name = 'bastokenizer'
project(":api").name = 'bastokenizer-api'
project(":tools").name = 'bastokenizer-tools'
project(":tools:bt").name = 'bastokenizer-tools-bt'
rootProject.name = 'bastools'
project(":api").name = 'bastools-api'
project(":tools").name = 'bastools-tools'
project(":tools:bt").name = 'bastools-tools-bt'
project(":tools:st").name = 'bastools-tools-st'

View File

@ -1,43 +1,47 @@
## Usage
```shell
$ bt
Missing required parameter: <sourceFile>
$ bt --help
Usage: bt [-chOVx] [--addresses] [--applesingle] [--debug] [--list] [--pretty]
[--stdout] [--tokens] [--variables]
[--max-line-length=<maxLineLength>] [-a=<address>] [-o=<outputFile>]
[--stdout] [--tokens] [--variables] [--wrapper] [-a=<address>]
[--max-line-length=<maxLineLength>] [-o=<outputFile>]
[-f=<optimizations>[,<optimizations>...]]... <sourceFile>
Transforms an AppleSoft program from text back to its tokenized state.
<sourceFile> AppleSoft BASIC program to process.
Options:
--addresses Dump line number addresses out.
--applesingle Write output in AppleSingle format
--debug Print debug output.
--list List structure as bastokenizer understands it.
--max-line-length=<maxLineLength>
Maximum line length for generated lines.
Default: 255
--pretty Pretty print structure as bastokenizer understands it.
--stdout Send binary output to stdout.
--tokens Dump token list to stdout for debugging.
--variables Generate a variable report
-a, --address=<address> Base address for program
Default: 2049
-c, --copy Generate a copy/paste form of output for testing in an
emulator.
-f= <optimizations>[,<optimizations>...]
--addresses Dump line number addresses out.
--applesingle Write output in AppleSingle format
-c, --copy Generate a copy/paste form of output for testing in
an emulator.
--debug Print debug output.
-f=<optimizations>[,<optimizations>...]
Enable specific optimizations.
* remove-empty-statements - Strip out all '::'-like
statements.
* remove-rem-statements - Remove all REM statements.
* shorten-variable-names - Ensure all variables are
1 or 2 characters long.
* extract-constant-values - Assign all constant
values first.
* merge-lines - Merge lines.
* renumber - Renumber program.
-h, --help Show this help message and exit.
--list List structure as bastools understands it.
--max-line-length=<maxLineLength>
Maximum line length for generated lines.
Default: 255
-o, --output=<outputFile> Write binary output to file.
-O, --optimize Apply all optimizations.
--pretty Pretty print structure as bastools understands it.
--stdout Send binary output to stdout.
--tokens Dump token list to stdout for debugging.
-V, --version Print version information and exit.
--variables Generate a variable report
--wrapper Wrap the Applesoft program (DOS 3.3).
-x, --hex Generate a binary hex dump for debugging.
```
@ -122,9 +126,18 @@ $ bt --list tools/bt/src/test/resources/circles.bas
```shell
$ bt --optimize --list tools/bt/src/test/resources/circles.bas
0 GOTO 2
1 FOR A = 0 TO PT:X = X(A) * SZ:Y = Y(A) * SZ: HPLOT XO + X,YO + Y: HPLOT XO - X,YO + Y: HPLOT XO + X,YO - Y: HPLOT XO - X,YO - Y: NEXT A: RETURN
2 HGR :C(0) = 1:C(1) = 2:C(2) = 3:C(3) = 5:C(4) = 6:C(5) = 7: HOME : VTAB 21: INVERSE : PRINT "JUST A MOMENT": NORMAL :PI = 3.14159:PT = 30: DIM X(PT),Y(PT): FOR A = 0 TO PT:B = PI * (A / (PT * 2)):X(A) = SIN (B):Y(A) = COS (B): NEXT A: HOME : VTAB 21: FOR Q = 1 TO 100:C = 6 * RND (1): HCOLOR= C(C):SZ = 10 + (40 * RND (1)):XO = (279 - SZ * 2) * RND (1) + SZ:YO = (159 - SZ * 2) * RND (1) + SZ: GOSUB 1: NEXT Q
0 D=0:E=1:F=2:G=3:H=5:I=4:J=6:K=7:L=21:M=100:N=10:O=40:P=279:R=159: GOTO 2
1 FOR A = D TO PT:X = X(A) * SZ:Y = Y(A) * SZ: HPLOT XO + X,YO + Y: HPLOT XO - X,YO + Y: HPLOT XO + X,YO - Y: HPLOT XO - X,YO - Y: NEXT A: RETURN
2 HGR :C(D) = E:C(E) = F:C(F) = G:C(G) = H:C(I) = J:C(H) = K: HOME : VTAB L: INVERSE : PRINT "JUST A MOMENT": NORMAL :PI = 3.14159:PT = 30: DIM X(PT),Y(PT): FOR A = D TO PT:B = PI * (A / (PT * F)):X(A) = SIN (B):Y(A) = COS (B): NEXT A: HOME : VTAB L: FOR Q = E TO M:C = J * RND (E): HCOLOR= C(C):SZ = N + (O * RND (E)):XO = (P - SZ * F) * RND (E) + SZ:YO = (R - SZ * F) * RND (E) + SZ: GOSUB 1: NEXT Q
```
Specific optimizations may also be triggered:
```shell
$ bt -fremove-rem-statements,merge-lines --list tools/bt/src/test/resources/circles.bas
10 GOTO 110
30 FOR A = 0 TO PT:X = X(A) * SZ:Y = Y(A) * SZ: HPLOT XO + X,YO + Y: HPLOT XO - X,YO + Y: HPLOT XO + X,YO - Y: HPLOT XO - X,YO - Y: NEXT A: RETURN
110 HGR :C(0) = 1:C(1) = 2:C(2) = 3:C(3) = 5:C(4) = 6:C(5) = 7: HOME : VTAB 21: INVERSE : PRINT "JUST A MOMENT": NORMAL :PI = 3.14159:PT = 30: DIM X(PT),Y(PT): FOR A = 0 TO PT:B = PI * (A / (PT * 2)):X(A) = SIN (B):Y(A) = COS (B): NEXT A: HOME : VTAB 21: FOR Q = 1 TO 100:C = 6 * RND (1): HCOLOR= C(C):SZ = 10 + (40 * RND (1)):XO = (279 - SZ * 2) * RND (1) + SZ:YO = (159 - SZ * 2) * RND (1) + SZ: GOSUB 30: NEXT Q
```
## Piping to stdout
@ -140,3 +153,13 @@ demo.dsk /DEMO/
ProDOS format; 139,264 bytes free; 4,096 bytes used.
```
## Wrapping the application
DOS 3.3 (but not ProDOS) seems to rewrite the application linked list when an Applesoft program is loaded; this rewrites the pointers and impacts any embedded (via `$embed`) machine code. With the wrapper, the application is "wrapped" with a startup Applesoft program that prevents the rewrite. The wrapper is just a simple program:
```basic
10 POKE 103,24:POKE 104,8:RUN
```
This is a valid program that resets the Applesoft pointer to just after the current program and runs that other program.

View File

@ -1,26 +1,29 @@
plugins {
id 'org.springframework.boot' version '2.0.2.RELEASE'
id 'org.springframework.boot' version "2.7.17"
id 'java'
id 'application'
}
sourceCompatibility = 11
targetCompatibility = 11
repositories {
jcenter()
mavenCentral()
}
apply plugin: 'application'
mainClassName = "io.github.applecommander.bastokenizer.tools.bt.Main"
mainClassName = "io.github.applecommander.bastools.tools.bt.Main"
bootJar {
manifest {
attributes(
'Implementation-Title': 'bastokenizer',
'Implementation-Version': "${version} (${new Date().format('yyyy-MM-dd HH:mm')})"
'Implementation-Title': 'BT CLI',
'Implementation-Version': "${project.version} (${new Date().format('yyyy-MM-dd HH:mm')})"
)
}
}
dependencies {
compile 'info.picocli:picocli:3.0.2'
compile 'net.sf.applecommander:applesingle-api:1.1.0'
compile project(':bastokenizer-api')
implementation 'info.picocli:picocli:4.7.5'
implementation 'net.sf.applecommander:applesingle-api:1.2.2'
implementation project(':bastools-api')
}

View File

@ -1,10 +0,0 @@
package io.github.applecommander.bastokenizer.tools.bt;
import picocli.CommandLine.IVersionProvider;
/** Display version information. Note that this is dependent on Gradle configuration. */
public class VersionProvider implements IVersionProvider {
public String[] getVersion() {
return new String[] { Main.class.getPackage().getImplementationVersion() };
}
}

View File

@ -1,4 +1,4 @@
package io.github.applecommander.bastokenizer.tools.bt;
package io.github.applecommander.bastools.tools.bt;
import java.io.PrintStream;
import java.util.Arrays;

View File

@ -1,6 +1,6 @@
package io.github.applecommander.bastokenizer.tools.bt;
package io.github.applecommander.bastools.tools.bt;
import io.github.applecommander.bastokenizer.api.utils.Converters;
import io.github.applecommander.bastools.api.utils.Converters;
import picocli.CommandLine.ITypeConverter;
/** Add support for "$801" and "0x801" instead of just decimal like 2049. */

View File

@ -1,27 +1,24 @@
package io.github.applecommander.bastokenizer.tools.bt;
package io.github.applecommander.bastools.tools.bt;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.*;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.Callable;
import io.github.applecommander.applesingle.AppleSingle;
import io.github.applecommander.bastokenizer.api.Configuration;
import io.github.applecommander.bastokenizer.api.Optimization;
import io.github.applecommander.bastokenizer.api.Parser;
import io.github.applecommander.bastokenizer.api.TokenReader;
import io.github.applecommander.bastokenizer.api.Visitors;
import io.github.applecommander.bastokenizer.api.model.Program;
import io.github.applecommander.bastokenizer.api.model.Token;
import io.github.applecommander.bastokenizer.api.model.Token.Type;
import io.github.applecommander.bastokenizer.api.visitors.ByteVisitor;
import io.github.applecommander.bastools.api.Configuration;
import io.github.applecommander.bastools.api.Optimization;
import io.github.applecommander.bastools.api.Parser;
import io.github.applecommander.bastools.api.TokenReader;
import io.github.applecommander.bastools.api.Visitors;
import io.github.applecommander.bastools.api.model.Program;
import io.github.applecommander.bastools.api.model.Token;
import io.github.applecommander.bastools.api.model.Token.Type;
import io.github.applecommander.bastools.api.visitors.ByteVisitor;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Help.Visibility;
@ -59,10 +56,10 @@ public class Main implements Callable<Void> {
@Option(names = "--applesingle", description = "Write output in AppleSingle format")
private boolean applesingleFlag;
@Option(names = "--pretty", description = "Pretty print structure as bastokenizer understands it.")
@Option(names = "--pretty", description = "Pretty print structure as bastools understands it.")
private boolean prettyPrint;
@Option(names = "--list", description = "List structure as bastokenizer understands it.")
@Option(names = "--list", description = "List structure as bastools understands it.")
private boolean listPrint;
@Option(names = "--tokens", description = "Dump token list to stdout for debugging.")
@ -73,11 +70,16 @@ public class Main implements Callable<Void> {
@Option(names = "--max-line-length", description = "Maximum line length for generated lines.", showDefaultValue = Visibility.ALWAYS)
private int maxLineLength = 255;
@Option(names = "--wrapper", description = "Wrap the Applesoft program (DOS 3.3).")
private boolean wrapProgram;
@Option(names = "-f", converter = OptimizationTypeConverter.class, split = ",", description = {
"Enable specific optimizations.",
"* @|green remove-empty-statements|@ - Strip out all '::'-like statements.",
"* @|green remove-rem-statements|@ - Remove all REM statements.",
"* @|green shorten-variable-names|@ - Ensure all variables are 1 or 2 characters long.",
"* @|green extract-constant-values|@ - Assign all constant values first.",
"* @|green merge-lines|@ - Merge lines.",
"* @|green renumber|@ - Renumber program."
})
@ -87,7 +89,7 @@ public class Main implements Callable<Void> {
private boolean allOptimizations;
@Option(names = "--debug", description = "Print debug output.")
private boolean debugFlag;
private static boolean debugFlag;
private PrintStream debug = new PrintStream(new OutputStream() {
@Override
public void write(int b) throws IOException {
@ -99,7 +101,22 @@ public class Main implements Callable<Void> {
private File sourceFile;
public static void main(String[] args) throws FileNotFoundException, IOException {
CommandLine.call(new Main(), args);
try {
int exitCode = new CommandLine(new Main()).execute(args);
System.exit(exitCode);
} catch (Throwable t) {
if (Main.debugFlag) {
t.printStackTrace(System.err);
} else {
String message = t.getMessage();
while (t != null) {
message = t.getMessage();
t = t.getCause();
}
System.err.printf("Error: %s\n", Optional.ofNullable(message).orElse("An error occurred."));
}
System.exit(1);
}
}
@Override
@ -156,17 +173,34 @@ public class Main implements Callable<Void> {
}
ByteVisitor byteVisitor = Visitors.byteVisitor(config);
byte[] data = byteVisitor.dump(program);
byte[] wrapperData = new byte[0];
if (wrapProgram) {
Queue<Token> wrapperTokens = TokenReader.tokenize(new ByteArrayInputStream(
"10 POKE 103,24:POKE 104,8:RUN".getBytes()));
Parser wrapperParser = new Parser(wrapperTokens);
Program wrapperProgram = wrapperParser.parse();
wrapperData = byteVisitor.dump(wrapperProgram);
}
byte[] programData = byteVisitor.dump(program);
if (showLineAddresses) {
byteVisitor.getLineAddresses().forEach((l,a) -> System.out.printf("%5d ... $%04x\n", l, a));
}
// Merge both programs together. Note that wrapperData may be a 0 byte array.
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write(wrapperData);
output.write(programData);
output.flush();
byte[] data = output.toByteArray();
if (hexFormat) {
HexDumper.standard().dump(address, data);
}
if (copyFormat) {
HexDumper.apple2().dump(address, data);
}
saveResults(data);
}

Some files were not shown because too many files have changed in this diff Show More