First draft of AppleSoft BASIC tokenizer for 'ac'. #23.
This commit is contained in:
parent
275a0b8b68
commit
f3cecfd415
|
@ -5,6 +5,7 @@ build/
|
||||||
|
|
||||||
# In case we run it locally, AppleCommander files we don't want to commit
|
# In case we run it locally, AppleCommander files we don't want to commit
|
||||||
AppleCommander.preferences
|
AppleCommander.preferences
|
||||||
|
*.bas
|
||||||
|
|
||||||
# Ignoring all disk images
|
# Ignoring all disk images
|
||||||
*.dsk
|
*.dsk
|
||||||
|
|
|
@ -30,7 +30,9 @@ import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
import com.webcodepro.applecommander.storage.DirectoryEntry;
|
import com.webcodepro.applecommander.storage.DirectoryEntry;
|
||||||
import com.webcodepro.applecommander.storage.Disk;
|
import com.webcodepro.applecommander.storage.Disk;
|
||||||
|
@ -53,6 +55,10 @@ import com.webcodepro.applecommander.util.AppleSingle.ProdosFileInfo;
|
||||||
import com.webcodepro.applecommander.util.AppleUtil;
|
import com.webcodepro.applecommander.util.AppleUtil;
|
||||||
import com.webcodepro.applecommander.util.StreamUtil;
|
import com.webcodepro.applecommander.util.StreamUtil;
|
||||||
import com.webcodepro.applecommander.util.TextBundle;
|
import com.webcodepro.applecommander.util.TextBundle;
|
||||||
|
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.TokenReader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ac provides a command line interface to key AppleCommander functions. Text
|
* ac provides a command line interface to key AppleCommander functions. Text
|
||||||
|
@ -90,6 +96,8 @@ import com.webcodepro.applecommander.util.TextBundle;
|
||||||
* -pas800 <imagename> <volname> create an 800K Pascal image.
|
* -pas800 <imagename> <volname> create an 800K Pascal image.
|
||||||
* -convert <filename> <imagename> uncompress a ShrinkIt file or disk image
|
* -convert <filename> <imagename> uncompress a ShrinkIt file or disk image
|
||||||
* or convert a DiskCopy 4.2 image into a ProDOS disk image.
|
* or convert a DiskCopy 4.2 image into a ProDOS disk image.
|
||||||
|
* -bas <imagename> <filename> import an AppleSoft basic file from text
|
||||||
|
* back to it's tokenized format.
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* @author John B. Matthews
|
* @author John B. Matthews
|
||||||
|
@ -155,6 +163,8 @@ public class ac {
|
||||||
convert(args[1], args[2], Integer.parseInt(args[3]));
|
convert(args[1], args[2], Integer.parseInt(args[3]));
|
||||||
else
|
else
|
||||||
convert(args[1], args[2]);
|
convert(args[1], args[2]);
|
||||||
|
} else if ("-bas".equalsIgnoreCase(args[0])) {
|
||||||
|
putAppleSoft(args[1], args[2]);
|
||||||
} else {
|
} else {
|
||||||
help();
|
help();
|
||||||
}
|
}
|
||||||
|
@ -165,6 +175,44 @@ public class ac {
|
||||||
help();
|
help();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the AppleSoft BASIC program from text into it's "native" tokenized format.
|
||||||
|
* Note that we try to infer the BASIC type dynamically and hard-code the start address
|
||||||
|
* to 0x801.
|
||||||
|
*/
|
||||||
|
public static void putAppleSoft(String imageName, String fileName) throws IOException, DiskException {
|
||||||
|
Queue<Token> tokens = TokenReader.tokenize(System.in);
|
||||||
|
Parser parser = new Parser(tokens);
|
||||||
|
Program program = parser.parse();
|
||||||
|
int address = 0x801;
|
||||||
|
byte[] data = program.toBytes(address);
|
||||||
|
|
||||||
|
Name name = new Name(fileName);
|
||||||
|
File file = new File(imageName);
|
||||||
|
if (!file.canRead()){
|
||||||
|
throw new IOException("Unable to read input file named "+imageName+".");
|
||||||
|
}
|
||||||
|
|
||||||
|
Disk disk = new Disk(imageName);
|
||||||
|
FormattedDisk[] formattedDisks = disk.getFormattedDisks();
|
||||||
|
FormattedDisk formattedDisk = formattedDisks[0];
|
||||||
|
// Look through the supplied types and try to pick AppleSoft. Otherwise, let's try "A".
|
||||||
|
String fileType = Arrays.asList(formattedDisk.getFiletypes()).stream()
|
||||||
|
.filter(ft -> "A".equalsIgnoreCase(ft) || "BAS".equalsIgnoreCase(ft))
|
||||||
|
.findFirst()
|
||||||
|
.orElse("A");
|
||||||
|
FileEntry entry = name.createEntry(formattedDisk);
|
||||||
|
if (entry != null) {
|
||||||
|
entry.setFiletype(fileType);
|
||||||
|
entry.setFilename(name.name);
|
||||||
|
entry.setFileData(data);
|
||||||
|
if (entry.needsAddress()) {
|
||||||
|
entry.setAddress(address);
|
||||||
|
}
|
||||||
|
formattedDisk.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Put fileName from the local filesytem into the file named fileOnImageName on the disk named imageName;
|
* Put fileName from the local filesytem into the file named fileOnImageName on the disk named imageName;
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
package com.webcodepro.applecommander.util.applesoft;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.StreamTokenizer;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/** All elements of AppleSoft that are tokenized in some manner. "Keyword" was picked as it is not the word token. ;-) */
|
||||||
|
public enum ApplesoftKeyword {
|
||||||
|
END(0x80, "END"),
|
||||||
|
FOR(0x81, "FOR"),
|
||||||
|
NEXT(0x82, "NEXT"),
|
||||||
|
DATA(0x83, "DATA"),
|
||||||
|
INPUT(0x84, "INPUT"),
|
||||||
|
DEL(0x85, "DEL"),
|
||||||
|
DIM(0x86, "DIM"),
|
||||||
|
READ(0x87, "READ"),
|
||||||
|
GR(0x88, "GR"),
|
||||||
|
TEXT(0x89, "TEXT"),
|
||||||
|
PR(0x8A, "PR#"),
|
||||||
|
IN(0x8B, "IN#"),
|
||||||
|
CALL(0x8C, "CALL"),
|
||||||
|
PLOT(0x8D, "PLOT"),
|
||||||
|
HLIN(0x8E, "HLIN"),
|
||||||
|
VLIN(0x8F, "VLIN"),
|
||||||
|
HGR2(0x90, "HGR2"),
|
||||||
|
HGR(0x91, "HGR"),
|
||||||
|
HCOLOR(0x92, "HCOLOR="),
|
||||||
|
HPLOT(0x93, "HPLOT"),
|
||||||
|
DRAW(0x94, "DRAW"),
|
||||||
|
XDRAW(0x95, "XDRAW"),
|
||||||
|
HTAB(0x96, "HTAB"),
|
||||||
|
HOME(0x97, "HOME"),
|
||||||
|
ROT(0x98, "ROT="),
|
||||||
|
SCALE(0x99, "SCALE="),
|
||||||
|
SHLOAD(0x9A, "SHLOAD"),
|
||||||
|
TRACE(0x9B, "TRACE"),
|
||||||
|
NOTRACE(0x9C, "NOTRACE"),
|
||||||
|
NORMAL(0x9D, "NORMAL"),
|
||||||
|
INVERSE(0x9E, "INVERSE"),
|
||||||
|
FLASH(0x9F, "FLASH"),
|
||||||
|
COLOR(0xA0, "COLOR="),
|
||||||
|
POP(0xA1, "POP"),
|
||||||
|
VTAB(0xA2, "VTAB"),
|
||||||
|
HIMEM(0xA3, "HIMEM:"),
|
||||||
|
LOMEM(0xA4, "LOMEM:"),
|
||||||
|
ONERR(0xA5, "ONERR"),
|
||||||
|
RESUME(0xA6, "RESUME"),
|
||||||
|
RECALL(0xA7, "RECALL"),
|
||||||
|
STORE(0xA8, "STORE"),
|
||||||
|
SPEED(0xA9, "SPEED="),
|
||||||
|
LET(0xAA, "LET"),
|
||||||
|
GOTO(0xAB, "GOTO"),
|
||||||
|
RUN(0xAC, "RUN"),
|
||||||
|
IF(0xAD, "IF"),
|
||||||
|
RESTORE(0xAE, "RESTORE"),
|
||||||
|
amp(0xAF, "&"),
|
||||||
|
GOSUB(0xB0, "GOSUB"),
|
||||||
|
RETURN(0xB1, "RETURN"),
|
||||||
|
REM(0xB2, "REM"),
|
||||||
|
STOP(0xB3, "STOP"),
|
||||||
|
ON(0xB4, "ON"),
|
||||||
|
WAIT(0xB5, "WAIT"),
|
||||||
|
LOAD(0xB6, "LOAD"),
|
||||||
|
SAVE(0xB7, "SAVE"),
|
||||||
|
DEF(0xB8, "DEF"),
|
||||||
|
POKE(0xB9, "POKE"),
|
||||||
|
PRINT(0xBA, "PRINT"),
|
||||||
|
CONT(0xBB, "CONT"),
|
||||||
|
LIST(0xBC, "LIST"),
|
||||||
|
CLEAR(0xBD, "CLEAR"),
|
||||||
|
GET(0xBE, "GET"),
|
||||||
|
NEW(0xBF, "NEW"),
|
||||||
|
TAB(0xC0, "TAB("),
|
||||||
|
TO(0xC1, "TO"),
|
||||||
|
FN(0xC2, "FN"),
|
||||||
|
SPC(0xC3, "SPC("),
|
||||||
|
THEN(0xC4, "THEN"),
|
||||||
|
AT(0xC5, "AT"),
|
||||||
|
NOT(0xC6, "NOT"),
|
||||||
|
STEP(0xC7, "STEP"),
|
||||||
|
add(0xC8, "+"),
|
||||||
|
sub(0xC9, "-"),
|
||||||
|
mul(0xCA, "*"),
|
||||||
|
div(0xCB, "/"),
|
||||||
|
pow(0xCC, "^"),
|
||||||
|
AND(0xCD, "AND"),
|
||||||
|
OR(0xCE, "OR"),
|
||||||
|
gt(0xCF, ">"),
|
||||||
|
eq(0xD0, "="),
|
||||||
|
lt(0xD1, "<"),
|
||||||
|
SGN(0xD2, "SGN"),
|
||||||
|
INT(0xD3, "INT"),
|
||||||
|
ABS(0xD4, "ABS"),
|
||||||
|
USR(0xD5, "USR"),
|
||||||
|
FRE(0xD6, "FRE"),
|
||||||
|
SCRN(0xD7, "SCRN("),
|
||||||
|
PDL(0xD8, "PDL"),
|
||||||
|
POS(0xD9, "POS"),
|
||||||
|
SQR(0xDA, "SQR"),
|
||||||
|
RND(0xDB, "RND"),
|
||||||
|
LOG(0xDC, "LOG"),
|
||||||
|
EXP(0xDD, "EXP"),
|
||||||
|
COS(0xDE, "COS"),
|
||||||
|
SIN(0xDF, "SIN"),
|
||||||
|
TAN(0xE0, "TAN"),
|
||||||
|
ATN(0xE1, "ATN"),
|
||||||
|
PEEK(0xE2, "PEEK"),
|
||||||
|
LEN(0xE3, "LEN"),
|
||||||
|
STR(0xE4, "STR$"),
|
||||||
|
VAL(0xE5, "VAL"),
|
||||||
|
ASC(0xE6, "ASC"),
|
||||||
|
CHR(0xE7, "CHR$"),
|
||||||
|
LEFT(0xE8, "LEFT$"),
|
||||||
|
RIGHT(0xE9, "RIGHT$"),
|
||||||
|
MID(0xEA, "MID$");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AppleSoft token value. Token is overloaded, so "code" is good enough.
|
||||||
|
*/
|
||||||
|
public final int code;
|
||||||
|
/**
|
||||||
|
* Full text of the token.
|
||||||
|
*/
|
||||||
|
public final String text;
|
||||||
|
/**
|
||||||
|
* Token parts as seen by the StreamTokenizer.
|
||||||
|
*/
|
||||||
|
public final List<String> parts;
|
||||||
|
/**
|
||||||
|
* Indicates that this needs _just_ a closing right parenthesis since the
|
||||||
|
* opening left parenthesis is included in the token
|
||||||
|
*/
|
||||||
|
public boolean needsRParen;
|
||||||
|
|
||||||
|
private ApplesoftKeyword(int code, String text) {
|
||||||
|
this.code = code;
|
||||||
|
this.text = text;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// A bit brute-force, but should always match the tokenizer configuration!
|
||||||
|
List<String> list = new ArrayList<>();
|
||||||
|
StreamTokenizer t = tokenizer(new StringReader(text));
|
||||||
|
while (t.nextToken() != StreamTokenizer.TT_EOF) {
|
||||||
|
switch (t.ttype) {
|
||||||
|
case StreamTokenizer.TT_WORD:
|
||||||
|
list.add(t.sval);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
list.add(String.format("%c", t.ttype));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.parts = Collections.unmodifiableList(list);
|
||||||
|
this.needsRParen = parts.contains("(");
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equalsIgnoreCase(String value) {
|
||||||
|
return this.text.equalsIgnoreCase(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("%s (%02x)", text, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Utility method to create a shared definition for AppleSoft file parsing. */
|
||||||
|
public static StreamTokenizer tokenizer(Reader r) {
|
||||||
|
StreamTokenizer tokenizer = new StreamTokenizer(r);
|
||||||
|
tokenizer.resetSyntax();
|
||||||
|
tokenizer.wordChars('a', 'z');
|
||||||
|
tokenizer.wordChars('A', 'Z');
|
||||||
|
tokenizer.wordChars(128 + 32, 255);
|
||||||
|
tokenizer.whitespaceChars(0, ' ');
|
||||||
|
tokenizer.quoteChar('"');
|
||||||
|
tokenizer.parseNumbers();
|
||||||
|
// This resets part of parseNumbers to match AppleSoft tokenization!
|
||||||
|
tokenizer.ordinaryChar('-');
|
||||||
|
tokenizer.eolIsSignificant(true);
|
||||||
|
return tokenizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Utility method to locate a keyword ignoring case. */
|
||||||
|
public static Optional<ApplesoftKeyword> find(String value) {
|
||||||
|
Objects.requireNonNull(value);
|
||||||
|
for (ApplesoftKeyword kw : values()) {
|
||||||
|
if (value.equalsIgnoreCase(kw.parts.get(0))) {
|
||||||
|
return Optional.of(kw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.webcodepro.applecommander.util.applesoft;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** An AppleSoft BASIC Line representation. */
|
||||||
|
public class Line {
|
||||||
|
public final int lineNumber;
|
||||||
|
public final List<Statement> statements = new ArrayList<>();
|
||||||
|
|
||||||
|
public Line(int lineNumber) {
|
||||||
|
this.lineNumber = lineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prettyPrint(PrintStream ps) {
|
||||||
|
boolean first = true;
|
||||||
|
for (Statement statement : statements) {
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
ps.printf("%5d ", lineNumber);
|
||||||
|
} else {
|
||||||
|
ps.printf("%5s ", ":");
|
||||||
|
}
|
||||||
|
statement.prettyPrint(ps);
|
||||||
|
ps.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int toBytes(int address, ByteArrayOutputStream os) throws IOException {
|
||||||
|
ByteArrayOutputStream tmp = new ByteArrayOutputStream();
|
||||||
|
for (Statement stmt : statements) {
|
||||||
|
if (tmp.size() > 0) tmp.write(':');
|
||||||
|
stmt.toBytes(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextAddress = address + tmp.size() + 5;
|
||||||
|
os.write(nextAddress);
|
||||||
|
os.write(nextAddress >> 8);
|
||||||
|
os.write(lineNumber);
|
||||||
|
os.write(lineNumber >> 8);
|
||||||
|
tmp.writeTo(os);
|
||||||
|
os.write(0x00);
|
||||||
|
return nextAddress;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package com.webcodepro.applecommander.util.applesoft;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
|
import com.webcodepro.applecommander.util.applesoft.Token.Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Parser will read a series of Tokens and build a Program.
|
||||||
|
* Note that this is not a compiler and does not "understand" the program.
|
||||||
|
*/
|
||||||
|
public class Parser {
|
||||||
|
private final Queue<Token> tokens;
|
||||||
|
|
||||||
|
public Parser(Queue<Token> tokens) {
|
||||||
|
Objects.requireNonNull(tokens);
|
||||||
|
this.tokens = tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Program parse() {
|
||||||
|
Program program = new Program();
|
||||||
|
while (!tokens.isEmpty()) {
|
||||||
|
Line line = readLine();
|
||||||
|
program.lines.add(line);
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Line readLine() {
|
||||||
|
Line line = new Line(expectNumber());
|
||||||
|
while (!tokens.isEmpty() && tokens.peek().type != Type.EOL) {
|
||||||
|
Statement statement = readStatement();
|
||||||
|
if (statement != null) {
|
||||||
|
line.statements.add(statement);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tokens.isEmpty() && tokens.peek().type == Type.EOL) {
|
||||||
|
tokens.remove(); // Skip that EOL
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Statement readStatement() {
|
||||||
|
Statement statement = new Statement();
|
||||||
|
while (!tokens.isEmpty()) {
|
||||||
|
if (tokens.peek().type == Type.EOL) break;
|
||||||
|
Token t = tokens.remove();
|
||||||
|
if (":".equals(t.text)) break;
|
||||||
|
statement.tokens.add(t);
|
||||||
|
}
|
||||||
|
return statement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int expectNumber() {
|
||||||
|
Token c = tokens.remove();
|
||||||
|
if (c.type != Type.NUMBER) {
|
||||||
|
throw new RuntimeException("Expected a number in line #" + c.line);
|
||||||
|
}
|
||||||
|
return c.number.intValue();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.webcodepro.applecommander.util.applesoft;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** A Program is a series of lines. */
|
||||||
|
public class Program {
|
||||||
|
public final List<Line> lines = new ArrayList<>();
|
||||||
|
|
||||||
|
public void prettyPrint(PrintStream ps) {
|
||||||
|
for (Line line : lines) {
|
||||||
|
line.prettyPrint(ps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] toBytes(int address) throws IOException {
|
||||||
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
for (Line line : lines) address = line.toBytes(address, os);
|
||||||
|
os.write(0x00);
|
||||||
|
os.write(0x00);
|
||||||
|
return os.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.webcodepro.applecommander.util.applesoft;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** A Statement is simply a series of Tokens. */
|
||||||
|
public class Statement {
|
||||||
|
public final List<Token> tokens = new ArrayList<>();
|
||||||
|
|
||||||
|
public void prettyPrint(PrintStream ps) {
|
||||||
|
for (Token token : tokens) {
|
||||||
|
token.prettyPrint(ps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toBytes(ByteArrayOutputStream os) throws IOException {
|
||||||
|
for (Token t : tokens) t.toBytes(os);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package com.webcodepro.applecommander.util.applesoft;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Token in the classic compiler sense, in that this represents a component of the application.
|
||||||
|
*
|
||||||
|
* @author rob
|
||||||
|
*/
|
||||||
|
public class Token {
|
||||||
|
public final int line;
|
||||||
|
public final Type type;
|
||||||
|
public final ApplesoftKeyword keyword;
|
||||||
|
public final Double number;
|
||||||
|
public final String text;
|
||||||
|
|
||||||
|
private Token(int line, Type type, ApplesoftKeyword keyword, Double number, String text) {
|
||||||
|
this.line = line;
|
||||||
|
this.type = type;
|
||||||
|
this.keyword = keyword;
|
||||||
|
this.number = number;
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
switch (type) {
|
||||||
|
case EOL:
|
||||||
|
return type.toString();
|
||||||
|
case KEYWORD:
|
||||||
|
return keyword.toString();
|
||||||
|
case NUMBER:
|
||||||
|
return String.format("%s(%f)", type, number);
|
||||||
|
default:
|
||||||
|
return String.format("%s(%s)", type, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prettyPrint(PrintStream ps) {
|
||||||
|
switch (type) {
|
||||||
|
case EOL:
|
||||||
|
ps.print("<EOL>");
|
||||||
|
break;
|
||||||
|
case COMMENT:
|
||||||
|
ps.printf(" REM %s", text);
|
||||||
|
break;
|
||||||
|
case STRING:
|
||||||
|
ps.printf("\"%s\"", text);
|
||||||
|
break;
|
||||||
|
case KEYWORD:
|
||||||
|
ps.printf(" %s ", keyword.text);
|
||||||
|
break;
|
||||||
|
case IDENT:
|
||||||
|
case SYNTAX:
|
||||||
|
ps.print(text);
|
||||||
|
break;
|
||||||
|
case NUMBER:
|
||||||
|
if (Math.rint(number) == number) {
|
||||||
|
ps.print(number.intValue());
|
||||||
|
} else {
|
||||||
|
ps.print(number);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toBytes(ByteArrayOutputStream os) throws IOException {
|
||||||
|
switch (type) {
|
||||||
|
case COMMENT:
|
||||||
|
os.write(ApplesoftKeyword.REM.code);
|
||||||
|
os.write(text.getBytes());
|
||||||
|
break;
|
||||||
|
case EOL:
|
||||||
|
os.write(0x00);
|
||||||
|
break;
|
||||||
|
case IDENT:
|
||||||
|
os.write(text.getBytes());
|
||||||
|
break;
|
||||||
|
case KEYWORD:
|
||||||
|
os.write(keyword.code);
|
||||||
|
break;
|
||||||
|
case NUMBER:
|
||||||
|
if (Math.rint(number) == number) {
|
||||||
|
os.write(Integer.toString(number.intValue()).getBytes());
|
||||||
|
} else {
|
||||||
|
os.write(Double.toString(number).getBytes());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case STRING:
|
||||||
|
os.write('"');
|
||||||
|
os.write(text.getBytes());
|
||||||
|
os.write('"');
|
||||||
|
break;
|
||||||
|
case SYNTAX:
|
||||||
|
Optional<ApplesoftKeyword> opt = ApplesoftKeyword.find(text);
|
||||||
|
if (opt.isPresent()) {
|
||||||
|
os.write(opt.get().code);
|
||||||
|
} else {
|
||||||
|
os.write(text.getBytes());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Token eol(int line) {
|
||||||
|
return new Token(line, Type.EOL, null, null, null);
|
||||||
|
}
|
||||||
|
public static Token number(int line, Double number) {
|
||||||
|
return new Token(line, Type.NUMBER, null, number, null);
|
||||||
|
}
|
||||||
|
public static Token ident(int line, String text) {
|
||||||
|
return new Token(line, Type.IDENT, null, null, text);
|
||||||
|
}
|
||||||
|
public static Token comment(int line, String text) {
|
||||||
|
return new Token(line, Type.COMMENT, null, null, text);
|
||||||
|
}
|
||||||
|
public static Token string(int line, String text) {
|
||||||
|
return new Token(line, Type.STRING, null, null, text);
|
||||||
|
}
|
||||||
|
public static Token keyword(int line, ApplesoftKeyword keyword) {
|
||||||
|
// Note that the text component is useful to have for parsing, so we replicate it...
|
||||||
|
return new Token(line, Type.KEYWORD, keyword, null, keyword.text);
|
||||||
|
}
|
||||||
|
public static Token syntax(int line, int ch) {
|
||||||
|
return new Token(line, Type.SYNTAX, null, null, String.format("%c", ch));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static enum Type {
|
||||||
|
EOL, NUMBER, IDENT, COMMENT, STRING, KEYWORD, SYNTAX
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
package com.webcodepro.applecommander.util.applesoft;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.StreamTokenizer;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The TokenReader, given a text file, generates a series of Tokens (in the compiler sense,
|
||||||
|
* not AppleSoft) for the AppleSoft program.
|
||||||
|
*
|
||||||
|
* @author rob
|
||||||
|
*/
|
||||||
|
public class TokenReader {
|
||||||
|
private boolean hasMore = true;
|
||||||
|
// Internal flag just in case we consume the EOL (see REM for instance)s
|
||||||
|
private boolean needSyntheticEol = false;
|
||||||
|
private Reader reader;
|
||||||
|
private StreamTokenizer tokenizer;
|
||||||
|
|
||||||
|
/** A handy method to generate a list of Tokens from a file. */
|
||||||
|
public static Queue<Token> tokenize(String filename) throws FileNotFoundException, IOException {
|
||||||
|
try (FileReader fileReader = new FileReader(filename)) {
|
||||||
|
return tokenize(fileReader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** A handy method to generate a list of Tokens from an InputStream. */
|
||||||
|
public static Queue<Token> tokenize(InputStream inputStream) throws IOException {
|
||||||
|
try (InputStreamReader streamReader = new InputStreamReader(inputStream)) {
|
||||||
|
return tokenize(streamReader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static Queue<Token> tokenize(Reader reader) throws IOException {
|
||||||
|
TokenReader tokenReader = new TokenReader(reader);
|
||||||
|
LinkedList<Token> tokens = new LinkedList<>();
|
||||||
|
while (tokenReader.hasMore()) {
|
||||||
|
// Magic number: maximum number of pieces from the StreamTokenizer that may be combined.
|
||||||
|
tokenReader.next(2)
|
||||||
|
.ifPresent(tokens::add);
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenReader(Reader reader) {
|
||||||
|
this.reader = reader;
|
||||||
|
this.tokenizer = ApplesoftKeyword.tokenizer(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasMore() {
|
||||||
|
return hasMore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Token> next(int depth) throws IOException {
|
||||||
|
// A cheesy attempt to prevent too much looping...
|
||||||
|
if (depth > 0) {
|
||||||
|
if (this.needSyntheticEol) {
|
||||||
|
this.needSyntheticEol = false;
|
||||||
|
int line = tokenizer.lineno();
|
||||||
|
return Optional.of(Token.eol(line));
|
||||||
|
}
|
||||||
|
hasMore = tokenizer.nextToken() != StreamTokenizer.TT_EOF;
|
||||||
|
if (hasMore) {
|
||||||
|
int line = tokenizer.lineno();
|
||||||
|
switch (tokenizer.ttype) {
|
||||||
|
case StreamTokenizer.TT_EOL:
|
||||||
|
return Optional.of(Token.eol(line));
|
||||||
|
case StreamTokenizer.TT_NUMBER:
|
||||||
|
return Optional.of(Token.number(line, tokenizer.nval));
|
||||||
|
case StreamTokenizer.TT_WORD:
|
||||||
|
Optional<ApplesoftKeyword> opt = ApplesoftKeyword.find(tokenizer.sval);
|
||||||
|
if (opt.filter(kw -> kw == ApplesoftKeyword.REM).isPresent()) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
while (true) {
|
||||||
|
// Bypass the Tokenizer and just read to EOL for the comment
|
||||||
|
int ch = reader.read();
|
||||||
|
if (ch == '\n') {
|
||||||
|
// Recover to the newline so that the next token is a EOL
|
||||||
|
// This is needed for parsing!
|
||||||
|
this.needSyntheticEol = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sb.append((char)ch);
|
||||||
|
}
|
||||||
|
return Optional.of(Token.comment(line, sb.toString()));
|
||||||
|
}
|
||||||
|
// Optional and exceptions don't play well. :-/
|
||||||
|
if (opt.isPresent() && opt.get().parts.size() > 1) {
|
||||||
|
// Pull next token and see if it is the 2nd part ("MID$" == "MID", "$"; checking for the "$")
|
||||||
|
next(depth-1)
|
||||||
|
.filter(t -> opt.get().parts.get(1).equals(t.text))
|
||||||
|
.orElseThrow(() -> new IOException("Expecting: " + opt.get().parts));
|
||||||
|
}
|
||||||
|
return Optional.of(opt
|
||||||
|
.map(kw -> Token.keyword(line, kw))
|
||||||
|
.orElse(Token.ident(line, tokenizer.sval)));
|
||||||
|
case '"':
|
||||||
|
return Optional.of(Token.string(line, tokenizer.sval));
|
||||||
|
case '(':
|
||||||
|
case ')':
|
||||||
|
case ',':
|
||||||
|
case ':':
|
||||||
|
case '$':
|
||||||
|
case '#':
|
||||||
|
case ';':
|
||||||
|
case '&':
|
||||||
|
case '=':
|
||||||
|
case '<':
|
||||||
|
case '>':
|
||||||
|
case '*':
|
||||||
|
case '+':
|
||||||
|
case '-':
|
||||||
|
case '/':
|
||||||
|
case '^':
|
||||||
|
return Optional.of(
|
||||||
|
ApplesoftKeyword.find(String.format("%c", tokenizer.ttype))
|
||||||
|
.map(kw -> Token.keyword(line, kw))
|
||||||
|
.orElse(Token.syntax(line, tokenizer.ttype)));
|
||||||
|
default:
|
||||||
|
throw new IOException(String.format(
|
||||||
|
"Unknown! ttype=%d, nval=%f, sval=%s\n", tokenizer.ttype, tokenizer.nval, tokenizer.sval));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
/**
|
||||||
|
* 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;
|
|
@ -102,7 +102,7 @@ CreateDirectoryMenuItem=Create Directory...
|
||||||
CommandLineErrorMessage = Error: {0}
|
CommandLineErrorMessage = Error: {0}
|
||||||
CommandLineNoMatchMessage = {0}: No match.
|
CommandLineNoMatchMessage = {0}: No match.
|
||||||
CommandLineStatus = {0} format; {1} bytes free; {2} bytes used.
|
CommandLineStatus = {0} format; {1} bytes free; {2} bytes used.
|
||||||
CommandLineHelp = CommandLineHelp = AppleCommander command line options [{0}]:\n-i <imagename> [<imagename>] display information about image(s).\n-ls <imagename> [<imagename>] list brief directory of image(s).\n-l <imagename> [<imagename>] list directory of image(s).\n-ll <imagename> [<imagename>] list detailed directory of image(s).\n-e <imagename> <filename> [<output>] export file from image to stdout\n or to an output file.\n-x <imagename> [<directory>] extract all files from image to directory.\n-g <imagename> <filename> [<output>] get raw file from image to stdout\n or to an output file.\n-p <imagename> <filename> <type> [[$|0x]<addr>] put stdin\n in filename on image, using file type and address [0x2000].\n-d <imagename> <filename> delete file from image.\n-k <imagename> <filename> lock file on image.\n-u <imagename> <filename> unlock file on image.\n-n <imagename> <volname> change volume name (ProDOS or Pascal).\n-dos <imagename> <filename> <type> put stdin with DOS header\n in filename on image, using file type and address from header.\n-as <imagename> [<filename>] put stdin with AppleSingle format\n in filename on image, using file type, address, and (optionally) name\n from the AppleSingle file.\n-geos <imagename> interpret stdin as a GEOS conversion file and\n place it on image (ProDOS only).\n-dos140 <imagename> create a 140K DOS 3.3 image.\n-pro140 <imagename> <volname> create a 140K ProDOS image.\n-pro800 <imagename> <volname> create an 800K ProDOS image.\n-pas140 <imagename> <volname> create a 140K Pascal image.\n-pas800 <imagename> <volname> create an 800K Pascal image.\n-convert <filename> <imagename> [<sizeblocks>] uncompress a ShrinkIt or Binary\n II file; or convert a DiskCopy 4.2 image into a ProDOS disk image.
|
CommandLineHelp = CommandLineHelp = AppleCommander command line options [{0}]:\n-i <imagename> [<imagename>] display information about image(s).\n-ls <imagename> [<imagename>] list brief directory of image(s).\n-l <imagename> [<imagename>] list directory of image(s).\n-ll <imagename> [<imagename>] list detailed directory of image(s).\n-e <imagename> <filename> [<output>] export file from image to stdout\n or to an output file.\n-x <imagename> [<directory>] extract all files from image to directory.\n-g <imagename> <filename> [<output>] get raw file from image to stdout\n or to an output file.\n-p <imagename> <filename> <type> [[$|0x]<addr>] put stdin\n in filename on image, using file type and address [0x2000].\n-d <imagename> <filename> delete file from image.\n-k <imagename> <filename> lock file on image.\n-u <imagename> <filename> unlock file on image.\n-n <imagename> <volname> change volume name (ProDOS or Pascal).\n-dos <imagename> <filename> <type> put stdin with DOS header\n in filename on image, using file type and address from header.\n-as <imagename> [<filename>] put stdin with AppleSingle format\n in filename on image, using file type, address, and (optionally) name\n from the AppleSingle file.\n-geos <imagename> interpret stdin as a GEOS conversion file and\n place it on image (ProDOS only).\n-dos140 <imagename> create a 140K DOS 3.3 image.\n-pro140 <imagename> <volname> create a 140K ProDOS image.\n-pro800 <imagename> <volname> create an 800K ProDOS image.\n-pas140 <imagename> <volname> create a 140K Pascal image.\n-pas800 <imagename> <volname> create an 800K Pascal image.\n-convert <filename> <imagename> [<sizeblocks>] uncompress a ShrinkIt or Binary\n II file; or convert a DiskCopy 4.2 image into a ProDOS disk image.\n-bas <imagename> <filename> import an AppleSoft basic file from text\n back to it's tokenized format.
|
||||||
CommandLineSDKReadOnly = SDK, SHK, and DC42 files are read-only. Use the convert option on them first.
|
CommandLineSDKReadOnly = SDK, SHK, and DC42 files are read-only. Use the convert option on them first.
|
||||||
CommandLineDC42Bad = Unable to interpret this DiskCopy 42 image.
|
CommandLineDC42Bad = Unable to interpret this DiskCopy 42 image.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue