diff --git a/.gitignore b/.gitignore index d444b80..0f651f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,22 @@ -/target/ -/.settings/ -/.classpath -/.project -pom.xml.versionsBackup +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# Eclipse +.settings/ +.classpath +.project +bin/ + +# Misc. Apple II remnants *.bin *.dsk + +# Gradle extras +.gradle/ +build/ diff --git a/api/build.gradle b/api/build.gradle new file mode 100644 index 0000000..ea21718 --- /dev/null +++ b/api/build.gradle @@ -0,0 +1,71 @@ +repositories { + jcenter() +} + +apply plugin: 'java-library' +apply plugin: 'maven' +apply plugin: 'signing' + +dependencies { + compile group: 'net.sf.applecommander', name: 'applesingle-api', version:'1.1.0' + testImplementation 'junit:junit:4.12' +} + +task javadocJar(type: Jar) { + classifier = 'javadoc' + from javadoc +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allSource +} + +artifacts { + archives javadocJar, sourcesJar +} + +signing { + sign configurations.archives +} + +uploadArchives { + repositories { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { + authentication(userName: findProperty('ossrhUsername'), password: findProperty('ossrhPassword')) + } + + snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { + authentication(userName: findProperty('ossrhUsername'), password: findProperty('ossrhPassword')) + } + + pom.project { + name archivesBaseName + packaging 'jar' + description 'Experiments with generating an AppleSoft B/BAS tokenized "binary".' + url 'https://applecommander.github.io/' + + scm { + url 'https://github.com/AppleCommander/bastokenizer' + } + + licenses { + license { + name 'The GNU General Public License (GPL) Version 3, 29 June 2007' + url 'https://www.gnu.org/licenses/gpl-3.0.html' + } + } + + developers { + developer { + id 'robgreene' + email 'robgreene@gmail.com' + } + } + } + } + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/Configuration.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/Configuration.java new file mode 100644 index 0000000..7333f34 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/Configuration.java @@ -0,0 +1,61 @@ +package io.github.applecommander.bastokenizer.api; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Objects; + +public class Configuration { + public final File sourceFile; + public final int startAddress; + public final int maxLineLength; + public final PrintStream debugStream; + + private Configuration(Builder b) { + this.sourceFile = b.sourceFile; + this.startAddress = b.startAddress; + this.maxLineLength = b.maxLineLength; + this.debugStream = b.debugStream; + } + + public static Builder builder() { + return new Builder(); + } + public static class Builder { + private Builder() { /* Prevent construction */ } + + private File sourceFile; + private int startAddress = 0x801; + private int maxLineLength = 255; + private PrintStream debugStream = new PrintStream(new OutputStream() { + @Override + public void write(int b) throws IOException { + // Do nothing + } + }); + + public Builder sourceFile(File sourceFile) { + this.sourceFile = sourceFile; + return this; + } + public Builder startAddress(int startAddress) { + this.startAddress = startAddress; + return this; + } + public Builder maxLineLength(int maxLineLength) { + this.maxLineLength = maxLineLength; + return this; + } + public Builder debugStream(PrintStream debugStream) { + this.debugStream = debugStream; + return this; + } + + public Configuration build() { + Objects.requireNonNull(sourceFile, "Please configure a sourceFile"); + Objects.requireNonNull(debugStream, "debugStream cannot be null"); + return new Configuration(this); + } + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/Directive.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/Directive.java new file mode 100644 index 0000000..8996ad4 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/Directive.java @@ -0,0 +1,84 @@ +package io.github.applecommander.bastokenizer.api; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.github.applecommander.bastokenizer.api.model.Line; +import io.github.applecommander.bastokenizer.api.model.Token; +import io.github.applecommander.bastokenizer.api.model.Token.Type; +import io.github.applecommander.bastokenizer.api.utils.Converters; + +public abstract class Directive { + protected Configuration config; + protected OutputStream outputStream; + protected List parameters = new ArrayList<>(); + + protected Directive(Configuration config, OutputStream outputStream) { + Objects.requireNonNull(config); + Objects.requireNonNull(outputStream); + this.config = config; + this.outputStream = outputStream; + } + + public void append(Token token) { + // Skip the commas... + if (token.type == Type.SYNTAX && ",".equals(token.text)) return; + parameters.add(token); + } + protected Token require(Type... types) { + Token t = parameters.remove(0); + boolean matches = false; + for (Type type : types) { + matches |= type == t.type; + } + if (!matches) { + throw new IllegalArgumentException("Expecting a type of " + types); + } + return t; + } + protected String requiresString() { + Token t = require(Type.STRING); + return t.text; + } + protected int requiresInteger() { + Token t = require(Type.NUMBER, Type.STRING); + if (t.type == Type.NUMBER) { + return t.number.intValue(); + } + return Converters.toInteger(t.text); + } + + protected void ldy(int value) throws IOException { + outputStream.write(0xa0); + outputStream.write(value); + } + protected void jmp(int address) throws IOException { + outputStream.write(0x4c); + outputStream.write(address & 0xff); + outputStream.write(address >> 8); + } + protected void lda(int value) throws IOException { + outputStream.write(0xa9); + outputStream.write(value); + } + protected void sta(int address) throws IOException { + if ((address & 0xff00) == 0) { + outputStream.write(0x85); + outputStream.write(address); + } else { + throw new RuntimeException("sta does not handle 16 bit addresses yet!"); + } + } + protected void setAddress(int value, int address) throws IOException { + lda(value & 0xff); + sta(address); + lda(value >> 8); + sta(address+1); + } + + /** Write directive contents to output file. Note that address is adjusted for the line header already. */ + public abstract void writeBytes(int startAddress, Line line) throws IOException; +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/Directives.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/Directives.java new file mode 100644 index 0000000..eca24e5 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/Directives.java @@ -0,0 +1,32 @@ +package io.github.applecommander.bastokenizer.api; + +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.util.Map; +import java.util.TreeMap; + +import io.github.applecommander.bastokenizer.api.directives.EmbeddedBinaryDirective; + +public abstract class Directives { + private static Map> DIRECTIVES = + new TreeMap>(String.CASE_INSENSITIVE_ORDER) { + private static final long serialVersionUID = -8111460701487331592L; + + { + put("$embed", EmbeddedBinaryDirective.class); + } + }; + + public static Directive find(String text, Configuration config, OutputStream outputStream) { + if (DIRECTIVES.containsKey(text)) { + try { + Class clazz = DIRECTIVES.get(text); + Constructor constructor = clazz.getConstructor(Configuration.class, OutputStream.class); + return constructor.newInstance(config, outputStream); + } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) { + throw new IllegalArgumentException(String.format("Unable to construct directive '%s'", text), e); + } + } + throw new IllegalArgumentException(String.format("Unable to find directive '%s'", text)); + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/Optimization.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/Optimization.java new file mode 100644 index 0000000..d9dd14c --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/Optimization.java @@ -0,0 +1,26 @@ +package io.github.applecommander.bastokenizer.api; + +import java.util.function.Function; + +import io.github.applecommander.bastokenizer.api.optimizations.MergeLines; +import io.github.applecommander.bastokenizer.api.optimizations.RemoveEmptyStatements; +import io.github.applecommander.bastokenizer.api.optimizations.RemoveRemStatements; +import io.github.applecommander.bastokenizer.api.optimizations.Renumber; + +public enum Optimization { + REMOVE_EMPTY_STATEMENTS(config -> new RemoveEmptyStatements()), + REMOVE_REM_STATEMENTS(config -> new RemoveRemStatements()), + MERGE_LINES(config -> new MergeLines(config)), + RENUMBER(config -> new Renumber()); + + private Function factory; + + private Optimization(Function factory) { + this.factory = factory; + } + + public Visitor create(Configuration config) { + return factory.apply(config); + } + +} diff --git a/src/main/java/com/webcodepro/applecommander/util/applesoft/Parser.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/Parser.java similarity index 80% rename from src/main/java/com/webcodepro/applecommander/util/applesoft/Parser.java rename to api/src/main/java/io/github/applecommander/bastokenizer/api/Parser.java index e84758e..2cdaa5f 100644 --- a/src/main/java/com/webcodepro/applecommander/util/applesoft/Parser.java +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/Parser.java @@ -1,9 +1,13 @@ -package com.webcodepro.applecommander.util.applesoft; +package io.github.applecommander.bastokenizer.api; import java.util.Objects; import java.util.Queue; -import com.webcodepro.applecommander.util.applesoft.Token.Type; +import io.github.applecommander.bastokenizer.api.model.Line; +import io.github.applecommander.bastokenizer.api.model.Program; +import io.github.applecommander.bastokenizer.api.model.Statement; +import io.github.applecommander.bastokenizer.api.model.Token; +import io.github.applecommander.bastokenizer.api.model.Token.Type; /** * The Parser will read a series of Tokens and build a Program. diff --git a/src/main/java/com/webcodepro/applecommander/util/applesoft/TokenReader.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/TokenReader.java similarity index 96% rename from src/main/java/com/webcodepro/applecommander/util/applesoft/TokenReader.java rename to api/src/main/java/io/github/applecommander/bastokenizer/api/TokenReader.java index a276f11..f5bd3b0 100644 --- a/src/main/java/com/webcodepro/applecommander/util/applesoft/TokenReader.java +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/TokenReader.java @@ -1,4 +1,4 @@ -package com.webcodepro.applecommander.util.applesoft; +package io.github.applecommander.bastokenizer.api; import java.io.File; import java.io.FileNotFoundException; @@ -12,6 +12,9 @@ import java.util.LinkedList; import java.util.Optional; import java.util.Queue; +import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword; +import io.github.applecommander.bastokenizer.api.model.Token; + /** * The TokenReader, given a text file, generates a series of Tokens (in the compiler sense, * not AppleSoft) for the AppleSoft program. diff --git a/src/main/java/com/webcodepro/applecommander/util/applesoft/Visitor.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/Visitor.java similarity index 66% rename from src/main/java/com/webcodepro/applecommander/util/applesoft/Visitor.java rename to api/src/main/java/io/github/applecommander/bastokenizer/api/Visitor.java index e9de39d..a535145 100644 --- a/src/main/java/com/webcodepro/applecommander/util/applesoft/Visitor.java +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/Visitor.java @@ -1,4 +1,9 @@ -package com.webcodepro.applecommander.util.applesoft; +package io.github.applecommander.bastokenizer.api; + +import io.github.applecommander.bastokenizer.api.model.Line; +import io.github.applecommander.bastokenizer.api.model.Program; +import io.github.applecommander.bastokenizer.api.model.Statement; +import io.github.applecommander.bastokenizer.api.model.Token; /** * The Visitor interface allows some flexibility in what can be done with the diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/Visitors.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/Visitors.java new file mode 100644 index 0000000..5277513 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/Visitors.java @@ -0,0 +1,73 @@ +package io.github.applecommander.bastokenizer.api; + +import java.io.PrintStream; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +import io.github.applecommander.bastokenizer.api.visitors.ByteVisitor; +import io.github.applecommander.bastokenizer.api.visitors.LineNumberTargetCollector; +import io.github.applecommander.bastokenizer.api.visitors.PrettyPrintVisitor; +import io.github.applecommander.bastokenizer.api.visitors.PrintVisitor; +import io.github.applecommander.bastokenizer.api.visitors.ReassignmentVisitor; +import io.github.applecommander.bastokenizer.api.visitors.VariableReportVisitor; + +/** + * This class presents all of the common Visitor implementations via builder patterns. + * The number is currently small enough that all the builders and visitors are defined + * in this one class. + * + * @author rob + */ +public class Visitors { + public static PrintBuilder printBuilder() { + return new PrintBuilder(); + } + public static class PrintBuilder { + private PrintStream printStream = System.out; + private Function creator = PrintVisitor::new; + + public PrintBuilder printStream(PrintStream printStream) { + Objects.requireNonNull(printStream); + this.printStream = printStream; + return this; + } + public PrintBuilder prettyPrint(boolean flag) { + creator = flag ? PrettyPrintVisitor::new : PrintVisitor::new; + return this; + } + public PrintBuilder prettyPrint() { + creator = PrettyPrintVisitor::new; + return this; + } + public PrintBuilder print() { + creator = PrintVisitor::new; + return this; + } + + public Visitor build() { + return creator.apply(this); + } + public PrintStream getPrintStream() { + return printStream; + } + } + + public static ByteVisitor byteVisitor(Configuration config) { + return new ByteVisitor(config); + } + + /** Rewrite the Program tree with the line number reassignments given. */ + public static ReassignmentVisitor reassignVisitor(Map reassignments) { + return new ReassignmentVisitor(reassignments); + } + + /** Collect all line numbers that are a target of GOTO, GOSUB, etc. */ + public static LineNumberTargetCollector lineNumberTargetCollector() { + return new LineNumberTargetCollector(); + } + + public static Visitor variableReportVisitor() { + return new VariableReportVisitor(); + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/directives/EmbeddedBinaryDirective.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/directives/EmbeddedBinaryDirective.java new file mode 100644 index 0000000..4b13513 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/directives/EmbeddedBinaryDirective.java @@ -0,0 +1,81 @@ +package io.github.applecommander.bastokenizer.api.directives; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Optional; + +import io.github.applecommander.bastokenizer.api.Configuration; +import io.github.applecommander.bastokenizer.api.Directive; +import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword; +import io.github.applecommander.bastokenizer.api.model.Line; + +public class EmbeddedBinaryDirective extends Directive { + public EmbeddedBinaryDirective(Configuration config, OutputStream outputStream) { + super(config, outputStream); + } + + @Override + public void writeBytes(int startAddress, Line line) throws IOException { + if (parameters.size() != 2) { + throw new IllegalArgumentException("$embed requires a name and address parameter"); + } + String filename = requiresString(); + int targetAddress = requiresInteger(); + + File file = new File(config.sourceFile.getParentFile(), filename); + byte[] bin = Files.readAllBytes(file.toPath()); + + Optional nextLine = line.nextLine(); + byte[] basicCode = nextLine.isPresent() + ? callAndGoto(startAddress,nextLine.get()) + : callAndReturn(startAddress); + + final int moveLength = 8*3 + 2 + 3; // LDA/STA, LDY, JMP. + int embeddedStart = startAddress + basicCode.length + moveLength; + int embeddedEnd = embeddedStart + bin.length; + + outputStream.write(basicCode); + setAddress(embeddedStart, 0x3c); + setAddress(embeddedEnd, 0x3e); + setAddress(targetAddress, 0x42); + ldy(0x00); + jmp(0xfe2c); + outputStream.write(bin); + } + // In program, "CALL
: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(); + } +} diff --git a/src/main/java/com/webcodepro/applecommander/util/applesoft/ApplesoftKeyword.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/model/ApplesoftKeyword.java similarity index 98% rename from src/main/java/com/webcodepro/applecommander/util/applesoft/ApplesoftKeyword.java rename to api/src/main/java/io/github/applecommander/bastokenizer/api/model/ApplesoftKeyword.java index c648ce8..06903d3 100644 --- a/src/main/java/com/webcodepro/applecommander/util/applesoft/ApplesoftKeyword.java +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/model/ApplesoftKeyword.java @@ -1,4 +1,4 @@ -package com.webcodepro.applecommander.util.applesoft; +package io.github.applecommander.bastokenizer.api.model; import java.io.IOException; import java.io.Reader; diff --git a/src/main/java/com/webcodepro/applecommander/util/applesoft/Line.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/model/Line.java similarity index 85% rename from src/main/java/com/webcodepro/applecommander/util/applesoft/Line.java rename to api/src/main/java/io/github/applecommander/bastokenizer/api/model/Line.java index 7e366af..b6346d2 100644 --- a/src/main/java/com/webcodepro/applecommander/util/applesoft/Line.java +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/model/Line.java @@ -1,10 +1,12 @@ -package com.webcodepro.applecommander.util.applesoft; +package io.github.applecommander.bastokenizer.api.model; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; +import io.github.applecommander.bastokenizer.api.Visitor; + /** An AppleSoft BASIC Line representation. */ public class Line { public final Program program; diff --git a/src/main/java/com/webcodepro/applecommander/util/applesoft/Program.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/model/Program.java similarity index 66% rename from src/main/java/com/webcodepro/applecommander/util/applesoft/Program.java rename to api/src/main/java/io/github/applecommander/bastokenizer/api/model/Program.java index dd2b335..b33a949 100644 --- a/src/main/java/com/webcodepro/applecommander/util/applesoft/Program.java +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/model/Program.java @@ -1,8 +1,10 @@ -package com.webcodepro.applecommander.util.applesoft; +package io.github.applecommander.bastokenizer.api.model; import java.util.ArrayList; import java.util.List; +import io.github.applecommander.bastokenizer.api.Visitor; + /** A Program is a series of lines. */ public class Program { public final List lines = new ArrayList<>(); diff --git a/src/main/java/com/webcodepro/applecommander/util/applesoft/Statement.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/model/Statement.java similarity index 68% rename from src/main/java/com/webcodepro/applecommander/util/applesoft/Statement.java rename to api/src/main/java/io/github/applecommander/bastokenizer/api/model/Statement.java index 8a062f5..2a5b7de 100644 --- a/src/main/java/com/webcodepro/applecommander/util/applesoft/Statement.java +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/model/Statement.java @@ -1,8 +1,10 @@ -package com.webcodepro.applecommander.util.applesoft; +package io.github.applecommander.bastokenizer.api.model; import java.util.ArrayList; import java.util.List; +import io.github.applecommander.bastokenizer.api.Visitor; + /** A Statement is simply a series of Tokens. */ public class Statement { public final List tokens = new ArrayList<>(); diff --git a/src/main/java/com/webcodepro/applecommander/util/applesoft/Token.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/model/Token.java similarity index 94% rename from src/main/java/com/webcodepro/applecommander/util/applesoft/Token.java rename to api/src/main/java/io/github/applecommander/bastokenizer/api/model/Token.java index 62a0285..423bf3c 100644 --- a/src/main/java/com/webcodepro/applecommander/util/applesoft/Token.java +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/model/Token.java @@ -1,4 +1,6 @@ -package com.webcodepro.applecommander.util.applesoft; +package io.github.applecommander.bastokenizer.api.model; + +import io.github.applecommander.bastokenizer.api.Visitor; /** * A Token in the classic compiler sense, in that this represents a component of the application. diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/BaseVisitor.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/BaseVisitor.java new file mode 100644 index 0000000..abd7d7c --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/BaseVisitor.java @@ -0,0 +1,64 @@ +package io.github.applecommander.bastokenizer.api.optimizations; + +import java.util.HashMap; +import java.util.Map; + +import io.github.applecommander.bastokenizer.api.Visitor; +import io.github.applecommander.bastokenizer.api.Visitors; +import io.github.applecommander.bastokenizer.api.model.Line; +import io.github.applecommander.bastokenizer.api.model.Program; +import io.github.applecommander.bastokenizer.api.model.Statement; +import io.github.applecommander.bastokenizer.api.model.Token; + +/** Common base class for optimization visitors that allow the program tree to be rewritten. */ +public class BaseVisitor implements Visitor { + protected Map reassignments = new HashMap<>(); + protected Program newProgram; + + @Override + public Program visit(Program program) { + newProgram = new Program(); + program.lines.forEach(l -> { + Line line = l.accept(this); + boolean lineKept = line != null && !line.statements.isEmpty(); + if (lineKept) { + newProgram.lines.add(line); + reassignments.replaceAll((k,v) -> v == null ? l.lineNumber : v); + } else { + // Make a place-holder for the reassignment; we'll patch it in once we find a line that sticks around. + reassignments.put(l.lineNumber, null); + } + }); + if (!reassignments.isEmpty()) { + // Now, renumber based on our findings! + return newProgram.accept(Visitors.reassignVisitor(reassignments)); + } else { + return newProgram; + } + } + + @Override + public Line visit(Line line) { + Line newLine = new Line(line.lineNumber, this.newProgram); + line.statements.forEach(s -> { + Statement statement = s.accept(this); + if (statement != null) newLine.statements.add(statement); + }); + return newLine; + } + + @Override + public Statement visit(Statement statement) { + Statement newStatement = new Statement(); + statement.tokens.forEach(t -> { + Token token = t.accept(this); + if (token != null) newStatement.tokens.add(token); + }); + return newStatement; + } + + @Override + public Token visit(Token token) { + return token; + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/MergeLines.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/MergeLines.java new file mode 100644 index 0000000..832a447 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/MergeLines.java @@ -0,0 +1,84 @@ +package io.github.applecommander.bastokenizer.api.optimizations; + +import java.io.PrintStream; +import java.util.Set; + +import io.github.applecommander.bastokenizer.api.Configuration; +import io.github.applecommander.bastokenizer.api.Visitors; +import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword; +import io.github.applecommander.bastokenizer.api.model.Line; +import io.github.applecommander.bastokenizer.api.model.Program; +import io.github.applecommander.bastokenizer.api.model.Statement; +import io.github.applecommander.bastokenizer.api.model.Token; +import io.github.applecommander.bastokenizer.api.model.Token.Type; +import io.github.applecommander.bastokenizer.api.visitors.ByteVisitor; +import io.github.applecommander.bastokenizer.api.visitors.LineNumberTargetCollector; + +public class MergeLines extends BaseVisitor { + private Set targets; + private Line mergeLine; + private ByteVisitor bv; + private int maxLineLength; + private PrintStream debugStream; + + public MergeLines(Configuration config) { + this.maxLineLength = config.maxLineLength; + this.debugStream = config.debugStream; + this.bv = Visitors.byteVisitor(config); + } + + @Override + public Program visit(Program program) { + LineNumberTargetCollector c = Visitors.lineNumberTargetCollector(); + program.accept(c); + targets = c.getTargets(); + debugStream.printf("Target lines = %s\n", targets); + return super.visit(program); + } + + @Override + public Line visit(Line line) { + debugStream.printf("Line # %d : ", line.lineNumber); + Line newLine = new Line(line.lineNumber, this.newProgram); + newLine.statements.addAll(line.statements); + if (mergeLine == null || targets.contains(line.lineNumber)) { + // Either forced to a new line or this is a GOTO type target: Ignore length + debugStream.printf("%s\n", mergeLine == null ? "mergeLine is null" : "target line #"); + } else { + // Check length and decide if it merges based on that. + Line tmpLine = new Line(mergeLine.lineNumber, mergeLine.program); + tmpLine.statements.addAll(mergeLine.statements); + tmpLine.statements.addAll(line.statements); + if (bv.length(tmpLine) > maxLineLength) { + // It was too big, do not add + debugStream.printf("merge would exceed max line length: %d > %d\n", bv.length(tmpLine), maxLineLength); + } else { + // We can add line to mergeLine (mergeLine is already added to program, must keep that object) + mergeLine.statements.addAll(line.statements); + if (hasTerminal(line)) mergeLine = null; + debugStream.printf("line %s\n", mergeLine == null ? "had terminals" : "was added to mergeLine"); + return null; + } + } + // Always reset mergeLine based on the terminal characteristics + mergeLine = hasTerminal(line) ? null : newLine; + debugStream.printf("line %s\n", mergeLine == null ? "had terminals" : "is now mergeLine"); + return newLine; + } + + private boolean hasTerminal(Line line) { + // Terminals are: IF, REM, GOTO, END, ON .. GOTO (GOTO is trigger), RESUME, RETURN, STOP + // Includes directives. + for (Statement s : line.statements) { + for (Token t : s.tokens) { + boolean terminal = t.keyword == ApplesoftKeyword.IF || t.type == Type.COMMENT /* REM */ + || t.keyword == ApplesoftKeyword.GOTO || t.keyword == ApplesoftKeyword.END + || t.keyword == ApplesoftKeyword.RESUME || t.keyword == ApplesoftKeyword.RETURN + || t.keyword == ApplesoftKeyword.STOP + || t.type == Type.DIRECTIVE; + if (terminal) return true; + } + } + return false; + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/RemoveEmptyStatements.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/RemoveEmptyStatements.java new file mode 100644 index 0000000..3833af3 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/RemoveEmptyStatements.java @@ -0,0 +1,10 @@ +package io.github.applecommander.bastokenizer.api.optimizations; + +import io.github.applecommander.bastokenizer.api.model.Statement; + +public class RemoveEmptyStatements extends BaseVisitor { + @Override + public Statement visit(Statement statement) { + return statement.tokens.isEmpty() ? null : statement; + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/RemoveRemStatements.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/RemoveRemStatements.java new file mode 100644 index 0000000..2160691 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/RemoveRemStatements.java @@ -0,0 +1,11 @@ +package io.github.applecommander.bastokenizer.api.optimizations; + +import io.github.applecommander.bastokenizer.api.model.Statement; +import io.github.applecommander.bastokenizer.api.model.Token.Type; + +public class RemoveRemStatements extends BaseVisitor { + @Override + public Statement visit(Statement statement) { + return statement.tokens.get(0).type == Type.COMMENT ? null : statement; + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/Renumber.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/Renumber.java new file mode 100644 index 0000000..95f6910 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/Renumber.java @@ -0,0 +1,15 @@ +package io.github.applecommander.bastokenizer.api.optimizations; + +import io.github.applecommander.bastokenizer.api.model.Line; + +public class Renumber extends BaseVisitor { + protected int lineNumber = 0; + @Override + public Line visit(Line line) { + Line newLine = new Line(lineNumber++, this.newProgram); + newLine.statements.addAll(line.statements); + // Track what went where so lines can get renumbered automatically + reassignments.put(line.lineNumber, newLine.lineNumber); + return newLine; + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/utils/Converters.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/utils/Converters.java new file mode 100644 index 0000000..bd97f7b --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/utils/Converters.java @@ -0,0 +1,22 @@ +package io.github.applecommander.bastokenizer.api.utils; + +public class Converters { + private Converters() { /* Prevent construction */ } + + /** + * Convert a string to an integer allowing multiple formats. + * Normal decimal, or hexadecimal with a $ or 0x prefix. + */ + public static Integer toInteger(String value) { + if (value == null) { + return null; + } else if (value.startsWith("$")) { + return Integer.valueOf(value.substring(1), 16); + } else if (value.startsWith("0x") || value.startsWith("0X")) { + return Integer.valueOf(value.substring(2), 16); + } else { + return Integer.valueOf(value); + } + } + +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/ByteVisitor.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/ByteVisitor.java new file mode 100644 index 0000000..56bd9ae --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/ByteVisitor.java @@ -0,0 +1,159 @@ +package io.github.applecommander.bastokenizer.api.visitors; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.Stack; +import java.util.TreeMap; + +import io.github.applecommander.bastokenizer.api.Configuration; +import io.github.applecommander.bastokenizer.api.Directive; +import io.github.applecommander.bastokenizer.api.Directives; +import io.github.applecommander.bastokenizer.api.Visitor; +import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword; +import io.github.applecommander.bastokenizer.api.model.Line; +import io.github.applecommander.bastokenizer.api.model.Program; +import io.github.applecommander.bastokenizer.api.model.Statement; +import io.github.applecommander.bastokenizer.api.model.Token; + +public class ByteVisitor implements Visitor { + private Stack stack; + private Map lineAddresses; + private Configuration config; + private int address; + private Directive currentDirective; + + public ByteVisitor(Configuration config) { + this.config = config; + this.address = config.startAddress; + this.stack = new Stack<>(); + this.lineAddresses = new TreeMap<>(); + } + + /** A convenience method to invoke {@link Program#accept(Visitor)} and {@link #getBytes()}. */ + public byte[] dump(Program program) { + program.accept(this); + return getBytes(); + } + + /** A convenience method to get the length of a line. */ + public int length(Line line) { + stack.push(new ByteArrayOutputStream()); + line.accept(this); + return stack.pop().size(); + } + + public Map getLineAddresses() { + return lineAddresses; + } + + public byte[] getBytes() { + if (stack.size() != 1) { + throw new RuntimeException("Error in processing internal BASIC model!"); + } + return stack.peek().toByteArray(); + } + + @Override + public Program visit(Program program) { + stack.clear(); + stack.push(new ByteArrayOutputStream()); + program.lines.forEach(line -> line.accept(this)); + ByteArrayOutputStream os = stack.peek(); + os.write(0x00); + os.write(0x00); + return program; + } + + @Override + public Line visit(Line line) { + try { + stack.push(new ByteArrayOutputStream()); + boolean first = true; + for (Statement statement : line.statements) { + if (currentDirective != null) { + throw new RuntimeException("No statements are allowed after a directive!"); + } + if (!first) { + stack.peek().write(':'); + } + first = false; + statement.accept(this); + } + if (currentDirective != null) { + currentDirective.writeBytes(this.address+4, line); + currentDirective = null; + } + + this.lineAddresses.put(line.lineNumber, this.address); + byte[] content = stack.pop().toByteArray(); + int nextAddress = address + content.length + 5; + ByteArrayOutputStream os = stack.peek(); + os.write(nextAddress); + os.write(nextAddress >> 8); + os.write(line.lineNumber); + os.write(line.lineNumber >> 8); + os.write(content); + os.write(0x00); + this.address = nextAddress; + return line; + } catch (IOException ex) { + // Hiding the IOException as ByteArrayOutputStream does not throw it + throw new RuntimeException(ex); + } + } + + @Override + public Token visit(Token token) { + if (currentDirective != null) { + currentDirective.append(token); + return token; + } + try { + ByteArrayOutputStream os = stack.peek(); + switch (token.type) { + case COMMENT: + os.write(ApplesoftKeyword.REM.code); + os.write(token.text.getBytes()); + break; + case EOL: + os.write(0x00); + break; + case IDENT: + os.write(token.text.getBytes()); + break; + case KEYWORD: + os.write(token.keyword.code); + break; + case DIRECTIVE: + currentDirective = Directives.find(token.text, config, os); + break; + case NUMBER: + if (Math.rint(token.number) == token.number) { + os.write(Integer.toString(token.number.intValue()).getBytes()); + } else { + os.write(Double.toString(token.number).getBytes()); + } + break; + case STRING: + os.write('"'); + os.write(token.text.getBytes()); + os.write('"'); + break; + case SYNTAX: + Optional opt = ApplesoftKeyword.find(token.text); + if (opt.isPresent()) { + os.write(opt.get().code); + } else { + os.write(token.text.getBytes()); + } + break; + } + return token; + } catch (IOException ex) { + // Hiding the IOException as ByteArrayOutputStream does not throw it + throw new RuntimeException(ex); + } + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/LineNumberTargetCollector.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/LineNumberTargetCollector.java new file mode 100644 index 0000000..372add7 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/LineNumberTargetCollector.java @@ -0,0 +1,51 @@ +package io.github.applecommander.bastokenizer.api.visitors; + +import java.util.Set; +import java.util.TreeSet; + +import io.github.applecommander.bastokenizer.api.Visitor; +import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword; +import io.github.applecommander.bastokenizer.api.model.Statement; +import io.github.applecommander.bastokenizer.api.model.Token; +import io.github.applecommander.bastokenizer.api.model.Token.Type; + +public class LineNumberTargetCollector implements Visitor { + private Set targets = new TreeSet<>(); + + public Set getTargets() { + return targets; + } + + /** + * We saw a trigger, collect any numbers that follow. + * + * Trigger cases: + * - GOSUB n + * - GOTO n + * - IF ... THEN n + * - LIST n [ ,m ] + * - ON x GOTO n, m, ... + * - ON x GOSUB n, m, ... + * - ONERR GOTO n + * - RUN n + */ + @Override + public Statement visit(Statement statement) { + boolean next = false; + boolean multiple = false; + for (Token t : statement.tokens) { + if (next) { + if (t.type == Type.NUMBER) { + targets.add(t.number.intValue()); + } + next = multiple; // preserve next based on if we have multiple line numbers or not. + } else { + next = t.keyword == ApplesoftKeyword.GOSUB || t.keyword == ApplesoftKeyword.GOTO + || t.keyword == ApplesoftKeyword.THEN || t.keyword == ApplesoftKeyword.RUN + || t.keyword == ApplesoftKeyword.LIST; + multiple |= t.keyword == ApplesoftKeyword.LIST || t.keyword == ApplesoftKeyword.ON; + } + } + return statement; + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/PrettyPrintVisitor.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/PrettyPrintVisitor.java new file mode 100644 index 0000000..add15da --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/PrettyPrintVisitor.java @@ -0,0 +1,65 @@ +package io.github.applecommander.bastokenizer.api.visitors; + +import java.io.PrintStream; + +import io.github.applecommander.bastokenizer.api.Visitor; +import io.github.applecommander.bastokenizer.api.Visitors.PrintBuilder; +import io.github.applecommander.bastokenizer.api.model.Line; +import io.github.applecommander.bastokenizer.api.model.Statement; +import io.github.applecommander.bastokenizer.api.model.Token; + +public class PrettyPrintVisitor implements Visitor { + private PrintStream printStream; + + public PrettyPrintVisitor(PrintBuilder builder) { + this.printStream = builder.getPrintStream(); + } + + @Override + public Line visit(Line line) { + boolean first = true; + for (Statement statement : line.statements) { + if (first) { + first = false; + printStream.printf("%5d ", line.lineNumber); + } else { + printStream.printf("%5s ", ":"); + } + statement.accept(this); + printStream.println(); + } + return line; + } + @Override + public Token visit(Token token) { + switch (token.type) { + case EOL: + printStream.print(""); + break; + case COMMENT: + printStream.printf(" REM %s", token.text); + break; + case STRING: + printStream.printf("\"%s\"", token.text); + break; + case KEYWORD: + printStream.printf(" %s ", token.keyword.text); + break; + case IDENT: + case SYNTAX: + printStream.print(token.text); + break; + case DIRECTIVE: + printStream.printf("%s ", token.text); + break; + case NUMBER: + if (Math.rint(token.number) == token.number) { + printStream.print(token.number.intValue()); + } else { + printStream.print(token.number); + } + break; + } + return token; + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/PrintVisitor.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/PrintVisitor.java new file mode 100644 index 0000000..eaeb232 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/PrintVisitor.java @@ -0,0 +1,65 @@ +package io.github.applecommander.bastokenizer.api.visitors; + +import java.io.PrintStream; + +import io.github.applecommander.bastokenizer.api.Visitor; +import io.github.applecommander.bastokenizer.api.Visitors.PrintBuilder; +import io.github.applecommander.bastokenizer.api.model.Line; +import io.github.applecommander.bastokenizer.api.model.Statement; +import io.github.applecommander.bastokenizer.api.model.Token; + +public class PrintVisitor implements Visitor { + private PrintStream printStream; + + public PrintVisitor(PrintBuilder builder) { + this.printStream = builder.getPrintStream(); + } + + @Override + public Line visit(Line line) { + printStream.printf("%d ", line.lineNumber); + boolean first = true; + for (Statement statement : line.statements) { + if (first) { + first = false; + } else { + printStream.printf(":"); + } + statement.accept(this); + } + printStream.println(); + return line; + } + @Override + public Token visit(Token token) { + switch (token.type) { + case EOL: + printStream.print(""); + break; + case COMMENT: + printStream.printf("REM %s", token.text); + break; + case STRING: + printStream.printf("\"%s\"", token.text); + break; + case KEYWORD: + printStream.printf(" %s ", token.keyword.text); + break; + case IDENT: + case SYNTAX: + printStream.print(token.text); + break; + case DIRECTIVE: + printStream.printf("%s ", token.text); + break; + case NUMBER: + if (Math.rint(token.number) == token.number) { + printStream.print(token.number.intValue()); + } else { + printStream.print(token.number); + } + break; + } + return token; + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/ReassignmentVisitor.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/ReassignmentVisitor.java new file mode 100644 index 0000000..ec02a62 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/ReassignmentVisitor.java @@ -0,0 +1,75 @@ +package io.github.applecommander.bastokenizer.api.visitors; + +import java.util.Map; + +import io.github.applecommander.bastokenizer.api.Visitor; +import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword; +import io.github.applecommander.bastokenizer.api.model.Line; +import io.github.applecommander.bastokenizer.api.model.Program; +import io.github.applecommander.bastokenizer.api.model.Statement; +import io.github.applecommander.bastokenizer.api.model.Token; +import io.github.applecommander.bastokenizer.api.model.Token.Type; + +/** This is a mildly rewritable Visitor. */ +public class ReassignmentVisitor implements Visitor { + private Map reassignments; + private Program newProgram; + + public ReassignmentVisitor(Map reassignments) { + this.reassignments = reassignments; + } + + @Override + public Program visit(Program program) { + newProgram = new Program(); + program.lines.forEach(l -> { + Line line = l.accept(this); + newProgram.lines.add(line); + }); + return newProgram; + } + @Override + public Line visit(Line line) { + Line newLine = new Line(line.lineNumber, this.newProgram); + line.statements.forEach(s -> { + Statement statement = s.accept(this); + newLine.statements.add(statement); + }); + return newLine; + } + /** + * We saw a trigger, reassign any numbers that follow. + * + * Trigger cases: + * - GOSUB n + * - GOTO n + * - IF ... THEN n + * - LIST n [ ,m ] + * - ON x GOTO n, m, ... + * - ON x GOSUB n, m, ... + * - ONERR GOTO n + * - RUN n + */ + @Override + public Statement visit(Statement statement) { + boolean next = false; + boolean multiple = false; + Statement newStatement = new Statement(); + for (Token t : statement.tokens) { + Token newToken = t; + if (next) { + if (t.type == Type.NUMBER && reassignments.containsKey(t.number.intValue())) { + newToken = Token.number(t.line, reassignments.get(t.number.intValue()).doubleValue()); + } + next = multiple; // preserve next based on if we have multiple line numbers or not. + } else { + next = t.keyword == ApplesoftKeyword.GOSUB || t.keyword == ApplesoftKeyword.GOTO + || t.keyword == ApplesoftKeyword.THEN || t.keyword == ApplesoftKeyword.RUN + || t.keyword == ApplesoftKeyword.LIST; + multiple |= t.keyword == ApplesoftKeyword.LIST || t.keyword == ApplesoftKeyword.ON; + } + newStatement.tokens.add(newToken); + } + return newStatement; + } +} diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/VariableReportVisitor.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/VariableReportVisitor.java new file mode 100644 index 0000000..0d961e6 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/visitors/VariableReportVisitor.java @@ -0,0 +1,54 @@ +package io.github.applecommander.bastokenizer.api.visitors; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import io.github.applecommander.bastokenizer.api.Visitor; +import io.github.applecommander.bastokenizer.api.model.Line; +import io.github.applecommander.bastokenizer.api.model.Program; +import io.github.applecommander.bastokenizer.api.model.Token; +import io.github.applecommander.bastokenizer.api.model.Token.Type; + +public class VariableReportVisitor implements Visitor { + private Map> refs = new HashMap<>(); + private int currentLineNumber = -1; + + @Override + public Program visit(Program program) { + Program p = Visitor.super.visit(program); + refs.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(this::print); + return p; + } + private void print(Map.Entry> e) { + System.out.printf("%-8s ", e.getKey()); + int c = 0; + for (int i : e.getValue()) { + if (c > 0) System.out.print(", "); + if (c > 0 && c % 10 == 0) System.out.printf("\n "); + System.out.print(i); + c += 1; + } + System.out.println(); + } + + @Override + public Line visit(Line line) { + currentLineNumber = line.lineNumber; + return Visitor.super.visit(line); + } + + @Override + public Token visit(Token token) { + if (token.type == Type.IDENT) { + refs.merge(token.text, + new TreeSet<>(Arrays.asList(currentLineNumber)), + (a,b) -> { a.addAll(b); return a; }); + } + return Visitor.super.visit(token); + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..a918b82 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,8 @@ +# Universal applesingle version number. Used for: +# - Naming JAR file. +# - The build will insert this into a file that is read at run time as well. +version=0.2.0 + +# Maven Central Repository G and A of GAV coordinate. :-) +group=net.sf.applecommander +archivesBaseName=bastokenizer diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..91ca28c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..16d2805 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml deleted file mode 100644 index c26c2c3..0000000 --- a/pom.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - 4.0.0 - net.sf.applecommander - bastokenizer - 0.2.0-SNAPSHOT - AppleSoft BASIC Tokenizer - Experiments with generating an AppleSoft B/BAS tokenized "binary". - - - 1.8 - 1.8 - UTF-8 - yyyy-MM-dd HH:mm - ${maven.build.timestamp} - - - - - info.picocli - picocli - [3.0,3.1) - - - net.sf.applecommander - applesingle-api - 1.1.0 - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.0.2 - - - - true - true - false - - - ${project.version} (${timestamp}) - - - - - - org.springframework.boot - spring-boot-maven-plugin - 2.0.0.RELEASE - - - - repackage - - - - - - - - \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..6b1328e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,7 @@ +include 'api' +include 'tools:bt' + +rootProject.name = 'bastokenizer' +project(":api").name = 'bastokenizer-api' +project(":tools").name = 'bastokenizer-tools' +project(":tools:bt").name = 'bastokenizer-tools-bt' diff --git a/src/main/java/com/webcodepro/applecommander/util/applesoft/Directive.java b/src/main/java/com/webcodepro/applecommander/util/applesoft/Directive.java deleted file mode 100644 index ba820b2..0000000 --- a/src/main/java/com/webcodepro/applecommander/util/applesoft/Directive.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.webcodepro.applecommander.util.applesoft; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.TreeMap; - -import com.webcodepro.applecommander.util.applesoft.Token.Type; - -import io.github.applecommander.bastokenizer.Main; -import io.github.applecommander.bastokenizer.Main.IntegerTypeConverter; - -public abstract class Directive { - private static Map> directives = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);; - static { - Directive.directives.put("$embed", EmbeddedBinaryDirective.class); - } - - private static IntegerTypeConverter integerConverter = new IntegerTypeConverter(); - - protected OutputStream outputStream; - protected List parameters = new ArrayList<>(); - - public static Directive find(String text, OutputStream outputStream) { - if (directives.containsKey(text)) { - try { - // Bypassing the constructor with arguments ... as that reduces code in the subclasses. - Directive directive = directives.get(text).newInstance(); - directive.setOutputStream(outputStream); - return directive; - } catch (InstantiationException | IllegalAccessException e) { - throw new IllegalArgumentException(String.format("Unable to construct directive '%s'", text), e); - } - } - throw new IllegalArgumentException(String.format("Unable to find directive '%s'", text)); - } - - public void setOutputStream(OutputStream outputStream) { - this.outputStream = outputStream; - } - - public void append(Token token) { - // Skip the commas... - if (token.type == Type.SYNTAX && ",".equals(token.text)) return; - parameters.add(token); - } - private Token require(Type... types) { - Token t = parameters.remove(0); - boolean matches = false; - for (Type type : types) { - matches |= type == t.type; - } - if (!matches) { - throw new IllegalArgumentException("Expecting a type of " + types); - } - return t; - } - protected String requiresString() { - Token t = require(Type.STRING); - return t.text; - } - protected int requiresInteger() { - Token t = require(Type.NUMBER, Type.STRING); - if (t.type == Type.NUMBER) { - return t.number.intValue(); - } - return integerConverter.convert(t.text); - } - - protected void ldy(int value) throws IOException { - outputStream.write(0xa0); - outputStream.write(value); - } - protected void jmp(int address) throws IOException { - outputStream.write(0x4c); - outputStream.write(address & 0xff); - outputStream.write(address >> 8); - } - protected void lda(int value) throws IOException { - outputStream.write(0xa9); - outputStream.write(value); - } - protected void sta(int address) throws IOException { - if ((address & 0xff00) == 0) { - outputStream.write(0x85); - outputStream.write(address); - } else { - throw new RuntimeException("sta does not handle 16 bit addresses yet!"); - } - } - protected void setAddress(int value, int address) throws IOException { - lda(value & 0xff); - sta(address); - lda(value >> 8); - sta(address+1); - } - - /** Write directive contents to output file. Note that address is adjusted for the line header already. */ - public abstract void writeBytes(int startAddress, Line line) throws IOException; - - public static class EmbeddedBinaryDirective extends Directive { - @Override - public void writeBytes(int startAddress, Line line) throws IOException { - if (parameters.size() != 2) { - throw new IllegalArgumentException("$embed requires a name and address parameter"); - } - String filename = requiresString(); - int targetAddress = requiresInteger(); - - File file = new File(Main.configuration.sourceFile.getParentFile(), filename); - byte[] bin = Files.readAllBytes(file.toPath()); - - Optional nextLine = line.nextLine(); - byte[] basicCode = nextLine.isPresent() - ? callAndGoto(startAddress,nextLine.get()) - : callAndReturn(startAddress); - - final int moveLength = 8*3 + 2 + 3; // LDA/STA, LDY, JMP. - int embeddedStart = startAddress + basicCode.length + moveLength; - int embeddedEnd = embeddedStart + bin.length; - - outputStream.write(basicCode); - setAddress(embeddedStart, 0x3c); - setAddress(embeddedEnd, 0x3e); - setAddress(targetAddress, 0x42); - ldy(0x00); - jmp(0xfe2c); - outputStream.write(bin); - } - // In program, "CALL
: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(); - } - } -} diff --git a/src/main/java/com/webcodepro/applecommander/util/applesoft/Visitors.java b/src/main/java/com/webcodepro/applecommander/util/applesoft/Visitors.java deleted file mode 100644 index 1168c25..0000000 --- a/src/main/java/com/webcodepro/applecommander/util/applesoft/Visitors.java +++ /dev/null @@ -1,472 +0,0 @@ -package com.webcodepro.applecommander.util.applesoft; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.SortedSet; -import java.util.Stack; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.function.Function; - -import com.webcodepro.applecommander.util.applesoft.Token.Type; - -/** - * This class presents all of the common Visitor implementations via builder patterns. - * The number is currently small enough that all the builders and visitors are defined - * in this one class. - * - * @author rob - */ -public class Visitors { - public static PrintBuilder printBuilder() { - return new PrintBuilder(); - } - public static class PrintBuilder { - private PrintStream printStream = System.out; - private Function creator = PrintVisitor::new; - - public PrintBuilder printStream(PrintStream printStream) { - Objects.requireNonNull(printStream); - this.printStream = printStream; - return this; - } - public PrintBuilder prettyPrint(boolean flag) { - creator = flag ? PrettyPrintVisitor::new : PrintVisitor::new; - return this; - } - public PrintBuilder prettyPrint() { - creator = PrettyPrintVisitor::new; - return this; - } - public PrintBuilder print() { - creator = PrintVisitor::new; - return this; - } - - public Visitor build() { - return creator.apply(this); - } - } - - public static ByteVisitor byteVisitor(int address) { - return new ByteVisitor(address); - } - - /** Rewrite the Program tree with the line number reassignments given. */ - public static ReassignmentVisitor reassignVisitor(Map reassignments) { - return new ReassignmentVisitor(reassignments); - } - - /** Collect all line numbers that are a target of GOTO, GOSUB, etc. */ - public static LineNumberTargetCollector lineNumberTargetCollector() { - return new LineNumberTargetCollector(); - } - - public static Visitor variableReportVisitor() { - return new VariableReportVisitor(); - } - - private static class PrettyPrintVisitor implements Visitor { - private PrintStream printStream; - - private PrettyPrintVisitor(PrintBuilder builder) { - this.printStream = builder.printStream; - } - - @Override - public Line visit(Line line) { - boolean first = true; - for (Statement statement : line.statements) { - if (first) { - first = false; - printStream.printf("%5d ", line.lineNumber); - } else { - printStream.printf("%5s ", ":"); - } - statement.accept(this); - printStream.println(); - } - return line; - } - @Override - public Token visit(Token token) { - switch (token.type) { - case EOL: - printStream.print(""); - break; - case COMMENT: - printStream.printf(" REM %s", token.text); - break; - case STRING: - printStream.printf("\"%s\"", token.text); - break; - case KEYWORD: - printStream.printf(" %s ", token.keyword.text); - break; - case IDENT: - case SYNTAX: - printStream.print(token.text); - break; - case DIRECTIVE: - printStream.printf("%s ", token.text); - break; - case NUMBER: - if (Math.rint(token.number) == token.number) { - printStream.print(token.number.intValue()); - } else { - printStream.print(token.number); - } - break; - } - return token; - } - } - - private static class PrintVisitor implements Visitor { - private PrintStream printStream; - - private PrintVisitor(PrintBuilder builder) { - this.printStream = builder.printStream; - } - - @Override - public Line visit(Line line) { - printStream.printf("%d ", line.lineNumber); - boolean first = true; - for (Statement statement : line.statements) { - if (first) { - first = false; - } else { - printStream.printf(":"); - } - statement.accept(this); - } - printStream.println(); - return line; - } - @Override - public Token visit(Token token) { - switch (token.type) { - case EOL: - printStream.print(""); - break; - case COMMENT: - printStream.printf("REM %s", token.text); - break; - case STRING: - printStream.printf("\"%s\"", token.text); - break; - case KEYWORD: - printStream.printf(" %s ", token.keyword.text); - break; - case IDENT: - case SYNTAX: - printStream.print(token.text); - break; - case DIRECTIVE: - printStream.printf("%s ", token.text); - break; - case NUMBER: - if (Math.rint(token.number) == token.number) { - printStream.print(token.number.intValue()); - } else { - printStream.print(token.number); - } - break; - } - return token; - } - } - - public static class ByteVisitor implements Visitor { - private Stack stack; - private Map lineAddresses; - private int address; - private Directive currentDirective; - - private ByteVisitor(int address) { - this.address = address; - this.stack = new Stack<>(); - this.lineAddresses = new TreeMap<>(); - } - - /** A convenience method to invoke {@link Program#accept(Visitor)} and {@link #getBytes()}. */ - public byte[] dump(Program program) { - program.accept(this); - return getBytes(); - } - - /** A convenience method to get the length of a line. */ - public int length(Line line) { - stack.push(new ByteArrayOutputStream()); - line.accept(this); - return stack.pop().size(); - } - - public Map getLineAddresses() { - return lineAddresses; - } - - public byte[] getBytes() { - if (stack.size() != 1) { - throw new RuntimeException("Error in processing internal BASIC model!"); - } - return stack.peek().toByteArray(); - } - - @Override - public Program visit(Program program) { - stack.clear(); - stack.push(new ByteArrayOutputStream()); - program.lines.forEach(line -> line.accept(this)); - ByteArrayOutputStream os = stack.peek(); - os.write(0x00); - os.write(0x00); - return program; - } - - @Override - public Line visit(Line line) { - try { - stack.push(new ByteArrayOutputStream()); - boolean first = true; - for (Statement statement : line.statements) { - if (currentDirective != null) { - throw new RuntimeException("No statements are allowed after a directive!"); - } - if (!first) { - stack.peek().write(':'); - } - first = false; - statement.accept(this); - } - if (currentDirective != null) { - currentDirective.writeBytes(this.address+4, line); - currentDirective = null; - } - - this.lineAddresses.put(line.lineNumber, this.address); - byte[] content = stack.pop().toByteArray(); - int nextAddress = address + content.length + 5; - ByteArrayOutputStream os = stack.peek(); - os.write(nextAddress); - os.write(nextAddress >> 8); - os.write(line.lineNumber); - os.write(line.lineNumber >> 8); - os.write(content); - os.write(0x00); - this.address = nextAddress; - return line; - } catch (IOException ex) { - // Hiding the IOException as ByteArrayOutputStream does not throw it - throw new RuntimeException(ex); - } - } - - @Override - public Token visit(Token token) { - if (currentDirective != null) { - currentDirective.append(token); - return token; - } - try { - ByteArrayOutputStream os = stack.peek(); - switch (token.type) { - case COMMENT: - os.write(ApplesoftKeyword.REM.code); - os.write(token.text.getBytes()); - break; - case EOL: - os.write(0x00); - break; - case IDENT: - os.write(token.text.getBytes()); - break; - case KEYWORD: - os.write(token.keyword.code); - break; - case DIRECTIVE: - currentDirective = Directive.find(token.text, os); - break; - case NUMBER: - if (Math.rint(token.number) == token.number) { - os.write(Integer.toString(token.number.intValue()).getBytes()); - } else { - os.write(Double.toString(token.number).getBytes()); - } - break; - case STRING: - os.write('"'); - os.write(token.text.getBytes()); - os.write('"'); - break; - case SYNTAX: - Optional opt = ApplesoftKeyword.find(token.text); - if (opt.isPresent()) { - os.write(opt.get().code); - } else { - os.write(token.text.getBytes()); - } - break; - } - return token; - } catch (IOException ex) { - // Hiding the IOException as ByteArrayOutputStream does not throw it - throw new RuntimeException(ex); - } - } - } - - /** This is a mildly rewritable Visitor. */ - private static class ReassignmentVisitor implements Visitor { - private Map reassignments; - private Program newProgram; - - private ReassignmentVisitor(Map reassignments) { - this.reassignments = reassignments; - } - - @Override - public Program visit(Program program) { - newProgram = new Program(); - program.lines.forEach(l -> { - Line line = l.accept(this); - newProgram.lines.add(line); - }); - return newProgram; - } - @Override - public Line visit(Line line) { - Line newLine = new Line(line.lineNumber, this.newProgram); - line.statements.forEach(s -> { - Statement statement = s.accept(this); - newLine.statements.add(statement); - }); - return newLine; - } - /** - * We saw a trigger, reassign any numbers that follow. - * - * Trigger cases: - * - GOSUB n - * - GOTO n - * - IF ... THEN n - * - LIST n [ ,m ] - * - ON x GOTO n, m, ... - * - ON x GOSUB n, m, ... - * - ONERR GOTO n - * - RUN n - */ - @Override - public Statement visit(Statement statement) { - boolean next = false; - boolean multiple = false; - Statement newStatement = new Statement(); - for (Token t : statement.tokens) { - Token newToken = t; - if (next) { - if (t.type == Type.NUMBER && reassignments.containsKey(t.number.intValue())) { - newToken = Token.number(t.line, reassignments.get(t.number.intValue()).doubleValue()); - } - next = multiple; // preserve next based on if we have multiple line numbers or not. - } else { - next = t.keyword == ApplesoftKeyword.GOSUB || t.keyword == ApplesoftKeyword.GOTO - || t.keyword == ApplesoftKeyword.THEN || t.keyword == ApplesoftKeyword.RUN - || t.keyword == ApplesoftKeyword.LIST; - multiple |= t.keyword == ApplesoftKeyword.LIST || t.keyword == ApplesoftKeyword.ON; - } - newStatement.tokens.add(newToken); - } - return newStatement; - } - } - - public static class LineNumberTargetCollector implements Visitor { - private Set targets = new TreeSet<>(); - - public Set getTargets() { - return targets; - } - - /** - * We saw a trigger, collect any numbers that follow. - * - * Trigger cases: - * - GOSUB n - * - GOTO n - * - IF ... THEN n - * - LIST n [ ,m ] - * - ON x GOTO n, m, ... - * - ON x GOSUB n, m, ... - * - ONERR GOTO n - * - RUN n - */ - @Override - public Statement visit(Statement statement) { - boolean next = false; - boolean multiple = false; - for (Token t : statement.tokens) { - if (next) { - if (t.type == Type.NUMBER) { - targets.add(t.number.intValue()); - } - next = multiple; // preserve next based on if we have multiple line numbers or not. - } else { - next = t.keyword == ApplesoftKeyword.GOSUB || t.keyword == ApplesoftKeyword.GOTO - || t.keyword == ApplesoftKeyword.THEN || t.keyword == ApplesoftKeyword.RUN - || t.keyword == ApplesoftKeyword.LIST; - multiple |= t.keyword == ApplesoftKeyword.LIST || t.keyword == ApplesoftKeyword.ON; - } - } - return statement; - } - } - - private static class VariableReportVisitor implements Visitor { - private Map> refs = new HashMap<>(); - private int currentLineNumber = -1; - - @Override - public Program visit(Program program) { - Program p = Visitor.super.visit(program); - refs.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .forEach(this::print); - return p; - } - private void print(Map.Entry> e) { - System.out.printf("%-8s ", e.getKey()); - int c = 0; - for (int i : e.getValue()) { - if (c > 0) System.out.print(", "); - if (c > 0 && c % 10 == 0) System.out.printf("\n "); - System.out.print(i); - c += 1; - } - System.out.println(); - } - - @Override - public Line visit(Line line) { - currentLineNumber = line.lineNumber; - return Visitor.super.visit(line); - } - - @Override - public Token visit(Token token) { - if (token.type == Type.IDENT) { - refs.merge(token.text, - new TreeSet<>(Arrays.asList(currentLineNumber)), - (a,b) -> { a.addAll(b); return a; }); - } - return Visitor.super.visit(token); - } - } -} diff --git a/src/main/java/com/webcodepro/applecommander/util/applesoft/package-info.java b/src/main/java/com/webcodepro/applecommander/util/applesoft/package-info.java deleted file mode 100644 index 2674183..0000000 --- a/src/main/java/com/webcodepro/applecommander/util/applesoft/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -/** - * This package is used by the AppleSoft text file import capabilities of AppleCommander. - * It is separate from the other components, primarily due to it being used on the input side, - * but also due to the fact that this is the third time AppleSoft tokens have been defined! - * - * @author rob - */ -package com.webcodepro.applecommander.util.applesoft; \ No newline at end of file diff --git a/src/main/java/io/github/applecommander/bastokenizer/Optimization.java b/src/main/java/io/github/applecommander/bastokenizer/Optimization.java deleted file mode 100644 index 5746679..0000000 --- a/src/main/java/io/github/applecommander/bastokenizer/Optimization.java +++ /dev/null @@ -1,190 +0,0 @@ -package io.github.applecommander.bastokenizer; - -import java.io.PrintStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; - -import com.webcodepro.applecommander.util.applesoft.ApplesoftKeyword; -import com.webcodepro.applecommander.util.applesoft.Line; -import com.webcodepro.applecommander.util.applesoft.Program; -import com.webcodepro.applecommander.util.applesoft.Statement; -import com.webcodepro.applecommander.util.applesoft.Token; -import com.webcodepro.applecommander.util.applesoft.Token.Type; -import com.webcodepro.applecommander.util.applesoft.Visitor; -import com.webcodepro.applecommander.util.applesoft.Visitors; -import com.webcodepro.applecommander.util.applesoft.Visitors.ByteVisitor; -import com.webcodepro.applecommander.util.applesoft.Visitors.LineNumberTargetCollector; - -import picocli.CommandLine.ITypeConverter; - -public enum Optimization { - REMOVE_EMPTY_STATEMENTS(opts -> new RemoveEmptyStatements()), - REMOVE_REM_STATEMENTS(opts -> new RemoveRemStatements()), - MERGE_LINES(opts -> new MergeLines(opts)), - RENUMBER(opts -> new Renumber()) - ; - - private Function factory; - - private Optimization(Function factory) { - this.factory = factory; - } - - public Visitor create(Main options) { - return factory.apply(options); - } - - /** Add support for lower-case Optimization flags. */ - public static class TypeConverter implements ITypeConverter { - @Override - public Optimization convert(String value) throws Exception { - try { - return Optimization.valueOf(value); - } catch (IllegalArgumentException ex) { - for (Optimization opt : Optimization.values()) { - String checkName = opt.name().replace('_', '-'); - if (checkName.equalsIgnoreCase(value)) { - return opt; - } - } - throw ex; - } - } - } - /** Common base class for optimization visitors that allow the program tree to be rewritten. */ - private static class BaseVisitor implements Visitor { - protected Map reassignments = new HashMap<>(); - protected Program newProgram; - @Override - public Program visit(Program program) { - newProgram = new Program(); - program.lines.forEach(l -> { - Line line = l.accept(this); - boolean lineKept = line != null && !line.statements.isEmpty(); - if (lineKept) { - newProgram.lines.add(line); - reassignments.replaceAll((k,v) -> v == null ? l.lineNumber : v); - } else { - // Make a place-holder for the reassignment; we'll patch it in once we find a line that sticks around. - reassignments.put(l.lineNumber, null); - } - }); - if (!reassignments.isEmpty()) { - // Now, renumber based on our findings! - return newProgram.accept(Visitors.reassignVisitor(reassignments)); - } else { - return newProgram; - } - } - @Override - public Line visit(Line line) { - Line newLine = new Line(line.lineNumber, this.newProgram); - line.statements.forEach(s -> { - Statement statement = s.accept(this); - if (statement != null) newLine.statements.add(statement); - }); - return newLine; - } - @Override - public Statement visit(Statement statement) { - Statement newStatement = new Statement(); - statement.tokens.forEach(t -> { - Token token = t.accept(this); - if (token != null) newStatement.tokens.add(token); - }); - return newStatement; - } - @Override - public Token visit(Token token) { - return token; - } - } - private static class RemoveEmptyStatements extends BaseVisitor { - @Override - public Statement visit(Statement statement) { - return statement.tokens.isEmpty() ? null : statement; - } - } - private static class RemoveRemStatements extends BaseVisitor { - @Override - public Statement visit(Statement statement) { - return statement.tokens.get(0).type == Type.COMMENT ? null : statement; - } - } - private static class MergeLines extends BaseVisitor { - private Set targets; - private Line mergeLine; - private ByteVisitor bv = Visitors.byteVisitor(0x801); - private int maxLineLength; - private PrintStream debug; - private MergeLines(Main options) { - this.maxLineLength = options.maxLineLength; - this.debug = options.debug; - } - @Override - public Program visit(Program program) { - LineNumberTargetCollector c = Visitors.lineNumberTargetCollector(); - program.accept(c); - targets = c.getTargets(); - debug.printf("Target lines = %s\n", targets); - return super.visit(program); - } - @Override - public Line visit(Line line) { - debug.printf("Line # %d : ", line.lineNumber); - Line newLine = new Line(line.lineNumber, this.newProgram); - newLine.statements.addAll(line.statements); - if (mergeLine == null || targets.contains(line.lineNumber)) { - // Either forced to a new line or this is a GOTO type target: Ignore length - debug.printf("%s\n", mergeLine == null ? "mergeLine is null" : "target line #"); - } else { - // Check length and decide if it merges based on that. - Line tmpLine = new Line(mergeLine.lineNumber, mergeLine.program); - tmpLine.statements.addAll(mergeLine.statements); - tmpLine.statements.addAll(line.statements); - if (bv.length(tmpLine) > maxLineLength) { - // It was too big, do not add - debug.printf("merge would exceed max line length: %d > %d\n", bv.length(tmpLine), maxLineLength); - } else { - // We can add line to mergeLine (mergeLine is already added to program, must keep that object) - mergeLine.statements.addAll(line.statements); - if (hasTerminal(line)) mergeLine = null; - debug.printf("line %s\n", mergeLine == null ? "had terminals" : "was added to mergeLine"); - return null; - } - } - // Always reset mergeLine based on the terminal characteristics - mergeLine = hasTerminal(line) ? null : newLine; - debug.printf("line %s\n", mergeLine == null ? "had terminals" : "is now mergeLine"); - return newLine; - } - private boolean hasTerminal(Line line) { - // Terminals are: IF, REM, GOTO, END, ON .. GOTO (GOTO is trigger), RESUME, RETURN, STOP - // Includes directives. - for (Statement s : line.statements) { - for (Token t : s.tokens) { - boolean terminal = t.keyword == ApplesoftKeyword.IF || t.type == Type.COMMENT /* REM */ - || t.keyword == ApplesoftKeyword.GOTO || t.keyword == ApplesoftKeyword.END - || t.keyword == ApplesoftKeyword.RESUME || t.keyword == ApplesoftKeyword.RETURN - || t.keyword == ApplesoftKeyword.STOP - || t.type == Type.DIRECTIVE; - if (terminal) return true; - } - } - return false; - } - } - private static class Renumber extends BaseVisitor { - protected int lineNumber = 0; - @Override - public Line visit(Line line) { - Line newLine = new Line(lineNumber++, this.newProgram); - newLine.statements.addAll(line.statements); - // Track what went where so lines can get renumbered automatically - reassignments.put(line.lineNumber, newLine.lineNumber); - return newLine; - } - } -} diff --git a/tools/bt/build.gradle b/tools/bt/build.gradle new file mode 100644 index 0000000..175e1c7 --- /dev/null +++ b/tools/bt/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'org.springframework.boot' version '2.0.2.RELEASE' +} + +repositories { + jcenter() +} + +apply plugin: 'application' + +mainClassName = "io.github.applecommander.bastokenizer.tools.bt.Main" + +bootJar { + manifest { + attributes( + 'Implementation-Title': 'bastokenizer', + 'Implementation-Version': "${version} (${new Date().format('yyyy-MM-dd HH:mm')})" + ) + } +} + +dependencies { + compile 'info.picocli:picocli:3.0.2' + compile project(':bastokenizer-api') +} diff --git a/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/HexDumper.java b/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/HexDumper.java new file mode 100644 index 0000000..d9aba0b --- /dev/null +++ b/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/HexDumper.java @@ -0,0 +1,71 @@ +package io.github.applecommander.bastokenizer.tools.bt; + +import java.io.PrintStream; +import java.util.Arrays; +import java.util.function.BiConsumer; + +/** A slightly-configurable reusable hex dumping mechanism. */ +public class HexDumper { + private PrintStream ps = System.out; + private int lineWidth = 16; + private BiConsumer printHeader; + private BiConsumer printLine; + + public static HexDumper standard() { + HexDumper hd = new HexDumper(); + hd.printHeader = hd::emptyHeader; + hd.printLine = hd::standardLine; + return hd; + } + public static HexDumper apple2() { + HexDumper hd = new HexDumper(); + hd.printHeader = hd::apple2Header; + hd.printLine = hd::apple2Line; + return hd; + } + + public void dump(int address, byte[] data) { + printHeader.accept(address, data.length); + int offset = 0; + while (offset < data.length) { + byte[] line = Arrays.copyOfRange(data, offset, Math.min(offset+lineWidth,data.length)); + printLine.accept(address+offset, line); + offset += line.length; + } + } + + public void emptyHeader(int address, int length) { + // Do Nothing + } + public void apple2Header(int address, int length) { + int end = address + length; + printLine.accept(0x67, new byte[] { (byte)(address&0xff), (byte)(address>>8), (byte)(end&0xff), (byte)(end>>8) }); + printLine.accept(address-1, new byte[] { 0x00 }); + } + + public void standardLine(int address, byte[] data) { + ps.printf("%04x: ", address); + for (int i=0; i= ' ') ? (char)b : '.'; + } + ps.printf("%c", ch); + } + ps.printf("\n"); + } + public void apple2Line(int address, byte[] data) { + ps.printf("%04X:", address); + for (byte b : data) ps.printf("%02X ", b); + ps.printf("\n"); + } +} diff --git a/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/IntegerTypeConverter.java b/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/IntegerTypeConverter.java new file mode 100644 index 0000000..f9dedc6 --- /dev/null +++ b/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/IntegerTypeConverter.java @@ -0,0 +1,12 @@ +package io.github.applecommander.bastokenizer.tools.bt; + +import io.github.applecommander.bastokenizer.api.utils.Converters; +import picocli.CommandLine.ITypeConverter; + +/** Add support for "$801" and "0x801" instead of just decimal like 2049. */ +public class IntegerTypeConverter implements ITypeConverter { + @Override + public Integer convert(String value) { + return Converters.toInteger(value); + } +} diff --git a/src/main/java/io/github/applecommander/bastokenizer/Main.java b/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/Main.java similarity index 55% rename from src/main/java/io/github/applecommander/bastokenizer/Main.java rename to tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/Main.java index f029311..2e53c48 100644 --- a/src/main/java/io/github/applecommander/bastokenizer/Main.java +++ b/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/Main.java @@ -1,4 +1,4 @@ -package io.github.applecommander.bastokenizer; +package io.github.applecommander.bastokenizer.tools.bt; import java.io.File; import java.io.FileNotFoundException; @@ -11,22 +11,20 @@ import java.util.Arrays; import java.util.List; import java.util.Queue; import java.util.concurrent.Callable; -import java.util.function.BiConsumer; - -import com.webcodepro.applecommander.util.applesoft.Parser; -import com.webcodepro.applecommander.util.applesoft.Program; -import com.webcodepro.applecommander.util.applesoft.Token; -import com.webcodepro.applecommander.util.applesoft.Token.Type; -import com.webcodepro.applecommander.util.applesoft.TokenReader; -import com.webcodepro.applecommander.util.applesoft.Visitors; -import com.webcodepro.applecommander.util.applesoft.Visitors.ByteVisitor; import io.github.applecommander.applesingle.AppleSingle; +import io.github.applecommander.bastokenizer.api.Configuration; +import io.github.applecommander.bastokenizer.api.Optimization; +import io.github.applecommander.bastokenizer.api.Parser; +import io.github.applecommander.bastokenizer.api.TokenReader; +import io.github.applecommander.bastokenizer.api.Visitors; +import io.github.applecommander.bastokenizer.api.model.Program; +import io.github.applecommander.bastokenizer.api.model.Token; +import io.github.applecommander.bastokenizer.api.model.Token.Type; +import io.github.applecommander.bastokenizer.api.visitors.ByteVisitor; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Help.Visibility; -import picocli.CommandLine.ITypeConverter; -import picocli.CommandLine.IVersionProvider; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; @@ -36,62 +34,61 @@ import picocli.CommandLine.Parameters; commandListHeading = "%nCommands:%n", optionListHeading = "%nOptions:%n", name = "bt", mixinStandardHelpOptions = true, - versionProvider = Main.VersionProvider.class) + versionProvider = VersionProvider.class) public class Main implements Callable { private static final int BAS = 0xfc; - public static Configuration configuration = new Configuration(); @Option(names = { "-o", "--output" }, description = "Write binary output to file.") - File outputFile; + private File outputFile; @Option(names = { "-x", "--hex"}, description = "Generate a binary hex dump for debugging.") - boolean hexFormat; + private boolean hexFormat; @Option(names = { "-c", "--copy"}, description = "Generate a copy/paste form of output for testing in an emulator.") - boolean copyFormat; + private boolean copyFormat; @Option(names = { "-a", "--address" }, description = "Base address for program", showDefaultValue = Visibility.ALWAYS, converter = IntegerTypeConverter.class) - int address = 0x801; + private int address = 0x801; @Option(names = { "--variables" }, description = "Generate a variable report") - boolean showVariableReport; + private boolean showVariableReport; @Option(names = "--stdout", description = "Send binary output to stdout.") - boolean stdoutFlag; + private boolean stdoutFlag; @Option(names = "--applesingle", description = "Write output in AppleSingle format") - boolean applesingleFlag; + private boolean applesingleFlag; @Option(names = "--pretty", description = "Pretty print structure as bastokenizer understands it.") - boolean prettyPrint; + private boolean prettyPrint; @Option(names = "--list", description = "List structure as bastokenizer understands it.") - boolean listPrint; + private boolean listPrint; @Option(names = "--tokens", description = "Dump token list to stdout for debugging.") - boolean showTokens; + private boolean showTokens; @Option(names = "--addresses", description = "Dump line number addresses out.") - boolean showLineAddresses; + private boolean showLineAddresses; @Option(names = "--max-line-length", description = "Maximum line length for generated lines.", showDefaultValue = Visibility.ALWAYS) - int maxLineLength = 255; + private int maxLineLength = 255; - @Option(names = "-f", converter = Optimization.TypeConverter.class, split = ",", description = { + @Option(names = "-f", converter = OptimizationTypeConverter.class, split = ",", description = { "Enable specific optimizations.", "* @|green remove-empty-statements|@ - Strip out all '::'-like statements.", "* @|green remove-rem-statements|@ - Remove all REM statements.", "* @|green merge-lines|@ - Merge lines.", "* @|green renumber|@ - Renumber program." }) - List optimizations = new ArrayList<>(); + private List optimizations = new ArrayList<>(); @Option(names = { "-O", "--optimize" }, description = "Apply all optimizations.") - boolean allOptimizations; + private boolean allOptimizations; @Option(names = "--debug", description = "Print debug output.") - boolean debugFlag; - PrintStream debug = new PrintStream(new OutputStream() { + private boolean debugFlag; + private PrintStream debug = new PrintStream(new OutputStream() { @Override public void write(int b) throws IOException { // Do nothing @@ -99,7 +96,7 @@ public class Main implements Callable { }); @Parameters(index = "0", description = "AppleSoft BASIC program to process.") - File sourceFile; + private File sourceFile; public static void main(String[] args) throws FileNotFoundException, IOException { CommandLine.call(new Main(), args); @@ -108,9 +105,12 @@ public class Main implements Callable { @Override public Void call() throws FileNotFoundException, IOException { if (checkParameters()) { - if (debugFlag) debug = System.out; - configuration.sourceFile = this.sourceFile; - process(); + Configuration.Builder builder = Configuration.builder() + .maxLineLength(this.maxLineLength) + .sourceFile(this.sourceFile) + .startAddress(this.address); + if (debugFlag) builder.debugStream(System.out); + process(builder.build()); } return null; // To satisfy object "Void" @@ -135,7 +135,7 @@ public class Main implements Callable { } /** General CLI processing. */ - public void process() throws FileNotFoundException, IOException { + public void process(Configuration config) throws FileNotFoundException, IOException { Queue tokens = TokenReader.tokenize(sourceFile); if (showTokens) { tokens.forEach(t -> System.out.printf("%s%s", t, t.type == Type.EOL ? "\n" : ", ")); @@ -145,7 +145,7 @@ public class Main implements Callable { for (Optimization optimization : optimizations) { debug.printf("Optimization: %s\n", optimization.name()); - program = program.accept(optimization.create(this)); + program = program.accept(optimization.create(config)); } if (prettyPrint || listPrint) { @@ -155,7 +155,7 @@ public class Main implements Callable { program.accept(Visitors.variableReportVisitor()); } - ByteVisitor byteVisitor = Visitors.byteVisitor(address); + ByteVisitor byteVisitor = Visitors.byteVisitor(config); byte[] data = byteVisitor.dump(program); if (showLineAddresses) { byteVisitor.getLineAddresses().forEach((l,a) -> System.out.printf("%5d ... $%04x\n", l, a)); @@ -204,96 +204,4 @@ public class Main implements Callable { } } } - - /** A slightly-configurable reusable hex dumping mechanism. */ - public static class HexDumper { - private PrintStream ps = System.out; - private int lineWidth = 16; - private BiConsumer printHeader; - private BiConsumer printLine; - - public static HexDumper standard() { - HexDumper hd = new HexDumper(); - hd.printHeader = hd::emptyHeader; - hd.printLine = hd::standardLine; - return hd; - } - public static HexDumper apple2() { - HexDumper hd = new HexDumper(); - hd.printHeader = hd::apple2Header; - hd.printLine = hd::apple2Line; - return hd; - } - - public void dump(int address, byte[] data) { - printHeader.accept(address, data.length); - int offset = 0; - while (offset < data.length) { - byte[] line = Arrays.copyOfRange(data, offset, Math.min(offset+lineWidth,data.length)); - printLine.accept(address+offset, line); - offset += line.length; - } - } - - public void emptyHeader(int address, int length) { - // Do Nothing - } - public void apple2Header(int address, int length) { - int end = address + length; - printLine.accept(0x67, new byte[] { (byte)(address&0xff), (byte)(address>>8), (byte)(end&0xff), (byte)(end>>8) }); - printLine.accept(address-1, new byte[] { 0x00 }); - } - - public void standardLine(int address, byte[] data) { - ps.printf("%04x: ", address); - for (int i=0; i= ' ') ? (char)b : '.'; - } - ps.printf("%c", ch); - } - ps.printf("\n"); - } - public void apple2Line(int address, byte[] data) { - ps.printf("%04X:", address); - for (byte b : data) ps.printf("%02X ", b); - ps.printf("\n"); - } - } - - /** Display version information. Note that this is dependent on Maven configuration. */ - public static class VersionProvider implements IVersionProvider { - public String[] getVersion() { - return new String[] { Main.class.getPackage().getImplementationVersion() }; - } - } - /** Add support for "$801" and "0x801" instead of just decimal like 2049. */ - public static class IntegerTypeConverter implements ITypeConverter { - @Override - public Integer convert(String value) { - if (value == null) { - return null; - } else if (value.startsWith("$")) { - return Integer.valueOf(value.substring(1), 16); - } else if (value.startsWith("0x") || value.startsWith("0X")) { - return Integer.valueOf(value.substring(2), 16); - } else { - return Integer.valueOf(value); - } - } - } - /** Expose configuration details for other components to see. */ - public static class Configuration { - public File sourceFile; - } } diff --git a/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/OptimizationTypeConverter.java b/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/OptimizationTypeConverter.java new file mode 100644 index 0000000..28854f5 --- /dev/null +++ b/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/OptimizationTypeConverter.java @@ -0,0 +1,22 @@ +package io.github.applecommander.bastokenizer.tools.bt; + +import io.github.applecommander.bastokenizer.api.Optimization; +import picocli.CommandLine.ITypeConverter; + +/** Add support for lower-case Optimization flags. */ +public class OptimizationTypeConverter implements ITypeConverter { + @Override + public Optimization convert(String value) throws Exception { + try { + return Optimization.valueOf(value); + } catch (IllegalArgumentException ex) { + for (Optimization opt : Optimization.values()) { + String checkName = opt.name().replace('_', '-'); + if (checkName.equalsIgnoreCase(value)) { + return opt; + } + } + throw ex; + } + } +} diff --git a/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/VersionProvider.java b/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/VersionProvider.java new file mode 100644 index 0000000..2f18208 --- /dev/null +++ b/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/VersionProvider.java @@ -0,0 +1,10 @@ +package io.github.applecommander.bastokenizer.tools.bt; + +import picocli.CommandLine.IVersionProvider; + +/** Display version information. Note that this is dependent on Gradle configuration. */ +public class VersionProvider implements IVersionProvider { + public String[] getVersion() { + return new String[] { Main.class.getPackage().getImplementationVersion() }; + } +} diff --git a/src/test/resources/alien-drizzle.bas b/tools/bt/src/test/resources/alien-drizzle.bas similarity index 100% rename from src/test/resources/alien-drizzle.bas rename to tools/bt/src/test/resources/alien-drizzle.bas diff --git a/src/test/resources/circles-timing.bas b/tools/bt/src/test/resources/circles-timing.bas similarity index 100% rename from src/test/resources/circles-timing.bas rename to tools/bt/src/test/resources/circles-timing.bas diff --git a/src/test/resources/circles.bas b/tools/bt/src/test/resources/circles.bas similarity index 100% rename from src/test/resources/circles.bas rename to tools/bt/src/test/resources/circles.bas diff --git a/src/test/resources/escape-from-monster-caverns.bas b/tools/bt/src/test/resources/escape-from-monster-caverns.bas similarity index 100% rename from src/test/resources/escape-from-monster-caverns.bas rename to tools/bt/src/test/resources/escape-from-monster-caverns.bas diff --git a/src/test/resources/gravestone.bas b/tools/bt/src/test/resources/gravestone.bas similarity index 100% rename from src/test/resources/gravestone.bas rename to tools/bt/src/test/resources/gravestone.bas diff --git a/src/test/resources/jewel-draw1.bas b/tools/bt/src/test/resources/jewel-draw1.bas similarity index 100% rename from src/test/resources/jewel-draw1.bas rename to tools/bt/src/test/resources/jewel-draw1.bas diff --git a/src/test/resources/jewel-draw2.bas b/tools/bt/src/test/resources/jewel-draw2.bas similarity index 100% rename from src/test/resources/jewel-draw2.bas rename to tools/bt/src/test/resources/jewel-draw2.bas diff --git a/src/test/resources/rem-renumber.bas b/tools/bt/src/test/resources/rem-renumber.bas similarity index 100% rename from src/test/resources/rem-renumber.bas rename to tools/bt/src/test/resources/rem-renumber.bas diff --git a/src/test/resources/route6502.bas b/tools/bt/src/test/resources/route6502.bas similarity index 100% rename from src/test/resources/route6502.bas rename to tools/bt/src/test/resources/route6502.bas diff --git a/src/test/resources/test.bas b/tools/bt/src/test/resources/test.bas similarity index 100% rename from src/test/resources/test.bas rename to tools/bt/src/test/resources/test.bas