package com.bytezone.diskbrowser.applefile; import static com.bytezone.diskbrowser.utilities.Utility.ASCII_BACKSPACE; import static com.bytezone.diskbrowser.utilities.Utility.ASCII_COLON; import static com.bytezone.diskbrowser.utilities.Utility.ASCII_COMMA; import static com.bytezone.diskbrowser.utilities.Utility.ASCII_CR; import static com.bytezone.diskbrowser.utilities.Utility.ASCII_DOLLAR; import static com.bytezone.diskbrowser.utilities.Utility.ASCII_LEFT_BRACKET; import static com.bytezone.diskbrowser.utilities.Utility.ASCII_LF; import static com.bytezone.diskbrowser.utilities.Utility.ASCII_MINUS; import static com.bytezone.diskbrowser.utilities.Utility.ASCII_PERCENT; import static com.bytezone.diskbrowser.utilities.Utility.ASCII_QUOTE; import static com.bytezone.diskbrowser.utilities.Utility.ASCII_RIGHT_BRACKET; import static com.bytezone.diskbrowser.utilities.Utility.getIndent; import static com.bytezone.diskbrowser.utilities.Utility.isControlCharacter; import static com.bytezone.diskbrowser.utilities.Utility.isDigit; import static com.bytezone.diskbrowser.utilities.Utility.isHighBitSet; import static com.bytezone.diskbrowser.utilities.Utility.isLetter; import static com.bytezone.diskbrowser.utilities.Utility.isPossibleNumber; import static com.bytezone.diskbrowser.utilities.Utility.isPossibleVariable; import java.util.ArrayList; import java.util.List; import com.bytezone.diskbrowser.utilities.HexFormatter;; // -----------------------------------------------------------------------------------// public class SubLine implements ApplesoftConstants // -----------------------------------------------------------------------------------// { SourceLine sourceLine; byte[] buffer; int startPtr; int length; String[] nextVariables; String forVariable = ""; int equalsPosition; // used for aligning the equals sign int endPosition; // not sure yet - possibly for aligning REMs String functionArgument; String functionName; String callTarget; private final List gotoLines = new ArrayList<> (); private final List gosubLines = new ArrayList<> (); private final List variables = new ArrayList<> (); private final List functions = new ArrayList<> (); private final List arrays = new ArrayList<> (); private final List constantsInt = new ArrayList<> (); private final List constantsFloat = new ArrayList<> (); private final List stringsText = new ArrayList<> (); // ---------------------------------------------------------------------------------// SubLine (SourceLine sourceLine, int offset, int length) // ---------------------------------------------------------------------------------// { this.sourceLine = sourceLine; this.startPtr = offset; this.length = length; this.buffer = sourceLine.buffer; int ptr = startPtr; byte firstByte = buffer[startPtr]; if (isToken (firstByte)) { doToken (firstByte); if (is (TOKEN_REM) || is (TOKEN_DATA)) // no further processing return; if (is (TOKEN_CALL)) ptr = startPtr + callTarget.length (); else ptr = startPtr + 1; } else { if (isDigit (firstByte)) // split IF xx THEN nnn { addXref (getLineNumber (buffer, startPtr), gotoLines); return; } else if (isLetter (firstByte)) // variable name setEqualsPosition (); else if (isEndOfLine (firstByte)) // empty subline return; else // probably Beagle Bros 0D or 0A System.out.printf ("%s unexpected bytes at line %5d:%n%s%n", sourceLine.program.name, sourceLine.lineNumber, HexFormatter.formatNoHeader (buffer, startPtr, length)); } String var = ""; boolean inQuote = false; boolean inFunction = false; boolean inDefine = false; int stringPtr = 0; int endOfLine = startPtr + length - 1; while (isEndOfLine (buffer[endOfLine])) // zero or colon --endOfLine; while (ptr <= endOfLine) { byte b = buffer[ptr++]; if (inDefine) // ignore the name and argument { if (b == TOKEN_EQUALS) inDefine = false; continue; } if (b == TOKEN_DEF) { inDefine = true; continue; } if (b == TOKEN_FN) { assert !inDefine; inFunction = true; continue; } if (inQuote) { if (b == ASCII_QUOTE) // ignore strings { inQuote = false; addString (stringPtr, ptr); } continue; } if (b == ASCII_QUOTE) { inQuote = true; stringPtr = ptr; continue; } if (isPossibleVariable (b) || isPossibleNumber (b)) { if (var.isEmpty () && isPossibleNumber (b) && buffer[ptr - 2] == TOKEN_MINUS) var = "-"; var += (char) b; // allow for PRINT A$B$ if ((b == ASCII_DOLLAR || b == ASCII_PERCENT) // var name end && buffer[ptr] != ASCII_LEFT_BRACKET) // not an array { checkVar (var, b); var = ""; } } else { if (inFunction) { checkFunction (var, b); inFunction = false; } else checkVar (var, b); var = ""; } } if (inQuote) addString (stringPtr, ptr); // unterminated string else checkVar (var, (byte) 0); // unprocessed variable or number } // ---------------------------------------------------------------------------------// private boolean isEndOfLine (byte b) // ---------------------------------------------------------------------------------// { return b == 0 || b == ASCII_COLON; } // ---------------------------------------------------------------------------------// private void addString (int stringPtr, int ptr) // ---------------------------------------------------------------------------------// { stringsText.add (new String (buffer, stringPtr - 1, ptr - stringPtr + 1)); } // ---------------------------------------------------------------------------------// private void checkFunction (String var, byte terminator) // ---------------------------------------------------------------------------------// { // assert terminator == ASCII_LEFT_BRACKET; if (!functions.contains (var)) functions.add (var); } // ---------------------------------------------------------------------------------// private void checkVar (String var, byte terminator) // ---------------------------------------------------------------------------------// { if (var.length () == 0) return; if (!isLetter ((byte) var.charAt (0))) { if (is (TOKEN_GOTO) || is (TOKEN_GOSUB) || is (TOKEN_ON) || is (TOKEN_ONERR)) return; // ignore line numbers addNumber (var); return; } if (is (TOKEN_DEF) && (var.equals (functionName) || var.equals (functionArgument))) return; if (terminator == ASCII_LEFT_BRACKET) { if (!arrays.contains (var)) arrays.add (var); } else if (!variables.contains (var)) variables.add (var); } // ---------------------------------------------------------------------------------// private void doToken (byte b) // ---------------------------------------------------------------------------------// { 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: setEqualsPosition (); break; case TOKEN_GOTO: int targetLine = getLineNumber (buffer, startPtr + 1); addXref (targetLine, gotoLines); break; case TOKEN_GOSUB: targetLine = getLineNumber (buffer, startPtr + 1); addXref (targetLine, gosubLines); break; case TOKEN_ON: p = startPtr + 1; int max = startPtr + length - 1; while (p < max && buffer[p] != TOKEN_GOTO && buffer[p] != TOKEN_GOSUB) p++; switch (buffer[p++]) { case TOKEN_GOSUB: for (int destLine : getLineNumbers (buffer, p)) addXref (destLine, gosubLines); break; case TOKEN_GOTO: for (int destLine : getLineNumbers (buffer, p)) addXref (destLine, gotoLines); break; default: System.out.println ("GOTO / GOSUB not found"); } break; case TOKEN_ONERR: if (buffer[startPtr + 1] == TOKEN_GOTO) { targetLine = getLineNumber (buffer, startPtr + 2); addXref (targetLine, gotoLines); } break; case TOKEN_CALL: callTarget = getCallTarget (); break; case TOKEN_DEF: byte[] lineBuffer = getBuffer (); assert lineBuffer[0] == TOKEN_FN; int leftBracket = getPosition (lineBuffer, 1, ASCII_LEFT_BRACKET); int rightBracket = getPosition (lineBuffer, leftBracket + 1, ASCII_RIGHT_BRACKET); functionName = new String (lineBuffer, 1, leftBracket - 1); functionArgument = new String (lineBuffer, leftBracket + 1, rightBracket - leftBracket - 1); functions.add (functionName); break; case TOKEN_DATA: for (String chunk : new String (getBuffer ()).split (",")) { chunk = chunk.trim (); if (chunk.isEmpty ()) continue; b = (byte) chunk.charAt (0); if (isPossibleNumber (b) || b == ASCII_MINUS) { if (!addNumber (chunk)) stringsText.add (chunk); } else stringsText.add (chunk); } break; } } // ---------------------------------------------------------------------------------// private boolean addNumber (String var) // ---------------------------------------------------------------------------------// { try { if (var.indexOf ('.') < 0) // no decimal point { int varInt = Integer.parseInt (var); if (!constantsInt.contains (varInt)) constantsInt.add (varInt); } else { float varFloat = Float.parseFloat (var); if (!constantsFloat.contains (varFloat)) constantsFloat.add (varFloat); } } catch (NumberFormatException nfe) { return false; } return true; } // ---------------------------------------------------------------------------------// private String getCallTarget () // ---------------------------------------------------------------------------------// { StringBuilder text = new StringBuilder (); int ptr = startPtr + 1; int max = startPtr + length - 1; while (ptr < max) { byte b = buffer[ptr++]; if (isToken (b)) text.append (tokens[b & 0x7F]); else if (b == ASCII_COMMA) // end of call target break; else text.append ((char) b); } return text.toString (); } // ---------------------------------------------------------------------------------// private int getPosition (byte[] buffer, int start, byte value) // ---------------------------------------------------------------------------------// { for (int i = start; i < buffer.length; i++) if (buffer[i] == value) return i; return -1; } // ---------------------------------------------------------------------------------// private void addXref (int targetLine, List list) // ---------------------------------------------------------------------------------// { if (!list.contains (targetLine)) list.add (targetLine); } // ---------------------------------------------------------------------------------// private List getLineNumbers (byte[] buffer, int ptr) // ---------------------------------------------------------------------------------// { List lineNumbers = new ArrayList<> (); int start = ptr; while (ptr < buffer.length && buffer[ptr] != 0 && buffer[ptr] != ASCII_COLON) ptr++; String s = new String (buffer, start, ptr - start); String[] chunks = s.split (","); try { for (String chunk : chunks) lineNumbers.add (Integer.parseInt (chunk)); } catch (NumberFormatException e) { System.out.printf ("NFE2: %s%n", s); } return lineNumbers; } // ---------------------------------------------------------------------------------// private int getLineNumber (byte[] buffer, int ptr) // ---------------------------------------------------------------------------------// { int lineNumber = 0; while (ptr < buffer.length && isDigit (buffer[ptr])) lineNumber = lineNumber * 10 + (buffer[ptr++] & 0xFF) - 0x30; return lineNumber; } // ---------------------------------------------------------------------------------// boolean isImpliedGoto () // ---------------------------------------------------------------------------------// { return (isDigit (buffer[startPtr])); } // Record the position of the equals sign so it can be aligned with adjacent lines. // Illegal lines could have a variable name with no equals sign. // ---------------------------------------------------------------------------------// private void setEqualsPosition () // ---------------------------------------------------------------------------------// { int p = startPtr; int max = startPtr + length; while (++p < max) if (buffer[p] == TOKEN_EQUALS) { String expandedLine = toString (); equalsPosition = expandedLine.indexOf ('='); endPosition = expandedLine.length (); if (expandedLine.endsWith (":")) endPosition--; break; } } // ---------------------------------------------------------------------------------// boolean isJoinableRem () // ---------------------------------------------------------------------------------// { return is (TOKEN_REM) && isNotFirst (); } // ---------------------------------------------------------------------------------// boolean isFirst () // ---------------------------------------------------------------------------------// { return (sourceLine.linePtr + 4) == startPtr; } // ---------------------------------------------------------------------------------// boolean isNotFirst () // ---------------------------------------------------------------------------------// { return !isFirst (); } // ---------------------------------------------------------------------------------// boolean is (byte token) // ---------------------------------------------------------------------------------// { return buffer[startPtr] == token; } // ---------------------------------------------------------------------------------// boolean has (byte token) // ---------------------------------------------------------------------------------// { int ptr = startPtr; int max = startPtr + length; while (++ptr < max) if (buffer[ptr] == token) return true; return false; } // ---------------------------------------------------------------------------------// boolean isEmpty () // ---------------------------------------------------------------------------------// { return length == 1 && buffer[startPtr] == 0; } // ---------------------------------------------------------------------------------// boolean containsToken () // ---------------------------------------------------------------------------------// { // ignore first byte, check the rest for tokens for (int p = startPtr + 1, max = startPtr + length; p < max; p++) if (isToken (buffer[p])) return true; return false; } // ---------------------------------------------------------------------------------// boolean containsControlChars () // ---------------------------------------------------------------------------------// { for (int p = startPtr + 1, max = startPtr + length; p < max; p++) { int c = buffer[p] & 0xFF; if (c == 0) break; if (c < 32) return true; } return false; } // ---------------------------------------------------------------------------------// void addFormattedRem (StringBuilder text) // ---------------------------------------------------------------------------------// { int ptr = startPtr + 1; int max = startPtr + length - 1; if (isFirst ()) { if (containsBackspaces (ptr, max)) // probably going to erase the line number { // apple format uses left-justified line numbers so the length varies text.setLength (0); text.append (String.format (" %d REM ", sourceLine.lineNumber)); // mimic apple } else text.append (" REM "); } else text.append ("REM "); while (ptr < max) { switch (buffer[ptr]) { case ASCII_BACKSPACE: if (text.length () > 0) text.deleteCharAt (text.length () - 1); break; case ASCII_CR: text.append ("\n"); break; case ASCII_LF: int indent = getIndent (text); text.append ("\n"); for (int i = 0; i < indent; i++) text.append (" "); break; default: text.append ((char) buffer[ptr]); // do not mask with 0xFF } ptr++; } } // ---------------------------------------------------------------------------------// private boolean containsBackspaces (int ptr, int max) // ---------------------------------------------------------------------------------// { while (ptr < max) if (buffer[ptr++] == ASCII_BACKSPACE) return true; return false; } // ---------------------------------------------------------------------------------// public byte[] getBuffer () // ---------------------------------------------------------------------------------// { int len = length - 1; if (buffer[startPtr + len] == ASCII_COLON || buffer[startPtr + len] == 0) len--; byte[] buffer2 = new byte[len]; System.arraycopy (buffer, startPtr + 1, buffer2, 0, buffer2.length); return buffer2; } // ---------------------------------------------------------------------------------// boolean isToken (byte b) // ---------------------------------------------------------------------------------// { return isHighBitSet (b); } // ---------------------------------------------------------------------------------// List getVariables () // ---------------------------------------------------------------------------------// { return variables; } // ---------------------------------------------------------------------------------// List getFunctions () // ---------------------------------------------------------------------------------// { return functions; } // ---------------------------------------------------------------------------------// List getArrays () // ---------------------------------------------------------------------------------// { return arrays; } // ---------------------------------------------------------------------------------// List getGotoLines () // ---------------------------------------------------------------------------------// { return gotoLines; } // ---------------------------------------------------------------------------------// List getGosubLines () // ---------------------------------------------------------------------------------// { return gosubLines; } // ---------------------------------------------------------------------------------// List getConstantsInt () // ---------------------------------------------------------------------------------// { return constantsInt; } // ---------------------------------------------------------------------------------// List getConstantsFloat () // ---------------------------------------------------------------------------------// { return constantsFloat; } // ---------------------------------------------------------------------------------// List getStringsText () // ---------------------------------------------------------------------------------// { return stringsText; } // ---------------------------------------------------------------------------------// 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; if (isImpliedGoto () && !ApplesoftBasicProgram.basicPreferences.showThen) line.append ("GOTO "); for (int p = startPtr; p <= max; p++) { byte b = buffer[p]; if (isToken (b)) { if (line.length () > 0 && line.charAt (line.length () - 1) != ' ') line.append (' '); int val = b & 0x7F; if (b != TOKEN_THEN || ApplesoftBasicProgram.basicPreferences.showThen) line.append (ApplesoftConstants.tokens[val] + " "); } // else if (Utility.isControlCharacter (b)) // line.append (ApplesoftBasicProgram.basicPreferences.showCaret // ? "^" + (char) (b + 64) : "?"); else if (!isControlCharacter (b)) line.append ((char) b); } return line; } // ---------------------------------------------------------------------------------// @Override public String toString () // ---------------------------------------------------------------------------------// { return toStringBuilder ().toString (); } }