From aa29fb8399f19dca6d2811eda8a372279f84f64f Mon Sep 17 00:00:00 2001 From: Rob Greene Date: Thu, 12 Jul 2018 23:37:19 -0500 Subject: [PATCH] Extensive remodeling for slightly more complex directives. Adding $shape. #16. --- .gitignore | 1 + api/README-TOKENIZER.md | 66 ++++- .../bastools/api/Directive.java | 230 ++++++++++++++---- .../bastools/api/Directives.java | 6 +- .../bastools/api/code/AsmBuilder.java | 101 ++++++++ .../bastools/api/code/BasicBuilder.java | 120 +++++++++ .../bastools/api/code/CodeBuilder.java | 55 +++++ .../bastools/api/code/CodeGenerator.java | 32 +++ .../bastools/api/code/CodeMark.java | 37 +++ .../bastools/api/code/GeneratorState.java | 52 ++++ .../directives/EmbeddedBinaryDirective.java | 113 ++++----- .../api/directives/EmbeddedShapeTable.java | 136 +++++++++++ .../bastools/api/directives/HexDirective.java | 14 +- .../bastools/api/model/Token.java | 12 + .../bastools/api/shapes/BitmapShape.java | 5 + .../bastools/api/shapes/Shape.java | 2 + .../bastools/api/shapes/ShapeTable.java | 10 + .../bastools/api/shapes/VectorShape.java | 5 + .../bastools/api/utils/Converters.java | 6 + .../bastools/api/visitors/ByteVisitor.java | 2 + .../bastools/api/code/CodeBuilderTest.java | 43 ++++ 21 files changed, 934 insertions(+), 114 deletions(-) create mode 100644 api/src/main/java/io/github/applecommander/bastools/api/code/AsmBuilder.java create mode 100644 api/src/main/java/io/github/applecommander/bastools/api/code/BasicBuilder.java create mode 100644 api/src/main/java/io/github/applecommander/bastools/api/code/CodeBuilder.java create mode 100644 api/src/main/java/io/github/applecommander/bastools/api/code/CodeGenerator.java create mode 100644 api/src/main/java/io/github/applecommander/bastools/api/code/CodeMark.java create mode 100644 api/src/main/java/io/github/applecommander/bastools/api/code/GeneratorState.java create mode 100644 api/src/main/java/io/github/applecommander/bastools/api/directives/EmbeddedShapeTable.java create mode 100644 api/src/test/java/io/github/applecommander/bastools/api/code/CodeBuilderTest.java diff --git a/.gitignore b/.gitignore index 157c5f8..6d159fd 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ bin/ /*.po /*.do /*.bas +/*.st # Gradle extras .gradle/ diff --git a/api/README-TOKENIZER.md b/api/README-TOKENIZER.md index 9e0ba95..8804d32 100644 --- a/api/README-TOKENIZER.md +++ b/api/README-TOKENIZER.md @@ -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=`, required. Specifies the file to load. +* `moveto=`, optional. If provided, generates code to move binary to destination. Automatically `CALL`ed. +* `var=`, 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=] + [,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,:POKE 233,` into the line of code. +* `address=`, 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: diff --git a/api/src/main/java/io/github/applecommander/bastools/api/Directive.java b/api/src/main/java/io/github/applecommander/bastools/api/Directive.java index 53d344b..4f5383e 100644 --- a/api/src/main/java/io/github/applecommander/bastools/api/Directive.java +++ b/api/src/main/java/io/github/applecommander/bastools/api/Directive.java @@ -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 parameters = new ArrayList<>(); + private List paramTokens = new ArrayList<>(); + private Map parameters = new TreeMap<>(String::compareToIgnoreCase); + private Set 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 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 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 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 optionalMapExpression(String paramName) { + return optionalExpression(paramName) + .flatMap(Expression::toMapExpression); + } + + public static final Predicate ONLY_ONE = (n) -> n == 1; + public static final Predicate ZERO_OR_ONE = (n) -> n <= 1; + public static final Predicate 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 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 toSimpleExpression(); + public Optional 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 toSimpleExpression() { + return Optional.of(this); + } + public Optional toMapExpression() { + return Optional.empty(); + } + } + public static class MapExpression implements Expression { + private final Map expressions = new HashMap<>(); + public Optional get(String key) { + return Optional.ofNullable(expressions.get(key)); + } + public Set> entrySet() { + return expressions.entrySet(); + } + public Optional toSimpleExpression() { + return Optional.empty(); + } + public Optional toMapExpression() { + return Optional.of(this); + } + } } diff --git a/api/src/main/java/io/github/applecommander/bastools/api/Directives.java b/api/src/main/java/io/github/applecommander/bastools/api/Directives.java index 4af82e1..1b70486 100644 --- a/api/src/main/java/io/github/applecommander/bastools/api/Directives.java +++ b/api/src/main/java/io/github/applecommander/bastools/api/Directives.java @@ -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); } }; diff --git a/api/src/main/java/io/github/applecommander/bastools/api/code/AsmBuilder.java b/api/src/main/java/io/github/applecommander/bastools/api/code/AsmBuilder.java new file mode 100644 index 0000000..a1555b0 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/code/AsmBuilder.java @@ -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. + *

+ * 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: + *

+     * LDA #low(value)
+     * STA address
+     * LDA #high(value)
+     * STA address+1
+     * 
+ */ + 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: + *
+     * LDA #low(mark)
+     * STA address
+     * LDA #high(mark)
+     * STA address+1
+     * 
+ */ + 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!"); + } + } +} \ No newline at end of file diff --git a/api/src/main/java/io/github/applecommander/bastools/api/code/BasicBuilder.java b/api/src/main/java/io/github/applecommander/bastools/api/code/BasicBuilder.java new file mode 100644 index 0000000..4c52593 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/code/BasicBuilder.java @@ -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. + *

+ * 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 " 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 " 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 " 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

,:POKE ," 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; + } +} diff --git a/api/src/main/java/io/github/applecommander/bastools/api/code/CodeBuilder.java b/api/src/main/java/io/github/applecommander/bastools/api/code/CodeBuilder.java new file mode 100644 index 0000000..cf531b4 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/code/CodeBuilder.java @@ -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); + }); + } +} \ No newline at end of file diff --git a/api/src/main/java/io/github/applecommander/bastools/api/code/CodeGenerator.java b/api/src/main/java/io/github/applecommander/bastools/api/code/CodeGenerator.java new file mode 100644 index 0000000..23f59ef --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/code/CodeGenerator.java @@ -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); }; + } +} \ No newline at end of file diff --git a/api/src/main/java/io/github/applecommander/bastools/api/code/CodeMark.java b/api/src/main/java/io/github/applecommander/bastools/api/code/CodeMark.java new file mode 100644 index 0000000..f6ec4de --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/code/CodeMark.java @@ -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". + *

+ * Multiple passes occur for the following reasons:

    + *
  • an assembly address can be calculated on the second pass (1st is to actually calculate address and 2nd pass is to + * use that address).
  • + *
  • 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
  • + *
+ * + * @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; + } + } +} diff --git a/api/src/main/java/io/github/applecommander/bastools/api/code/GeneratorState.java b/api/src/main/java/io/github/applecommander/bastools/api/code/GeneratorState.java new file mode 100644 index 0000000..3d00cca --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/code/GeneratorState.java @@ -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); + } +} \ No newline at end of file diff --git a/api/src/main/java/io/github/applecommander/bastools/api/directives/EmbeddedBinaryDirective.java b/api/src/main/java/io/github/applecommander/bastools/api/directives/EmbeddedBinaryDirective.java index 331aaeb..fcb8da8 100644 --- a/api/src/main/java/io/github/applecommander/bastools/api/directives/EmbeddedBinaryDirective.java +++ b/api/src/main/java/io/github/applecommander/bastools/api/directives/EmbeddedBinaryDirective.java @@ -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=' parameter"); + Optional targetAddress = optionalIntegerExpression(PARAM_MOVETO); + Optional 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 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 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
: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
: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); } } diff --git a/api/src/main/java/io/github/applecommander/bastools/api/directives/EmbeddedShapeTable.java b/api/src/main/java/io/github/applecommander/bastools/api/directives/EmbeddedShapeTable.java new file mode 100644 index 0000000..74658a1 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/directives/EmbeddedShapeTable.java @@ -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 src = optionalStringExpression(PARAM_SRC); + Optional label = optionalStringExpression(PARAM_LABEL); + Optional assign = optionalMapExpression(PARAM_ASSIGN); + Optional bin = optionalStringExpression(PARAM_BIN); + boolean poke = defaultBooleanExpression(PARAM_POKE, true); + boolean init = defaultBooleanExpression(PARAM_INIT, true); + Optional 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 binData = bin.map(this::readBin); + Optional 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 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 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 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 65535) { throw new RuntimeException("$hex address out of range"); diff --git a/api/src/main/java/io/github/applecommander/bastools/api/model/Token.java b/api/src/main/java/io/github/applecommander/bastools/api/model/Token.java index 2962a92..fdc5575 100644 --- a/api/src/main/java/io/github/applecommander/bastools/api/model/Token.java +++ b/api/src/main/java/io/github/applecommander/bastools/api/model/Token.java @@ -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); diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/BitmapShape.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/BitmapShape.java index b244d06..b39e419 100644 --- a/api/src/main/java/io/github/applecommander/bastools/api/shapes/BitmapShape.java +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/BitmapShape.java @@ -126,6 +126,11 @@ public class BitmapShape implements Shape { } return !hasData; } + + @Override + public String getLabel() { + return label; + } @Override public BitmapShape toBitmap() { diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java index 2660d48..23be689 100644 --- a/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java @@ -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. */ diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeTable.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeTable.java index 55994d1..4e35332 100644 --- a/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeTable.java +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeTable.java @@ -57,6 +57,16 @@ public class ShapeTable { } public final List shapes = new ArrayList<>(); + + public int findPositionByLabel(String label) { + for (int i=0; i