Extensive remodeling for slightly more complex directives. Adding

$shape. #16.
This commit is contained in:
Rob Greene 2018-07-12 23:37:19 -05:00
parent b9f2eb28d1
commit aa29fb8399
21 changed files with 934 additions and 114 deletions

1
.gitignore vendored
View File

@ -19,6 +19,7 @@ bin/
/*.po
/*.do
/*.bas
/*.st
# Gradle extras
.gradle/

View File

@ -35,13 +35,22 @@ The framework allows embedding of directives.
### `$embed`
`$embed` will allow a binary to be embedded within the resulting application *and will 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. Usage example:
`$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 "read.time.bin", "0x0260"
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. It takes two parameters: file name and target address, both are strings.
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:
@ -70,14 +79,61 @@ 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 hexidecimal addresses into an application makes sense, the `$hex` directive allows that to be done in a rudimentary manner.
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 "fc58"
10 call $hex value="fc58"
```
Yields:

View File

@ -3,82 +3,224 @@ 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;
protected List<Token> parameters = new ArrayList<>();
private List<Token> paramTokens = new ArrayList<>();
private Map<String,Expression> parameters = new TreeMap<>(String::compareToIgnoreCase);
private Set<String> parameterNames;
protected Directive(Configuration config, OutputStream outputStream) {
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));
}
public void append(Token token) {
// Skip the commas...
if (token.type == Type.SYNTAX && ",".equals(token.text)) return;
parameters.add(token);
public Optional<Expression> optionalExpression(String paramName) {
return Optional.ofNullable(parameters.get(paramName));
}
protected Token require(Type... types) {
Token t = parameters.remove(0);
// 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 || (token.type == Type.SYNTAX && ",".equals(token.text))) {
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);
} 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) {
throw new IllegalArgumentException("Expecting a type of " + types);
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;
}
protected String requiresString() {
Token t = require(Type.STRING);
private String requireIdentToken() {
Token t = requireToken(Type.IDENT, Type.KEYWORD);
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);
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));
}
}
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);
private boolean checkSyntaxToken(String syntax) {
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

@ -6,6 +6,7 @@ import java.util.Map;
import java.util.TreeMap;
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 {
@ -16,8 +17,9 @@ public class Directives {
private static final long serialVersionUID = -8111460701487331592L;
{
put("$embed", EmbeddedBinaryDirective.class);
put("$hex", HexDirective.class);
put(EmbeddedBinaryDirective.NAME, EmbeddedBinaryDirective.class);
put(HexDirective.NAME, HexDirective.class);
put(EmbeddedShapeTable.NAME, EmbeddedShapeTable.class);
}
};

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,37 @@
package io.github.applecommander.bastools.api.code;
/**
* 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 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();
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

@ -1,6 +1,5 @@
package io.github.applecommander.bastools.api.directives;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
@ -9,73 +8,77 @@ import java.util.Optional;
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.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(config, outputStream);
super(NAME, config, outputStream, PARAM_FILE, PARAM_MOVETO, PARAM_VAR);
}
@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();
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 embeddedStart = new CodeMark();
CodeMark embeddedEnd = new CodeMark();
Optional<Line> nextLine = line.nextLine();
byte[] basicCode = nextLine.isPresent()
? callAndGoto(startAddress,nextLine.get())
: callAndReturn(startAddress);
variableName.ifPresent(var -> {
builder.basic()
.assign(var, embeddedStart)
.endStatement();
});
final int moveLength = 8*3 + 2 + 3; // LDA/STA, LDY, JMP.
int embeddedStart = startAddress + basicCode.length + moveLength;
int embeddedEnd = embeddedStart + bin.length;
targetAddress.ifPresent(address -> {
builder.basic()
.CALL(embeddedStart)
.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.asm()
.setAddress(embeddedStart, 0x3c)
.setAddress(embeddedEnd, 0x3e)
.setAddress(address, 0x42)
.ldy(0x00)
.jmp(0xfe2c)
.end();
});
builder.set(embeddedStart)
.addBinary(bin)
.set(embeddedEnd);
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();
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(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(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(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

@ -9,21 +9,19 @@ import io.github.applecommander.bastools.api.model.ApplesoftKeyword;
import io.github.applecommander.bastools.api.model.Line;
/**
* A simple directive to introduce hexidecimal capabilities. StreamTokenizer does not
* 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(config, outputStream);
super(NAME, config, outputStream);
}
@Override
public void writeBytes(int startAddress, Line line) throws IOException {
if (parameters.size() != 1) {
throw new RuntimeException("$hex directive requires one parameter");
}
String string = requiresString();
int value = Integer.parseInt(string, 16);
int value = requiredIntegerExpression("value", "$hex directive requires 'value' parameter");
if (value < 0 || value > 65535) {
throw new RuntimeException("$hex address out of range");

View File

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

@ -126,6 +126,11 @@ public class BitmapShape implements Shape {
}
return !hasData;
}
@Override
public String getLabel() {
return label;
}
@Override
public BitmapShape toBitmap() {

View File

@ -3,6 +3,8 @@ package io.github.applecommander.bastools.api.shapes;
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. */

View File

@ -57,6 +57,16 @@ public class ShapeTable {
}
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);

View File

@ -222,6 +222,11 @@ public class VectorShape implements Shape {
public boolean isEmpty() {
return vectors.isEmpty();
}
@Override
public String getLabel() {
return label;
}
@Override
public BitmapShape toBitmap() {

View File

@ -19,4 +19,10 @@ public class Converters {
}
}
public static Boolean toBoolean(String value) {
if (value == null) {
return null;
}
return "true".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value);
}
}

View File

@ -82,6 +82,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

@ -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());
}
}