Extensive remodeling for slightly more complex directives. Adding
$shape. #16.
This commit is contained in:
parent
b9f2eb28d1
commit
aa29fb8399
|
@ -19,6 +19,7 @@ bin/
|
|||
/*.po
|
||||
/*.do
|
||||
/*.bas
|
||||
/*.st
|
||||
|
||||
# Gradle extras
|
||||
.gradle/
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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); };
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -126,6 +126,11 @@ public class BitmapShape implements Shape {
|
|||
}
|
||||
return !hasData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BitmapShape toBitmap() {
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue