package com.bytezone.diskbrowser.applefile; import java.util.*; import com.bytezone.diskbrowser.HexFormatter; public class BasicProgram extends AbstractFile { private static final byte ASCII_QUOTE = 0x22; private static final byte ASCII_COLON = 0x3A; private static final byte ASCII_SEMI_COLON = 0x3B; private static final byte TOKEN_FOR = (byte) 0x81; private static final byte TOKEN_NEXT = (byte) 0x82; private static final byte TOKEN_LET = (byte) 0xAA; private static final byte TOKEN_GOTO = (byte) 0xAB; private static final byte TOKEN_IF = (byte) 0xAD; private static final byte TOKEN_GOSUB = (byte) 0xB0; private static final byte TOKEN_REM = (byte) 0xB2; private static final byte TOKEN_PRINT = (byte) 0xBA; private static final byte TOKEN_THEN = (byte) 0xC4; private static final byte TOKEN_EQUALS = (byte) 0xD0; private final List sourceLines = new ArrayList (); private final int endPtr; private final Set gotoLines = new HashSet (); private final Set gosubLines = new HashSet (); boolean splitRem = false; // should be a user preference boolean alignAssign = true; // should be a user preference boolean showTargets = true; // should be a user preference boolean showHeader = true; // should be a user preference boolean onlyShowTargetLineNumbers = false; // should be a user preference int wrapPrintAt = 40; int wrapRemAt = 60; public BasicProgram (String name, byte[] buffer) { super (name, buffer); int ptr = 0; int prevOffset = 0; int max = buffer.length - 4; // need at least 4 bytes to make a SourceLine while (ptr < max) { int offset = HexFormatter.intValue (buffer[ptr], buffer[ptr + 1]); if (offset <= prevOffset) break; SourceLine line = new SourceLine (ptr); sourceLines.add (line); ptr += line.length; prevOffset = offset; } endPtr = ptr; } @Override public String getText () { StringBuilder fullText = new StringBuilder (); Stack loopVariables = new Stack (); if (showHeader) addHeader (fullText); int alignPos = 0; StringBuilder text; int baseOffset = showTargets ? 12 : 8; for (SourceLine line : sourceLines) { text = new StringBuilder (getBase (line) + " "); int indent = loopVariables.size (); // each full line starts at the loop indent int ifIndent = 0; // IF statement(s) limit back indentation by NEXT for (SubLine subline : line.sublines) { // Allow empty statements (caused by a single colon) if (subline.isEmpty ()) continue; // A REM statement might conceal an assembler routine - see P.CREATE on Diags2E.DSK if (subline.is (TOKEN_REM) && subline.containsToken ()) { int address = subline.getAddress () + 1; // skip the REM token fullText.append (text + String.format ("REM - Inline assembler @ $%02X (%d)%n", address, address)); String padding = " ".substring (0, text.length () + 2); for (String asm : subline.getAssembler ()) fullText.append (padding + asm + "\n"); continue; } // Reduce the indent by each NEXT, but only as far as the IF indent allows if (subline.is (TOKEN_NEXT)) { popLoopVariables (loopVariables, subline); indent = Math.max (ifIndent, loopVariables.size ()); } // Are we joining REM lines with the previous subline? if (!splitRem && subline.isJoinableRem ()) { // Join this REM statement to the previous line, so no indenting fullText.deleteCharAt (fullText.length () - 1); // remove newline fullText.append (" "); } // ... otherwise do all the indenting and showing of targets etc. else { // Prepare target indicators for subsequent sublines (ie no line number) if (showTargets && !subline.isFirst ()) if (subline.is (TOKEN_GOSUB)) text.append ("<<--"); else if (subline.is (TOKEN_GOTO) || subline.isImpliedGoto ()) text.append (" <--"); // Align assign statements if required if (alignAssign) alignPos = alignEqualsPosition (subline, alignPos); int column = indent * 2 + baseOffset; while (text.length () < column) text.append (" "); } // Add the current text, then reset it int pos = subline.is (TOKEN_REM) ? 0 : alignPos; String lineText = subline.getAlignedText (pos); // if (subline.is (TOKEN_REM) && lineText.length () > wrapRemAt + 4) // { // System.out.println (subline.getAlignedText (pos)); // String copy = lineText.substring (4); // text.append ("REM "); // int inset = text.length (); // System.out.println (inset); // List remarks = splitRemark (copy, wrapRemAt); // for (String remark : remarks) // text.append (" ".substring (0, inset) + remark); // } // else text.append (lineText); // Check for a wrapable PRINT statement (see FROM MACHINE LANGUAGE TO BASIC on DOSToolkit2eB.dsk) if (subline.is (TOKEN_PRINT) && wrapPrintAt > 0 && countChars (text, ASCII_QUOTE) == 2 && countChars (text, ASCII_SEMI_COLON) == 0) { int first = text.indexOf ("\""); int last = text.indexOf ("\"", first + 1); if ((last - first) > wrapPrintAt) { int ptr = first + wrapPrintAt; do { fullText.append (text.substring (0, ptr) + "\n ".substring (0, first + 1)); text.delete (0, ptr); ptr = wrapPrintAt; } while (text.length () > wrapPrintAt); } } fullText.append (text + "\n"); text.setLength (0); // Calculate indent changes that take effect after the current subline if (subline.is (TOKEN_IF)) ifIndent = ++indent; else if (subline.is (TOKEN_FOR)) { loopVariables.push (subline.forVariable); ++indent; } } // Reset the alignment value if we just left an IF - the indentation will be different now. if (ifIndent > 0) alignPos = 0; } fullText.deleteCharAt (fullText.length () - 1); // remove last newline return fullText.toString (); } private List splitRemark (String remark, int wrapLength) { List remarks = new ArrayList (); while (remark.length () > wrapLength) { int max = Math.min (wrapLength, remark.length () - 1); while (max > 0 && remark.charAt (max) != ' ') --max; System.out.println (remark.substring (0, max)); remarks.add (remark.substring (0, max) + "\n"); if (max == 0) break; remark = remark.substring (max + 1); } remarks.add (remark); System.out.println (remark); return remarks; } private int countChars (StringBuilder text, byte ch) { int total = 0; for (int i = 0; i < text.length (); i++) if (text.charAt (i) == ch) total++; return total; } private String getBase (SourceLine line) { if (!showTargets) return String.format (" %5d", line.lineNumber); String lineNumberText = String.format ("%5d", line.lineNumber); SubLine subline = line.sublines.get (0); String c1 = " ", c2 = " "; if (subline.is (TOKEN_GOSUB)) c1 = "<<"; if (subline.is (TOKEN_GOTO)) c1 = " <"; if (gotoLines.contains (line.lineNumber)) c2 = "> "; if (gosubLines.contains (line.lineNumber)) c2 = ">>"; if (c1.equals (" ") && !c2.equals (" ")) c1 = "--"; if (!c1.equals (" ") && c2.equals (" ")) c2 = "--"; if (onlyShowTargetLineNumbers && !c2.startsWith (">")) lineNumberText = ""; return String.format ("%s%s %s", c1, c2, lineNumberText); } // Decide whether the current subline needs to be aligned on its equals sign. If so, // and the column hasn't been calculated, read ahead to find the highest position. private int alignEqualsPosition (SubLine subline, int currentAlignPosition) { if (subline.assignEqualPos > 0) // does the line have an equals sign? { if (currentAlignPosition == 0) currentAlignPosition = findHighest (subline); // examine following sublines for alignment return currentAlignPosition; } return 0; // reset it } // The IF processing is so that any assignment that is being aligned doesn't continue // to the next full line (because the indentation has changed). private int findHighest (SubLine startSubline) { boolean started = false; int highestAssign = startSubline.assignEqualPos; fast: for (SourceLine line : sourceLines) { boolean inIf = false; for (SubLine subline : line.sublines) { if (started) { // Stop when we come to a line without an equals sign (except for non-split REMs). // Lines that start with a REM always break. if (subline.assignEqualPos == 0 // && (splitRem || !subline.is (TOKEN_REM) || subline.isFirst ())) && (splitRem || !subline.isJoinableRem ())) break fast; // of champions if (subline.assignEqualPos > highestAssign) highestAssign = subline.assignEqualPos; } else if (subline == startSubline) started = true; else if (subline.is (TOKEN_IF)) inIf = true; } if (started && inIf) break; } return highestAssign; } @Override public String getHexDump () { if (buffer.length < 2) return super.getHexDump (); StringBuilder pgm = new StringBuilder (); if (showHeader) addHeader (pgm); int ptr = 0; int offset = HexFormatter.intValue (buffer[0], buffer[1]); int programLoadAddress = offset - getLineLength (0); while (ptr <= endPtr) // stop at the same place as the source listing { int length = getLineLength (ptr); if (length == 0) { pgm.append (HexFormatter.formatNoHeader (buffer, ptr, 2, programLoadAddress)); ptr += 2; break; } if (ptr + length < buffer.length) pgm.append (HexFormatter.formatNoHeader (buffer, ptr, length, programLoadAddress) + "\n\n"); ptr += length; } if (ptr < buffer.length) { int length = buffer.length - ptr; pgm.append ("\n\n"); pgm.append (HexFormatter.formatNoHeader (buffer, ptr, length, programLoadAddress)); } return pgm.toString (); } private void addHeader (StringBuilder pgm) { pgm.append ("Name : " + name + "\n"); pgm.append ("Length : $" + HexFormatter.format4 (buffer.length)); pgm.append (" (" + buffer.length + ")\n"); int programLoadAddress = getLoadAddress (); pgm.append ("Load at : $" + HexFormatter.format4 (programLoadAddress)); pgm.append (" (" + programLoadAddress + ")\n\n"); } private int getLoadAddress () { int programLoadAddress = 0; if (buffer.length > 1) { int offset = HexFormatter.intValue (buffer[0], buffer[1]); programLoadAddress = offset - getLineLength (0); } return programLoadAddress; } private int getLineLength (int ptr) { int offset = HexFormatter.intValue (buffer[ptr], buffer[ptr + 1]); if (offset == 0) return 0; ptr += 4; int length = 5; while (ptr < buffer.length && buffer[ptr++] != 0) length++; return length; } private void popLoopVariables (Stack loopVariables, SubLine subline) { if (subline.nextVariables.length == 0) // naked NEXT { if (loopVariables.size () > 0) loopVariables.pop (); } else for (String variable : subline.nextVariables) // e.g. NEXT X,Y,Z while (loopVariables.size () > 0) if (sameVariable (variable, loopVariables.pop ())) break; } private boolean sameVariable (String v1, String v2) { if (v1.equals (v2)) return true; if (v1.length () >= 2 && v2.length () >= 2 && v1.charAt (0) == v2.charAt (0) && v1.charAt (1) == v2.charAt (1)) return true; return false; } private class SourceLine { List sublines = new ArrayList (); int lineNumber; int linePtr; int length; public SourceLine (int ptr) { linePtr = ptr; lineNumber = HexFormatter.intValue (buffer[ptr + 2], buffer[ptr + 3]); int startPtr = ptr += 4; boolean inString = false; // can toggle boolean inRemark = false; // can only go false -> true byte b; while ((b = buffer[ptr++]) != 0) { switch (b) { // break IF statements into two sublines (allows for easier line indenting) case TOKEN_IF: if (!inString && !inRemark) { // skip to THEN or GOTO - if not found then it's an error while (buffer[ptr] != TOKEN_THEN && buffer[ptr] != TOKEN_GOTO && buffer[ptr] != 0) ptr++; // keep THEN with the IF if (buffer[ptr] == TOKEN_THEN) ++ptr; // create subline from the condition (and THEN if it exists) sublines.add (new SubLine (this, startPtr, ptr - startPtr)); startPtr = ptr; } break; // end of subline, so add it, advance startPtr and continue case ASCII_COLON: if (!inString && !inRemark) { sublines.add (new SubLine (this, startPtr, ptr - startPtr)); startPtr = ptr; } break; case TOKEN_REM: if (!inString && !inRemark) inRemark = true; break; case ASCII_QUOTE: if (!inRemark) inString = !inString; break; } } // add whatever is left sublines.add (new SubLine (this, startPtr, ptr - startPtr)); this.length = ptr - linePtr; } } private class SubLine { SourceLine parent; int startPtr; int length; String[] nextVariables; String forVariable = ""; int targetLine = -1; // used for aligning the equals sign int assignEqualPos; public SubLine (SourceLine parent, int startPtr, int length) { this.parent = parent; this.startPtr = startPtr; this.length = length; byte b = buffer[startPtr]; if ((b & 0x80) > 0) // token { switch (b) { case TOKEN_FOR: int p = startPtr + 1; while (buffer[p] != TOKEN_EQUALS) forVariable += (char) buffer[p++]; break; case TOKEN_NEXT: if (length == 2) // no variables nextVariables = new String[0]; else { String varList = new String (buffer, startPtr + 1, length - 2); nextVariables = varList.split (","); } break; case TOKEN_LET: recordEqualsPosition (); break; case TOKEN_GOTO: String target = new String (buffer, startPtr + 1, length - 2); try { gotoLines.add (Integer.parseInt (target)); } catch (NumberFormatException e) { System.out.println ("Error parsing : GOTO " + target + " in " + parent.lineNumber); } break; case TOKEN_GOSUB: String target2 = new String (buffer, startPtr + 1, length - 2); try { gosubLines.add (Integer.parseInt (target2)); } catch (NumberFormatException e) { System.out.println ("Error parsing : GOSUB " + target2 + " in " + parent.lineNumber); } break; } } else { if (b >= 48 && b <= 57) // numeric, so must be a line number { String target = new String (buffer, startPtr, length - 1); try { targetLine = Integer.parseInt (target); gotoLines.add (targetLine); } catch (NumberFormatException e) { System.out.println (target); System.out.println (HexFormatter.format (buffer, startPtr, length - 1)); System.out.println (e.toString ()); } } else if (alignAssign) recordEqualsPosition (); } } private boolean isImpliedGoto () { byte b = buffer[startPtr]; if ((b & 0x80) > 0) // token return false; return (b >= 48 && b <= 57); } // Record the position of the equals sign so it can be aligned with adjacent lines. private void recordEqualsPosition () { int p = startPtr + 1; int max = startPtr + length; while (buffer[p] != TOKEN_EQUALS && p < max) p++; if (buffer[p] == TOKEN_EQUALS) assignEqualPos = toString ().indexOf ('='); // use expanded line } private boolean isJoinableRem () { return is (TOKEN_REM) && !isFirst (); } public boolean isFirst () { return (parent.linePtr + 4) == startPtr; } public boolean is (byte token) { return buffer[startPtr] == token; } public boolean isEmpty () { return length == 1 && buffer[startPtr] == 0; } public boolean containsToken () { // ignore first byte, check the rest for tokens for (int p = startPtr + 1, max = startPtr + length; p < max; p++) if ((buffer[p] & 0x80) > 0) return true; return false; } public int getAddress () { return getLoadAddress () + startPtr; } public String getAlignedText (int alignPosition) { StringBuilder line = toStringBuilder (); while (alignPosition-- > assignEqualPos) line.insert (assignEqualPos, ' '); return line.toString (); } // A REM statement might conceal an assembler routine public String[] getAssembler () { byte[] buffer2 = new byte[length - 1]; System.arraycopy (buffer, startPtr + 1, buffer2, 0, buffer2.length); AssemblerProgram program = new AssemblerProgram ("REM assembler", buffer2, getAddress () + 1); return program.getAssembler ().split ("\n"); } @Override public String toString () { return toStringBuilder ().toString (); } public StringBuilder toStringBuilder () { StringBuilder line = new StringBuilder (); // All sublines end with 0 or : except IF lines that are split into two int max = startPtr + length - 1; if (buffer[max] == 0) --max; for (int p = startPtr; p <= max; p++) { byte b = buffer[p]; if ((b & 0x80) > 0) // token { if (line.length () > 0 && line.charAt (line.length () - 1) != ' ') line.append (' '); int val = b & 0x7F; if (val < ApplesoftConstants.tokens.length) line.append (ApplesoftConstants.tokens[b & 0x7F]); // else // System.out.println ("Bad value : " + val + " " + line.toString () + " " // + parent.lineNumber); } else if (b < 32) // CTRL character line.append ("^" + (char) (b + 64)); // would be better in inverse text else line.append ((char) b); } return line; } } }