summaryrefslogtreecommitdiff
path: root/common
diff options
context:
space:
mode:
Diffstat (limited to 'common')
-rw-r--r--common/README.md7
-rw-r--r--common/pom.xml14
-rw-r--r--common/src/main/java/com/seaofnodes/simple/common/CompilerException.java11
-rw-r--r--common/src/main/java/com/seaofnodes/simple/common/ErrorListener.java266
-rw-r--r--common/src/main/java/com/seaofnodes/simple/common/Severity.java19
-rw-r--r--common/src/main/java/com/seaofnodes/simple/lexer/aa/Lexer.java228
-rw-r--r--common/src/main/java/com/seaofnodes/simple/lexer/ecstasy/Lexer.java3041
-rw-r--r--common/src/main/java/com/seaofnodes/simple/lexer/ecstasy/Source.java826
-rw-r--r--common/src/main/java/com/seaofnodes/simple/lexer/ecstasy/Token.java870
-rw-r--r--common/src/main/java/com/seaofnodes/util/Ary.java367
-rw-r--r--common/src/main/java/com/seaofnodes/util/AryInt.java203
-rw-r--r--common/src/main/java/com/seaofnodes/util/Handy.java1778
-rw-r--r--common/src/main/java/com/seaofnodes/util/Hash.java299
-rw-r--r--common/src/main/java/com/seaofnodes/util/PackedInteger.java1360
-rw-r--r--common/src/main/java/com/seaofnodes/util/SB.java80
-rw-r--r--common/src/main/java/com/seaofnodes/util/Util.java151
-rw-r--r--common/src/main/java/com/seaofnodes/util/VBitSet.java27
-rw-r--r--common/src/test/java/com/seaofnodes/simple/lexer/TestLexer.java37
18 files changed, 9584 insertions, 0 deletions
diff --git a/common/README.md b/common/README.md
new file mode 100644
index 00000000..d6a5dbb8
--- /dev/null
+++ b/common/README.md
@@ -0,0 +1,7 @@
+# Common Utilities
+
+In this module certain common utilities are defined that
+will be reused throughout the project.
+
+* Lexer - the lexer is based upon the one in Ecstasy Programming Language. Its feature rich enough that we do not
+ expect to have to modify it much. \ No newline at end of file
diff --git a/common/pom.xml b/common/pom.xml
new file mode 100644
index 00000000..e70fcf1d
--- /dev/null
+++ b/common/pom.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.seaofnodes</groupId>
+ <artifactId>simple</artifactId>
+ <version>1.0</version>
+ </parent>
+ <artifactId>common</artifactId>
+ <packaging>jar</packaging>
+ <name>Common Stuff</name>
+</project> \ No newline at end of file
diff --git a/common/src/main/java/com/seaofnodes/simple/common/CompilerException.java b/common/src/main/java/com/seaofnodes/simple/common/CompilerException.java
new file mode 100644
index 00000000..518dcde6
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/simple/common/CompilerException.java
@@ -0,0 +1,11 @@
+package com.seaofnodes.simple.common;
+
+public class CompilerException extends RuntimeException {
+ public CompilerException(String message) {
+ super(message);
+ }
+
+ public CompilerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/common/src/main/java/com/seaofnodes/simple/common/ErrorListener.java b/common/src/main/java/com/seaofnodes/simple/common/ErrorListener.java
new file mode 100644
index 00000000..29ca46b6
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/simple/common/ErrorListener.java
@@ -0,0 +1,266 @@
+package com.seaofnodes.simple.common;
+
+import java.text.MessageFormat;
+
+import java.util.Arrays;
+
+import com.seaofnodes.simple.lexer.ecstasy.Source;
+
+import static com.seaofnodes.util.Handy.quotedString;
+
+
+/**
+ * A listener for errors being reported about source code, compilation, assembly, or verification of
+ * XVM structures.
+ */
+public class ErrorListener
+ {
+ // ----- API -----------------------------------------------------------------------------------
+
+ /**
+ * Handles the logging of an error that originates in Ecstasy source code.
+ *
+ * @param severity the severity level of the error; one of
+ * {@link Severity#INFO}, {@link Severity#WARNING},
+ * {@link Severity#ERROR}, or {@link Severity#FATAL}
+ * @param sCode the error code that identifies the error message
+ * @param aoParam the parameters for the error message; may be null
+ * @param source the source code (optional)
+ * @param lPosStart the position in the source where the error was detected
+ * @param lPosEnd the position in the source at which the error concluded
+ *
+ * @return true to attempt to abort the process that reported the error, or
+ * false to attempt to continue the process
+ */
+ public void log(Severity severity, String sCode, Object[] aoParam,
+ Source source, long lPosStart, long lPosEnd)
+ {
+ System.err.println(new ErrorInfo(severity, sCode, aoParam, source, lPosStart, lPosEnd));
+ }
+
+
+ // ----- inner class: ErrorInfo ----------------------------------------------------------------
+
+ /**
+ * Represents the information logged for a single error.
+ */
+ class ErrorInfo
+ {
+ /**
+ * Construct an ErrorInfo object.
+ *
+ * @param severity the severity level of the error; one of
+ * {@link Severity#INFO}, {@link Severity#WARNING,
+ * {@link Severity#ERROR}, or {@link Severity#FATAL}
+ * @param sCode the error code that identifies the error message
+ * @param aoParam the parameters for the error message; may be null
+ * @param source the source code
+ * @param lPosStart the starting position in the source code
+ * @param lPosEnd the ending position in the source code
+ */
+ public ErrorInfo(Severity severity, String sCode, Object[] aoParam,
+ Source source, long lPosStart, long lPosEnd)
+ {
+ m_severity = severity;
+ m_sCode = sCode;
+ m_aoParam = aoParam;
+ m_source = source;
+ m_lPosStart = lPosStart;
+ m_lPosEnd = lPosEnd;
+ }
+
+ /**
+ * Construct an ErrorInfo object.
+ *
+ * @param severity the severity level of the error; one of
+ * {@link Severity#INFO}, {@link Severity#WARNING,
+ * {@link Severity#ERROR}, or {@link Severity#FATAL}
+ * @param sCode the error code that identifies the error message
+ * @param aoParam the parameters for the error message; may be null
+ */
+ public ErrorInfo(Severity severity, String sCode, Object[] aoParam)
+ {
+ m_severity = severity;
+ m_sCode = sCode;
+ m_aoParam = aoParam;
+ // TODO need to be able to ask the XVM structure for the source & location
+ }
+
+ /**
+ * @return the Severity of the error
+ */
+ public Severity getSeverity()
+ {
+ return m_severity;
+ }
+
+ /**
+ * @return the error code
+ */
+ public String getCode()
+ {
+ return m_sCode;
+ }
+
+ /**
+ * @return the error message parameters
+ */
+ public Object[] getParams()
+ {
+ return m_aoParam;
+ }
+
+ /**
+ * Produce a localized message based on the error code and related parameters.
+ *
+ * @return a formatted message for display that includes the error code
+ */
+ public String getMessage()
+ {
+ return getMessageText();
+ }
+
+ /**
+ * Produce a localized message based on the error code and related parameters.
+ *
+ * @return a formatted message for display that doesn't include the error code
+ */
+ public String getMessageText()
+ {
+ return MessageFormat.format(getCode(), getParams());
+ }
+
+ /**
+ * @return the source code
+ */
+ public Source getSource()
+ {
+ return m_source;
+ }
+
+ /**
+ * @return the line number (zero based) at which the error occurred
+ */
+ public int getLine()
+ {
+ return Source.calculateLine(m_lPosStart);
+ }
+
+ /**
+ * @return the offset (zero based) at which the error occurred
+ */
+ public int getOffset()
+ {
+ return Source.calculateOffset(m_lPosStart);
+ }
+
+ /**
+ * @return the line number (zero based) at which the error concluded
+ */
+ public int getEndLine()
+ {
+ return Source.calculateLine(m_lPosEnd);
+ }
+
+ /**
+ * @return the offset (zero based) at which the error concluded
+ */
+ public int getEndOffset()
+ {
+ return Source.calculateOffset(m_lPosEnd);
+ }
+
+ /**
+ * @return an ID that allows redundant errors to be filtered out
+ */
+ public String genUID()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(m_severity.ordinal())
+ .append(':')
+ .append(m_sCode);
+
+ if (m_aoParam != null)
+ {
+ sb.append('#')
+ .append(Arrays.hashCode(m_aoParam));
+ }
+
+ if (!m_sCode.startsWith("VERIFY"))
+ {
+ if (m_source != null)
+ {
+ sb.append(':')
+ .append(m_source.getFileName())
+ .append(':')
+ .append(m_lPosStart)
+ .append(':')
+ .append(m_lPosStart);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ // source code location
+ if (m_source != null)
+ {
+ String sFile = m_source.getFileName();
+ if (sFile != null)
+ {
+ sb.append(sFile)
+ .append(':').append(getLine() + 1)
+ .append(' ');
+ }
+
+ sb.append("[")
+ .append(getLine() + 1)
+ .append(':')
+ .append(getOffset() + 1);
+
+ if (getEndLine() != getLine() || getEndOffset() != getOffset())
+ {
+ sb.append("..")
+ .append(getEndLine() + 1)
+ .append(':')
+ .append(getEndOffset() + 1);
+ }
+
+ sb.append("] ");
+ }
+
+
+ // localized message
+ sb.append(getMessage());
+
+ // source code snippet
+ if (m_source != null && m_lPosStart != m_lPosEnd)
+ {
+ String sSource = m_source.toString(m_lPosStart, m_lPosEnd);
+ if (sSource.length() > 80)
+ {
+ sSource = sSource.substring(0, 77) + "...";
+ }
+
+ sb.append(" (")
+ .append(quotedString(sSource))
+ .append(')');
+ }
+
+ return sb.toString();
+ }
+
+ private final Severity m_severity;
+ private final String m_sCode;
+ private final Object[] m_aoParam;
+ private Source m_source;
+ private long m_lPosStart;
+ private long m_lPosEnd;
+ }
+
+ } \ No newline at end of file
diff --git a/common/src/main/java/com/seaofnodes/simple/common/Severity.java b/common/src/main/java/com/seaofnodes/simple/common/Severity.java
new file mode 100644
index 00000000..f3325d52
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/simple/common/Severity.java
@@ -0,0 +1,19 @@
+package com.seaofnodes.simple.common;
+
+
+/**
+ * Severity levels.
+ */
+public enum Severity
+ {
+ NONE, INFO, WARNING, ERROR, FATAL;
+
+ public String desc()
+ {
+ String sName = name();
+ StringBuilder sb = new StringBuilder(sName.length());
+ sb.append(sName.charAt(0));
+ sb.append(sName.substring(1).toLowerCase());
+ return sb.toString();
+ }
+ }
diff --git a/common/src/main/java/com/seaofnodes/simple/lexer/aa/Lexer.java b/common/src/main/java/com/seaofnodes/simple/lexer/aa/Lexer.java
new file mode 100644
index 00000000..162cdea1
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/simple/lexer/aa/Lexer.java
@@ -0,0 +1,228 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * The Initial Developer of the Original Software is Cliff Click.
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+package com.seaofnodes.simple.lexer.aa;
+
+import com.seaofnodes.util.AryInt;
+import com.seaofnodes.util.SB;
+
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+
+public class Lexer {
+ private final String _srcName; // Source for error messages; usually a file name
+ private final byte[] _buf;
+ private int _x; // Parser index
+ private int _lastNWS; // Index of last non-white-space char
+ private final AryInt _lines; // char offset of each line
+
+ private final NumberFormat _nf;
+ private final ParsePosition _pp;
+ private final String _str;
+
+ public Lexer(String srcName, String source)
+ {
+ _srcName = srcName;
+ _buf = source.getBytes();
+ _x = 0;
+
+ // Set fields strictly for Java number parsing
+ _nf = NumberFormat.getInstance();
+ _nf.setGroupingUsed(false);
+ _pp = new ParsePosition(0);
+ _str = source; // Keep a complete string copy for java number parsing
+ _lines = new AryInt();//
+ _lines.push(0); // Line 0 at offset 0
+ }
+
+ // Skip WS, return true&skip if match, false & do not skip if miss.
+ public boolean peek( char c ) { return peek1(skipWS(),c); }
+ public boolean peek_noWS( char c ) { return peek1(_x >= _buf.length ? -1 : _buf[_x],c); }
+ // Already skipped WS & have character;
+ // return true & skip if a match, false& do not skip if a miss.
+ public boolean peek1( byte c0, char c ) {
+ assert c0==-1 || c0== _buf[_x];
+ if( c0!=c ) return false;
+ _x++; // Skip peeked character
+ return true;
+ }
+ // Already skipped WS & have character;
+ // return true&skip if match, false & do not skip if miss.
+ public boolean peek2( byte c0, String s2 ) {
+ if( c0 != s2.charAt(0) ) return false;
+ if( _x+1 >= _buf.length || _buf[_x+1] != s2.charAt(1) ) return false;
+ _x+=2; // Skip peeked characters
+ return true;
+ }
+ public boolean peek( String s ) {
+ if( !peek(s.charAt(0)) ) return false;
+ if( !peek_noWS(s.charAt(1)) ) { _x--; return false; }
+ return true;
+ }
+ // Peek 'c' and NOT followed by 'no'
+ public boolean peek_not( char c, char no ) {
+ byte c0 = skipWS();
+ if( c0 != c || (_x+1 < _buf.length && _buf[_x+1] == no) ) return false;
+ _x++;
+ return true;
+ }
+ // Match any of these, and return the match or null
+ public String peek(String[] toks) {
+ for( String tok : toks ) if( peek1(tok) ) return tok;
+ return null;
+ }
+ public boolean peek1(String tok) {
+ for( int i=0; i<tok.length(); i++ )
+ if( _x+i >= _buf.length || _buf[_x+i] != tok.charAt(i) )
+ return false;
+ return true;
+ }
+
+
+ /** Advance parse pointer to the first non-whitespace character, and return
+ * that character, -1 otherwise. */
+ public byte skipWS() {
+ int oldx = _x;
+ while( _x < _buf.length ) {
+ byte c = _buf[_x];
+ if( c=='/' && _x+1 < _buf.length && _buf[_x+1]=='/' ) { skipEOL() ; continue; }
+ if( c=='/' && _x+1 < _buf.length && _buf[_x+1]=='*' ) { skipBlock(); continue; }
+ if( c=='\n' && _x+1 > _lines.last() ) _lines.push(_x+1);
+ if( !isWS(c) ) {
+ if( oldx-1 > _lastNWS && !isWS(_buf[oldx-1]) ) _lastNWS = oldx-1;
+ return c;
+ }
+ _x++;
+ }
+ return -1;
+ }
+ public void skipEOL () { while( _x < _buf.length && _buf[_x] != '\n' ) _x++; }
+ public void skipBlock() { throw new UnsupportedOperationException("Block comments not yet implemented"); }
+ // Advance parse pointer to next WS. Used for parser syntax error recovery.
+ public void skipNonWS() {
+ while( _x < _buf.length && !isWS(_buf[_x]) ) _x++;
+ }
+
+ /** Return true if `c` passes a test */
+ public static boolean isWS (byte c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }
+ public static boolean isAlpha0(byte c) { return ('a'<=c && c <= 'z') || ('A'<=c && c <= 'Z') || (c=='_'); }
+ public static boolean isAlpha1(byte c) { return isAlpha0(c) || ('0'<=c && c <= '9'); }
+ public static boolean isJava (byte c) { return isAlpha1(c) || (c=='$') || (c=='.'); }
+ public static boolean isDigit (byte c) { return '0' <= c && c <= '9'; }
+
+ public String token() { skipWS(); return token0(); }
+ // Lexical tokens. Any alpha, followed by any alphanumerics is a alpha-
+ // token; alpha-tokens end with WS or any operator character. Any collection
+ // of the classic operator characters are a token, except that they will break
+ // un-ambiguously.
+ public String token0() {
+ if( _x >= _buf.length ) return null;
+ byte c=_buf[_x]; int x = _x;
+ if( isOp0(c) || (c=='_' && _x+1 < _buf.length && isOp0(_buf[_x+1])) )
+ while( _x < _buf.length && isOp1(_buf[_x]) ) _x++;
+ else if( isAlpha0(c) )
+ while( _x < _buf.length && isAlpha1(_buf[_x]) ) _x++;
+ else return null; // Not a token; specifically excludes e.g. all bytes >= 128, or most bytes < 32
+ if( (c==':' || c==',') && _x-x==1 ) // Disallow bare ':' as a token; ambiguous with ?: and type annotations; same for ','
+ { _x=x; return null; } // Unwind, not a token
+ if( c=='-' && _x-x>2 && _buf[x+1]=='>' ) // Disallow leading "->", confusing with function parameter list end; eg "not={x->!x}"
+ _x=x+2; // Just return the "->"
+ return new String(_buf,x,_x-x);
+ }
+ public boolean isOp(String s) {
+ if( s==null || s.isEmpty() ) return false;
+ byte c = (byte)s.charAt(0);
+ if( !isOp0(c) && (c!='_' || !isOp0((byte)s.charAt(1))) ) return false;
+ for( int i=1; i<s.length(); i++ )
+ if( !isOp1((byte)s.charAt(i)) ) return false;
+ return true;
+ }
+
+ // Parse a number; WS already skipped and sitting at a digit. Relies on
+ // Javas number parsing.
+ public Number number() {
+ _pp.setIndex(_x);
+ Number n = _nf.parse(_str,_pp);
+ _x = _pp.getIndex();
+ if( n instanceof Long ) {
+ if( _buf[_x-1]=='.' ) _x--; // Java default parser allows "1." as an integer; pushback the '.'
+ }
+ return n;
+ }
+
+ public int field_number() {
+ byte c = _buf[_x];
+ if( !isDigit(c) ) return -1;
+ _x++;
+ int sum = c-'0';
+ while( _x < _buf.length && isDigit(c=_buf[_x]) ) {
+ _x++;
+ sum = sum*10+c-'0';
+ }
+ return sum;
+ }
+
+ /** Parse a String; _x is at '"'.
+ * str = [.\%]* // String contents; \t\n\r\% standard escapes
+ * str = %[num]?[.num]?fact // Percent escape embeds a 'fact' in a string; "name=%name\n"
+ */
+ public String string() {
+ int oldx = ++_x;
+ byte c;
+ while( (c=_buf[_x++]) != '"' ) {
+ if( c=='%' ) throw new UnsupportedOperationException("Not implemented");
+ if( c=='\\' ) throw new UnsupportedOperationException("Not implemented");
+ if( _x == _buf.length ) return null;
+ }
+ return new String(_buf,oldx,_x-oldx-1).intern();
+ }
+
+ public static boolean isOp0(byte c) { return "!#$%*+-=<>^[]~/&|".indexOf(c) != -1; }
+ public static boolean isOp1(byte c) { return isOp0(c) || ":?_{}".indexOf(c) != -1; }
+
+ public String errLocMsg(String s) {
+ if( s.charAt(0)=='\n' ) return s;
+ // find line start
+ int a=_x;
+ while( a > 0 && _buf[a-1] != '\n' ) --a;
+ if( _buf[a]=='\r' ) a++; // do not include leading \n or \n\r
+ // find line end
+ int b=_x;
+ while( b < _buf.length && _buf[b] != '\n' ) b++;
+ if( b < _buf.length ) b--; // do not include trailing \n or \n\r
+ // Find line number. Bin-search returns the insertion-point, which is the NEXT
+ // line unless _x is exactly a line start.
+ int line = _lines.binary_search(_x); // Find zero-based line insertion point
+ if( line == _lines._len || _lines.at(line)>_x ) line--;
+ // error message using 1-based line
+ SB sb = new SB().p(_srcName).p(':').p(line+1).p(':').p(s).nl();
+ sb.p(new String(_buf,a,b-a)).nl();
+ int line_start = a;
+ for( int i=line_start; i<_x; i++ )
+ sb.p(' ');
+ return sb.p('^').nl().toString();
+ }
+
+ @Override public String toString() { return new String(_buf,_x,_buf.length-_x); }
+ @Override public boolean equals(Object loc) {
+ if( this==loc ) return true;
+ if( !(loc instanceof Lexer p) ) return false;
+ return _x==p._x && _srcName.equals(p._srcName);
+ }
+ @Override public int hashCode() {
+ return _srcName.hashCode()+_x;
+ }
+}
diff --git a/common/src/main/java/com/seaofnodes/simple/lexer/ecstasy/Lexer.java b/common/src/main/java/com/seaofnodes/simple/lexer/ecstasy/Lexer.java
new file mode 100644
index 00000000..90571ffd
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/simple/lexer/ecstasy/Lexer.java
@@ -0,0 +1,3041 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * The Initial Developer of the Original Software is
+ * The Ecstasy Project (https://github.com/xtclang/).
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+package com.seaofnodes.simple.lexer.ecstasy;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import java.net.IDN;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import java.util.function.Consumer;
+
+import com.seaofnodes.simple.common.ErrorListener;
+import com.seaofnodes.simple.lexer.ecstasy.Token.Id;
+import com.seaofnodes.util.PackedInteger;
+import com.seaofnodes.simple.common.Severity;
+
+import static com.seaofnodes.util.Handy.*;
+
+/**
+ * An XTC source code parser supporting both demand-based and stream-based
+ * parsing.
+ */
+public class Lexer
+ implements Iterator<Token>
+ {
+ // ----- constructors --------------------------------------------------------------------------
+
+ /**
+ * Construct an XTC lexical analyzer.
+ *
+ * @param source the source to parse
+ */
+ public Lexer(Source source, ErrorListener errorListener)
+ {
+ if (source == null)
+ {
+ throw new IllegalArgumentException("Source required");
+ }
+ if (errorListener == null)
+ {
+ throw new IllegalArgumentException("ErrorListener required");
+ }
+
+ m_source = source;
+ m_errorListener = errorListener;
+
+ eatWhitespace();
+ }
+
+ /**
+ * @param parent a Lexer that this Lexer can delegate to
+ */
+ protected Lexer(Lexer parent)
+ {
+ m_source = parent.m_source;
+ m_errorListener = parent.m_errorListener;
+ m_fWhitespace = parent.m_fWhitespace;
+ }
+
+
+ // ----- Iterator methods ----------------------------------------------------------------------
+
+ @Override
+ public boolean hasNext()
+ {
+ return m_source.hasNext();
+ }
+
+ /**
+ * Validate the specified identifier.
+ *
+ * @param sName the identifier
+ *
+ * @return true iff the identifier is a lexically valid identifier
+ */
+ public static boolean isValidIdentifier(String sName)
+ {
+ if (sName == null)
+ {
+ return false;
+ }
+
+ int cch = sName.length();
+ if (cch == 0)
+ {
+ return false;
+ }
+
+ if (!isIdentifierStart(sName.charAt(0)))
+ {
+ return false;
+ }
+
+ for (int i = 1; i < cch; ++i)
+ {
+ if (!isIdentifierPart(sName.charAt(i)))
+ {
+ return false;
+ }
+ }
+
+ // check if it is a reserved word
+ return Id.valueByText(sName) == null;
+ }
+
+
+ // ----- public API ----------------------------------------------------------------------------
+
+ /**
+ * Lexically analyze the source, emitting a stream of tokens to the specified consumer.
+ *
+ * @param consumer the Token Consumer
+ */
+ public void emit(Consumer<Token> consumer)
+ {
+ while (m_source.hasNext())
+ {
+ consumer.accept(next());
+ }
+ }
+
+ /**
+ * Create a temporary lexer that provides a stream of tokens as specified.
+ *
+ * @param atoken the tokens to stream
+ *
+ * @return a new Lexer
+ */
+ public Lexer createLexer(Token[] atoken)
+ {
+ return new Lexer(this)
+ {
+ int iNext = 0;
+
+ @Override
+ public boolean hasNext()
+ {
+ return iNext < atoken.length;
+ }
+
+ @Override
+ public Token next()
+ {
+ if (hasNext())
+ {
+ return atoken[iNext++];
+ }
+ throw new NoSuchElementException();
+ }
+
+ @Override
+ public long mark()
+ {
+ return iNext;
+ }
+
+ @Override
+ public void restore(long lPos)
+ {
+ iNext = (int) lPos;
+ }
+ };
+ }
+
+
+ // ----- internal ------------------------------------------------------------------------------
+
+ /**
+ * Validate the specified RFC1035 label.
+ *
+ * @param sName the RFC1035 label
+ *
+ * @return true iff the name is a lexically valid RFC1035 label
+ */
+ public static boolean isValidRFC1035Label(String sName)
+ {
+ if (sName == null)
+ {
+ return false;
+ }
+
+ // internationalization support; see https://tools.ietf.org/html/rfc5894
+ // convert the label to an ASCII label
+ try
+ {
+ sName = IDN.toASCII(sName);
+ }
+ catch (IllegalArgumentException e)
+ {
+ return false;
+ }
+
+ int cch = sName.length();
+ if (cch == 0 || cch > 63)
+ {
+ return false;
+ }
+
+ if (!isAsciiLetter(sName.charAt(0)))
+ {
+ return false;
+ }
+
+ for (int i = 1; i < cch; ++i)
+ {
+ char ch = sName.charAt(i);
+ if (!(isAsciiLetter(ch) || isDigit(ch) || i < cch - 1 && ch == '-'))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate the specified module name.
+ *
+ * @param sName the module name
+ *
+ * @return true iff the name is a lexically valid qualified module name
+ */
+ public static boolean isValidQualifiedModule(String sName)
+ {
+ if (sName == null)
+ {
+ return false;
+ }
+
+ String[] asName = parseDelimitedString(sName, '.');
+ int cNames = asName.length;
+ if (cNames < 1)
+ {
+ return false;
+ }
+
+ // the first simple name must be a valid identifier
+ if (!isValidIdentifier(asName[0]))
+ {
+ return false;
+ }
+
+ // the identifier must be followed by a domain name, which is composed of at least two
+ // RFC1035 labels
+ if (cNames == 2)
+ {
+ return false;
+ }
+
+ // check the optional domain name
+ for (int i = 1; i < cNames; ++i)
+ {
+ if (!isValidRFC1035Label(asName[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public Token next()
+ {
+ boolean fWhitespaceBefore = m_fWhitespace;
+ Token token = eatToken();
+ boolean fWhitespaceAfter = eatWhitespace();
+ token.noteWhitespace(fWhitespaceBefore, fWhitespaceAfter);
+ return token;
+ }
+
+ /**
+ * Eat a string literal.
+ *
+ * @return a string literal as a token
+ */
+ protected Token eatStringLiteral(long lInitPos)
+ {
+ return eatStringChars(lInitPos, false, false);
+ }
+
+ /**
+ * Eat a string literal that is in the freeform/multi-line form.
+ *
+ * @return a string literal as a token
+ */
+ protected Token eatMultilineLiteral(long lInitPos)
+ {
+ return eatStringChars(lInitPos, false, true);
+ }
+
+ /**
+ * Eat a template literal. A template literal is a string literal that may contain expressions;
+ * for example:
+ * <blockquote><code><pre>
+ * $"x={x}"
+ * </pre></code></blockquote>
+ *
+ * @param lInitPos the start of the template literal
+ *
+ * @return the resulting token
+ */
+ protected Token eatTemplateLiteral(long lInitPos)
+ {
+ return eatStringChars(lInitPos, true, false);
+ }
+
+ /**
+ * Eat a template literal that uses the multi-line format. A template literal is a string
+ * literal that may contain expressions; for example:
+ * <blockquote><code><pre>
+ * $|# TOML doc
+ * |[name]
+ * |first = "{person.firstname}"
+ * |last = "{person.lastname}"
+ * </pre></code></blockquote>
+ *
+ * @param lInitPos the start of the template literal
+ *
+ * @return the resulting token
+ */
+ protected Token eatMultilineTemplateLiteral(long lInitPos)
+ {
+ return eatStringChars(lInitPos, true, true);
+ }
+
+ /**
+ * Eat the characters defined as whitespace, which include line terminators and the file
+ * terminator. Whitespace does not include comments.
+ */
+ protected boolean eatWhitespace()
+ {
+ boolean fWhitespace = false;
+ Source source = m_source;
+ while (source.hasNext())
+ {
+ if (isWhitespace(nextChar()))
+ {
+ fWhitespace = true;
+ }
+ else
+ {
+ // put back the non-whitespace character
+ source.rewind();
+ break;
+ }
+ }
+ m_fWhitespace = fWhitespace;
+ return fWhitespace;
+ }
+
+ protected boolean isMultilineContinued()
+ {
+ long lPrev = mark();
+ Source source = m_source;
+ while (source.hasNext())
+ {
+ char ch = source.next();
+ if (!isWhitespace(ch))
+ {
+ if (ch == '|')
+ {
+ return true;
+ }
+ break;
+ }
+ }
+
+ restore(lPrev);
+ return false;
+ }
+
+ /**
+ * Eat an inline template expression that begins with a '{' and ends with a '}'. The contents
+ * <b>between</b> the opening and closing curlies are returned as an array of tokens.
+ *
+ * @param fMultiline true iff this is a multi-line format
+ *
+ * @return the tokens found between the opening and closing curly braces
+ */
+ protected Token[] eatTemplateExpression(boolean fMultiline)
+ {
+ int nPrevLine = m_source.getLine();
+ long lInitPos = m_source.getPosition();
+ expect('{');
+ long lPrevPos = mark();
+ int cDepth = 1;
+ ArrayList<Token> tokens = new ArrayList<>();
+ while (true)
+ {
+ Token token = next();
+ int nLine = Source.calculateLine(token.getEndPosition());
+ if (nLine != nPrevLine)
+ {
+ if (!fMultiline || token.getId() != Id.BIT_OR
+ || Source.calculateLine(token.getStartPosition()) != nLine)
+ {
+ // either it's not a multi-line literal, or the new line doesn't start with '|',
+ // or a token crosses a line boundary; log error: newline in string
+ restore(lPrevPos);
+ log(Severity.ERROR, STRING_NO_TERM, null, lInitPos);
+ return tokens.toArray(new Token[0]);
+ }
+
+ nPrevLine = nLine;
+ continue; // skip this '|' token
+ }
+
+ switch (token.getId())
+ {
+ case L_CURLY:
+ ++cDepth;
+ break;
+
+ case R_CURLY:
+ if (--cDepth <= 0)
+ {
+ if (token.hasTrailingWhitespace())
+ {
+ // don't steal the whitespace; we're inside a literal!
+ m_source.setPosition(token.getEndPosition());
+ }
+
+ return tokens.toArray(new Token[0]);
+ }
+ break;
+ }
+
+ tokens.add(token);
+ lPrevPos = mark();
+ }
+ }
+
+ /**
+ * Parse the binary literal value.
+ *
+ * @param lInitPos the location of the "#" at the start of the binary literal value
+ * @param fMultiline true iff the format is a multiline format
+ *
+ * @return the binary literal
+ */
+ protected Token eatBinaryLiteral(long lInitPos, boolean fMultiline)
+ {
+ StringBuilder sb = new StringBuilder();
+ Source source = m_source;
+ boolean fFirst = true;
+ while (source.hasNext())
+ {
+ char ch = source.next();
+ if (isHexit(ch))
+ {
+ sb.append(ch);
+ }
+ else if (ch == '_')
+ {
+ if (fFirst)
+ {
+ // it's an error to start with an underscore
+ log(Severity.ERROR, ILLEGAL_HEX, new Object[] {String.valueOf(ch)}, lInitPos);
+ }
+ // ignore the _ (it's used for spacing within the literal)
+ }
+ else if (!fMultiline)
+ {
+ // the first non-hexit / non-underscore character indicates an end-of-value
+ source.rewind();
+ break;
+ }
+ else if (isWhitespace(ch))
+ {
+ // ignore whitespace, unless it's newline
+ if (isLineTerminator(ch) && !isMultilineContinued())
+ {
+ source.rewind();
+ break;
+ }
+ }
+ else
+ {
+ // error
+ log(Severity.ERROR, ILLEGAL_HEX, new Object[] {String.valueOf(ch)}, lInitPos);
+ source.rewind();
+ break;
+ }
+
+ fFirst = false;
+ }
+
+ return new Token(lInitPos, source.getPosition(), Id.LIT_BINSTR,
+ toBinary(sb.toString().toCharArray()));
+ }
+
+ protected byte[] toBinary(char[] ach)
+ {
+ int cch = ach.length;
+ int ofch = 0;
+ int cb = (cch + 1) / 2;
+ int ofb = 0;
+ byte[] ab = new byte[cb];
+ if ((cch & 0x1) != 0)
+ {
+ // odd number of characters means that the first nibble is a pre-pended zero
+ ab[ofb++] = (byte) hexitValue(ach[ofch++]);
+ }
+ while (ofb < cb)
+ {
+ ab[ofb++] = (byte) ( (hexitValue(ach[ofch++]) << 4)
+ + hexitValue(ach[ofch++]) );
+ }
+
+ return ab;
+ }
+
+ /**
+ * Parse a single token.
+ *
+ * @return the next Token
+ */
+ protected Token eatToken()
+ {
+ Source source = m_source;
+ long lInitPos = source.getPosition();
+ char chInit = nextChar();
+
+ switch (chInit)
+ {
+ case '{':
+ return new Token(lInitPos, source.getPosition(), Id.L_CURLY);
+ case '}':
+ return new Token(lInitPos, source.getPosition(), Id.R_CURLY);
+ case '(':
+ return new Token(lInitPos, source.getPosition(), Id.L_PAREN);
+ case ')':
+ return new Token(lInitPos, source.getPosition(), Id.R_PAREN);
+ case '[':
+ return new Token(lInitPos, source.getPosition(), Id.L_SQUARE);
+ case ']':
+ return new Token(lInitPos, source.getPosition(), Id.R_SQUARE);
+
+ case ';':
+ return new Token(lInitPos, source.getPosition(), Id.SEMICOLON);
+ case ',':
+ return new Token(lInitPos, source.getPosition(), Id.COMMA);
+
+ case '.':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '.':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '/':
+ return new Token(lInitPos, source.getPosition(), Id.DIR_PARENT);
+
+ case '<':
+ return new Token(lInitPos, source.getPosition(), Id.I_RANGE_E);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.I_RANGE_I);
+
+ case '/':
+ return new Token(lInitPos, source.getPosition(), Id.DIR_CUR);
+
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ source.rewind();
+ source.rewind();
+ return eatNumericLiteral();
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.DOT);
+
+ case '$':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '\"':
+ return eatTemplateLiteral(lInitPos);
+
+ case '|':
+ return eatMultilineTemplateLiteral(lInitPos);
+
+ case '/':
+ // it is a file name
+ source.rewind();
+ return new Token(lInitPos, source.getPosition(), Id.STR_FILE);
+
+ case '.':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '.':
+ if (source.hasNext())
+ {
+ if (nextChar() == '/')
+ {
+ // it is a file name starting with "../"
+ source.rewind();
+ source.rewind();
+ source.rewind();
+ return new Token(lInitPos, source.getPosition(), Id.STR_FILE);
+ }
+
+ source.rewind();
+ }
+ break;
+
+ case '/':
+ // it is a file name starting with "./"
+ source.rewind();
+ source.rewind();
+ return new Token(lInitPos, source.getPosition(), Id.STR_FILE);
+ }
+
+ source.rewind();
+ }
+ break;
+ }
+
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.IDENTIFIER, "$");
+
+ case '#':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '.':
+ case '/':
+ // it is a file name
+ source.rewind();
+ return new Token(lInitPos, source.getPosition(), Id.BIN_FILE);
+
+ case '|':
+ return eatBinaryLiteral(lInitPos, true);
+
+ default:
+ // fall through (it will be an error)
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ source.rewind();
+ // fall through
+ }
+ }
+ return eatBinaryLiteral(lInitPos, false);
+
+ case '@':
+ return new Token(lInitPos, source.getPosition(), Id.AT);
+
+ case '?':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '=':
+ return new Token(lInitPos, source.getPosition(), Id.COND_NN_ASN);
+
+ case ':':
+ if (source.hasNext())
+ {
+ if (nextChar() == '=')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.COND_ELSE_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.COND_ELSE);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.COND);
+
+ case ':':
+ if (source.hasNext())
+ {
+ if (nextChar() == '=')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.COND_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.COLON);
+
+ case '+':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '+':
+ return new Token(lInitPos, source.getPosition(), Id.INC);
+
+ case '=':
+ return new Token(lInitPos, source.getPosition(), Id.ADD_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.ADD);
+
+ case '-':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '-':
+ return new Token(lInitPos, source.getPosition(), Id.DEC);
+
+ case '>':
+ return new Token(lInitPos, source.getPosition(), Id.LAMBDA);
+
+ case '=':
+ return new Token(lInitPos, source.getPosition(), Id.SUB_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.SUB);
+
+ case '*':
+ if (source.hasNext())
+ {
+ if (nextChar() == '=')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.MUL_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.MUL);
+
+ case '/':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '/':
+ return eatSingleLineComment(lInitPos);
+
+ case '*':
+ return eatEnclosedComment(lInitPos);
+
+ case '=':
+ return new Token(lInitPos, source.getPosition(), Id.DIV_ASN);
+
+ case '%':
+ return new Token(lInitPos, source.getPosition(), Id.DIVREM);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.DIV);
+
+ case '<':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '<':
+ if (source.hasNext())
+ {
+ if (nextChar() == '=')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.SHL_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.SHL);
+
+ case '-':
+ return new Token(lInitPos, source.getPosition(), Id.ASN_EXPR);
+
+ case '=':
+ if (source.hasNext())
+ {
+ if (nextChar() == '>')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.COMP_ORD);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.COMP_LTEQ);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.COMP_LT);
+
+ case '>':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '>':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '>':
+ if (source.hasNext())
+ {
+ if (nextChar() == '=')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.USHR_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.USHR);
+
+ case '=':
+ return new Token(lInitPos, source.getPosition(), Id.SHR_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.SHR);
+
+ case '.':
+ if (source.hasNext())
+ {
+ if (nextChar() == '.')
+ {
+ if (source.hasNext())
+ {
+ if (nextChar() == '<')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.E_RANGE_E);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.E_RANGE_I);
+ }
+ source.rewind();
+ }
+ break;
+
+ case '=':
+ return new Token(lInitPos, source.getPosition(), Id.COMP_GTEQ);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.COMP_GT);
+
+ case '&':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '&':
+ if (source.hasNext())
+ {
+ if (nextChar() == '=')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.COND_AND_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.COND_AND);
+
+ case '=':
+ return new Token(lInitPos, source.getPosition(), Id.BIT_AND_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.BIT_AND);
+
+ case '|':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '|':
+ if (source.hasNext())
+ {
+ if (nextChar() == '=')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.COND_OR_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.COND_OR);
+
+ case '=':
+ return new Token(lInitPos, source.getPosition(), Id.BIT_OR_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.BIT_OR);
+
+ case '=':
+ if (source.hasNext())
+ {
+ if (nextChar() == '=')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.COMP_EQ);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.ASN);
+
+ case '%':
+ if (source.hasNext())
+ {
+ if (nextChar() == '=')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.MOD_ASN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.MOD);
+
+ case '!':
+ if (source.hasNext())
+ {
+ if (nextChar() == '=')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.COMP_NEQ);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.NOT);
+
+ case '^':
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case '^':
+ return new Token(lInitPos, source.getPosition(), Id.COND_XOR);
+
+ case '=':
+ return new Token(lInitPos, source.getPosition(), Id.BIT_XOR_ASN);
+
+ case '(':
+ return new Token(lInitPos, source.getPosition(), Id.ASYNC_PAREN);
+ }
+ source.rewind();
+ }
+ return new Token(lInitPos, source.getPosition(), Id.BIT_XOR);
+
+ case '~':
+ return new Token(lInitPos, source.getPosition(), Id.BIT_NOT);
+
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ source.rewind();
+ return eatNumericLiteral();
+
+ case '\'':
+ return eatCharLiteral(lInitPos);
+
+ case '\"':
+ return eatStringLiteral(lInitPos);
+
+ case '\\':
+ if (source.hasNext())
+ {
+ if (nextChar() == '|')
+ {
+ return eatMultilineLiteral(lInitPos);
+ }
+ source.rewind();
+ }
+ // fall through
+
+ default:
+ if (!isIdentifierStart(chInit))
+ {
+ log(Severity.ERROR, ILLEGAL_CHAR, new Object[]{quotedChar(chInit)}, lInitPos);
+ }
+ // fall through
+ case 'A':case 'B':case 'C':case 'D':case 'E':case 'F':case 'G':
+ case 'H':case 'I':case 'J':case 'K':case 'L':case 'M':case 'N':
+ case 'O':case 'P':case 'Q':case 'R':case 'S':case 'T':case 'U':
+ case 'V':case 'W':case 'X':case 'Y':case 'Z':
+ case 'a':case 'b':case 'c':case 'd':case 'e':case 'f':case 'g':
+ case 'h':case 'i':case 'j':case 'k':case 'l':case 'm':case 'n':
+ case 'o':case 'p':case 'q':case 'r':case 's':case 't':case 'u':
+ case 'v':case 'w':case 'x':case 'y':case 'z':
+ case '_':
+ {
+ while (source.hasNext())
+ {
+ if (!isIdentifierPart(nextChar()))
+ {
+ source.rewind();
+ break;
+ }
+ }
+
+ String name = extractSource(lInitPos);
+ long lPos = source.getPosition();
+ if (source.hasNext())
+ {
+ char chNext = source.next();
+ if (name.equals(Id.TODO.TEXT))
+ {
+ source.rewind();
+ if (chNext == '(')
+ {
+ return new Token(lInitPos, source.getPosition(), Id.TODO, null);
+ }
+ else
+ {
+ // parse the to-do statement in the same manner as a single line comment
+ Token comment = eatSingleLineComment(lInitPos);
+ return new Token(comment.getStartPosition(), comment.getEndPosition(),
+ Id.TODO, comment.getValue());
+ }
+ }
+ else if (chNext == ':')
+ {
+ if (Token.Id.valueByPrefix(name) != null)
+ {
+ // check for a legal suffix, e.g. "this:private"
+ while (source.hasNext())
+ {
+ if (!isIdentifierPart(nextChar()))
+ {
+ source.rewind();
+ break;
+ }
+ }
+
+ String full = extractSource(lInitPos);
+ if (Id.valueByContextSensitiveText(full) != null)
+ {
+ name = full;
+ }
+ else
+ {
+ // false alarm; back up and just take "this"
+ source.setPosition(lPos);
+ }
+ }
+ else
+ {
+ // check for suffix of private / protected / public / struct (etc.)
+ long lPosSuffix = source.getPosition();
+ while (source.hasNext())
+ {
+ if (!isIdentifierPart(nextChar()))
+ {
+ source.rewind();
+ break;
+ }
+ }
+ String suffix = extractSource(lPosSuffix);
+ source.setPosition(lPosSuffix); // back up past the suffix
+
+ Token.Id idNum = null;
+ boolean fFloat = false;
+ if (Id.valueByText(suffix) == null && source.hasNext() && !isWhitespace(peekChar()))
+ {
+ switch (name)
+ {
+ case "Bit":
+ idNum = Id.LIT_BIT;
+ break;
+
+ case "Nibble":
+ idNum = Id.LIT_NIBBLE;
+ break;
+
+ case "Int":
+ idNum = Id.LIT_INTA;
+ break;
+ case "Int8":
+ idNum = Id.LIT_INT8;
+ break;
+ case "Int16":
+ idNum = Id.LIT_INT16;
+ break;
+ case "Int32":
+ idNum = Id.LIT_INT32;
+ break;
+ case "Int64":
+ idNum = Id.LIT_INT64;
+ break;
+ case "Int128":
+ idNum = Id.LIT_INT128;
+ break;
+ case "IntN":
+ idNum = Id.LIT_INTN;
+ break;
+
+ case "UInt":
+ idNum = Id.LIT_UINTA;
+ break;
+ case "Byte":
+ case "UInt8":
+ idNum = Id.LIT_UINT8;
+ break;
+ case "UInt16":
+ idNum = Id.LIT_UINT16;
+ break;
+ case "UInt32":
+ idNum = Id.LIT_UINT32;
+ break;
+ case "UInt64":
+ idNum = Id.LIT_UINT64;
+ break;
+ case "UInt128":
+ idNum = Id.LIT_UINT128;
+ break;
+ case "UIntN":
+ idNum = Id.LIT_UINTN;
+ break;
+
+ case "Dec":
+ idNum = Id.LIT_DECA;
+ fFloat = true;
+ break;
+ case "Dec32":
+ idNum = Id.LIT_DEC32;
+ fFloat = true;
+ break;
+ case "Dec64":
+ idNum = Id.LIT_DEC64;
+ fFloat = true;
+ break;
+ case "Dec128":
+ idNum = Id.LIT_DEC128;
+ fFloat = true;
+ break;
+ case "DecN":
+ idNum = Id.LIT_DECN;
+ fFloat = true;
+ break;
+
+ case "Float8e4":
+ idNum = Id.LIT_FLOAT8E4;
+ fFloat = true;
+ break;
+ case "Float8e5":
+ idNum = Id.LIT_FLOAT8E5;
+ fFloat = true;
+ break;
+ case "BFloat16":
+ idNum = Id.LIT_BFLOAT16;
+ fFloat = true;
+ break;
+ case "Float16":
+ idNum = Id.LIT_FLOAT16;
+ fFloat = true;
+ break;
+ case "Float32":
+ idNum = Id.LIT_FLOAT32;
+ fFloat = true;
+ break;
+ case "Float64":
+ idNum = Id.LIT_FLOAT64;
+ fFloat = true;
+ break;
+ case "Float128":
+ idNum = Id.LIT_FLOAT128;
+ fFloat = true;
+ break;
+ case "FloatN":
+ idNum = Id.LIT_FLOATN;
+ fFloat = true;
+ break;
+
+ case "Date":
+ return eatDate(lInitPos, false);
+ case "TimeOfDay":
+ return eatTimeOfDay(lInitPos, false);
+ case "Time":
+ return eatTime(lInitPos);
+ case "TimeZone":
+ return eatTimeZone(lInitPos);
+ case "Duration":
+ return eatDuration(lInitPos);
+
+ default:
+ source.rewind(); // back up past the ':'
+ }
+
+ if (idNum != null)
+ {
+ Token tokNum = eatNumericLiteral();
+ switch (tokNum.getId())
+ {
+ case LIT_DEC:
+ case LIT_FLOAT:
+ if (!fFloat)
+ {
+ // we were expecting an integer
+ log(Severity.ERROR, ILLEGAL_NUMBER, new Object[] {
+ extractSource(lInitPos, tokNum.getEndPosition())},
+ lInitPos);
+ return tokNum;
+ }
+ // fall through
+ case LIT_INT:
+ return tokNum.refine(idNum);
+
+ default:
+ throw new IllegalStateException("id=" + idNum);
+ }
+ }
+ }
+ else
+ {
+ source.rewind(); // back up past the ':'
+ }
+ }
+ }
+ else
+ {
+ source.rewind();
+ }
+ }
+
+ Id id = Id.valueByText(name);
+ return new Token(lInitPos, source.getPosition(), id == null ? Id.IDENTIFIER : id, name);
+ }
+ }
+ }
+
+ /**
+ * Eat a character literal.
+ *
+ * @return a character literal as a token
+ */
+ protected Token eatCharLiteral(long lInitPos)
+ {
+ Source source = m_source;
+ long lPosChar = source.getPosition();
+
+ char ch = '?';
+ boolean term = false;
+ if (source.hasNext())
+ {
+ switch (ch = source.next())
+ {
+ case '\'':
+ if (source.hasNext())
+ {
+ if (source.next() == '\'')
+ {
+ // assume the previous one should have been escaped
+ source.rewind();
+ log(Severity.ERROR, CHAR_BAD_ESC, null, lPosChar);
+ }
+ else
+ {
+ // assume the encountered quote that we thought was supposed to be
+ // the character value was instead supposed to be closing quote
+ source.rewind();
+ source.rewind();
+ log(Severity.ERROR, CHAR_NO_CHAR, null, lPosChar, lPosChar);
+ }
+ }
+ break;
+
+ case '\\':
+ // process escaped char
+ switch (ch = source.next())
+ {
+ case '\r':
+ case '\n':
+ case 0x000B: // VT Vertical Tab
+ case 0x000C: // FF Form Feed
+ case 0x0085: // NEL Next Line
+ case 0x2028: // LS Line Separator
+ case 0x2029: // PS Paragraph Separator
+ // log error: newline in string
+ source.rewind();
+ log(Severity.ERROR, CHAR_NO_TERM, null, lInitPos);
+ // assume it wasn't supposed to be an escape
+ ch = '\\';
+ break;
+
+ case '\\':
+ case '\'':
+ case '\"':
+ break;
+
+ case '0':
+ ch = '\000';
+ break;
+ case 'b':
+ ch = '\b';
+ break;
+ case 'd':
+ ch = '\177';
+ break;
+ case 'e':
+ ch = '\033';
+ break;
+ case 'f':
+ ch = '\f';
+ break;
+ case 'n':
+ ch = '\n';
+ break;
+ case 'r':
+ ch = '\r';
+ break;
+ case 't':
+ ch = '\t';
+ break;
+ case 'v':
+ ch = '\013';
+ break;
+ case 'z':
+ ch = '\032';
+ break;
+
+ default:
+ // log error: bad escape
+ log(Severity.ERROR, CHAR_BAD_ESC, null, lPosChar);
+ break;
+ }
+ break;
+
+ case '\r':
+ case '\n':
+ case 0x000B: // VT Vertical Tab
+ case 0x000C: // FF Form Feed
+ case 0x0085: // NEL Next Line
+ case 0x2028: // LS Line Separator
+ case 0x2029: // PS Paragraph Separator
+ // log error: newline in string
+ source.rewind();
+ log(Severity.ERROR, CHAR_NO_TERM, null, lInitPos);
+ break;
+
+ default:
+ break;
+ }
+
+ if (source.hasNext())
+ {
+ if (source.next() == '\'')
+ {
+ term = true;
+ }
+ else
+ {
+ source.rewind();
+ }
+ }
+ }
+
+ if (!term)
+ {
+ // log error: unterminated string
+ log(Severity.ERROR, CHAR_NO_TERM, null, lInitPos);
+ }
+
+ return new Token(lInitPos, source.getPosition(), Id.LIT_CHAR, Character.valueOf(ch));
+ }
+
+ /**
+ * Eat the character contents of a string literal, including the string literal portion(s) of a
+ * template literal.
+ *
+ * @param lInitPos the location of the start of the literal (the position of the {@code $} or
+ * the {@code "})
+ * @param fTemplate true iff this is a template literal
+ * @param fMultiline true iff this is a multi-line format
+ *
+ * @return the resulting token
+ */
+ protected Token eatStringChars(long lInitPos, boolean fTemplate, boolean fMultiline)
+ {
+ Source source = m_source;
+
+ StringBuilder sb = new StringBuilder();
+ ArrayList list = fTemplate ? new ArrayList<>() : null;
+ long lPosStart = lInitPos;
+ Appending: while (true)
+ {
+ if (source.hasNext())
+ {
+ char ch = source.next();
+ switch (ch)
+ {
+ case '\"':
+ if (fMultiline)
+ {
+ sb.append(ch);
+ break;
+ }
+ break Appending;
+
+ case '\\':
+ if (fMultiline)
+ {
+ if (source.hasNext())
+ {
+ if (isLineTerminator(source.next()) && isMultilineContinued())
+ {
+ continue; // Appending;
+ }
+ source.rewind();
+ }
+
+ if (!fTemplate)
+ {
+ sb.append(ch);
+ break;
+ }
+ }
+
+ // process escaped char
+ switch (ch = source.next())
+ {
+ case '\r':
+ case '\n':
+ case 0x000B: // VT Vertical Tab
+ case 0x000C: // FF Form Feed
+ case 0x0085: // NEL Next Line
+ case 0x2028: // LS Line Separator
+ case 0x2029: // PS Paragraph Separator
+ // log error: newline in string
+ source.rewind();
+ log(Severity.ERROR, STRING_NO_TERM, null, lInitPos);
+ // assume it wasn't supposed to be an escape
+ sb.append('\\');
+ break Appending;
+
+ case '\\':
+ sb.append('\\');
+ break;
+ case '\'':
+ sb.append('\'');
+ break;
+ case '\"':
+ sb.append('\"');
+ break;
+
+ case '0':
+ sb.append('\000');
+ break;
+ case 'b':
+ sb.append('\b');
+ break;
+ case 'd':
+ sb.append('\177');
+ break;
+ case 'e':
+ sb.append('\033');
+ break;
+ case 'f':
+ sb.append('\f');
+ break;
+ case 'n':
+ sb.append('\n');
+ break;
+ case 'r':
+ sb.append('\r');
+ break;
+ case 't':
+ sb.append('\t');
+ break;
+ case 'v':
+ sb.append('\013');
+ break;
+ case 'z':
+ sb.append('\032');
+ break;
+
+ case '{':
+ if (fTemplate)
+ {
+ sb.append('{');
+ break;
+ }
+ // fall through
+
+ default:
+ // log error: bad escape
+ long lPosEscEnd = source.getPosition();
+ source.rewind();
+ source.rewind();
+ log(Severity.ERROR, STRING_BAD_ESC, null,
+ source.getPosition(), lPosEscEnd);
+ source.setPosition(lPosEscEnd);
+
+ // assume it wasn't supposed to be an escape:
+ // append both the escape char and the escaped char
+ sb.append('\\')
+ .append(ch);
+ break;
+ }
+ break;
+
+ case '\r':
+ case '\n':
+ case 0x000B: // VT Vertical Tab
+ case 0x000C: // FF Form Feed
+ case 0x0085: // NEL Next Line
+ case 0x2028: // LS Line Separator
+ case 0x2029: // PS Paragraph Separator
+ if (fMultiline)
+ {
+ boolean fCRLF = false;
+ if (ch == '\r' && source.hasNext())
+ {
+ if (source.next() == '\n')
+ {
+ fCRLF = true;
+ }
+ else
+ {
+ source.rewind();
+ }
+ }
+
+ // eat whitespace and look for a continuation character
+ if (isMultilineContinued())
+ {
+ // it is a multi-line continuation, so include the newline that we
+ // just ate in the resulting text
+ if (fCRLF)
+ {
+ sb.append("\r\n");
+ }
+ else
+ {
+ sb.append(ch);
+ }
+ break;
+ }
+ else
+ {
+ // leave the newline in place (it's not part of the multiline)
+ source.rewind();
+ if (fCRLF)
+ {
+ source.rewind();
+ }
+ }
+ }
+ else
+ {
+ // not a multi-line literal; log error: newline in string
+ source.rewind();
+ log(Severity.ERROR, STRING_NO_TERM, null, lInitPos);
+ }
+ break Appending;
+
+ case '{':
+ if (fTemplate)
+ {
+ source.rewind();
+ long lPosEndText = source.getPosition();
+
+ // eat from the opening { to the closing } (inclusive of both)
+ Token[] aTokens = eatTemplateExpression(fMultiline);
+ int cTokens = aTokens.length;
+
+ // if the last token was '=', then add the entire expression up to that
+ // point as literal text, and discard the '='
+ if (cTokens > 1 && aTokens[cTokens-1].getId() == Id.ASN)
+ {
+ Token[] aTokensOld = aTokens;
+ aTokens = new Token[--cTokens];
+ System.arraycopy(aTokensOld, 0, aTokens, 0, cTokens);
+
+ sb.append(source.toString(aTokensOld[0].getStartPosition(),
+ aTokensOld[cTokens].getEndPosition()));
+ }
+
+ if (sb.length() > 0)
+ {
+ list.add(new Token(lPosStart, lPosEndText, Id.LIT_STRING, sb.toString()));
+ sb = new StringBuilder();
+ }
+
+ list.add(aTokens);
+
+ // start eating a new string portion of the template from this point
+ lPosStart = source.getPosition();
+ break;
+ }
+ // fall through
+
+ default:
+ sb.append(ch);
+ break;
+ }
+ }
+ else
+ {
+ // log error: unterminated string
+ log(Severity.ERROR, STRING_NO_TERM, null, lInitPos);
+ }
+ }
+
+ if (fTemplate)
+ {
+ long lPosCur = source.getPosition();
+ if (sb.length() > 0)
+ {
+ list.add(new Token(lPosStart, lPosCur, Id.LIT_STRING, sb.toString()));
+ }
+ else if (list.isEmpty())
+ {
+ return new Token(lPosStart, lPosCur, Id.LIT_STRING, "");
+ }
+ return new Token(lInitPos, lPosCur, Id.TEMPLATE, list.toArray());
+ }
+
+ return new Token(lPosStart, source.getPosition(), Id.LIT_STRING, sb.toString());
+ }
+
+ /**
+ * Eat a numeric literal.
+ *
+ * @return the numeric literal as a Token
+ */
+ protected Token eatNumericLiteral()
+ {
+ Source source = m_source;
+ long lStartPos = source.getPosition();
+
+ // eat the first part of the number (or the entire number, if it is an integer literal)
+ int[] results = new int[2];
+ PackedInteger piWhole = eatIntegerLiteral(results);
+ int mantissaRadix = results[0];
+ int signScalar = results[1];
+
+ // parse optional '.' + value
+ PackedInteger piFraction = null;
+ int fractionalDigits = 0;
+ if (source.hasNext())
+ {
+ if (source.next() == '.')
+ {
+ // could be ".."
+ if (source.hasNext())
+ {
+ boolean fNotDecimal = source.next() == '.';
+ // whatever it was, spit it back out
+ source.rewind();
+
+ // if it's not "..", it could be something else, e.g. a method name on type Int
+ if (!fNotDecimal && !isNextCharDigit(mantissaRadix))
+ {
+ fNotDecimal = true;
+ }
+
+ if (fNotDecimal)
+ {
+ // spit back out the first dot
+ source.rewind();
+
+ return new Token(lStartPos, source.getPosition(), Id.LIT_INT, piWhole);
+ }
+ }
+
+ piFraction = eatDigits(false, mantissaRadix, results);
+ fractionalDigits = results[0];
+
+ if (fractionalDigits == 0)
+ {
+ log(Severity.ERROR, ILLEGAL_NUMBER, lStartPos, lStartPos);
+ }
+ }
+ else
+ {
+ source.rewind();
+ }
+ }
+
+ // parse optional exponent
+ PackedInteger piExp = null;
+ boolean mustBeBinary = false;
+ if (source.hasNext())
+ {
+ char ch = source.next();
+ switch (ch)
+ {
+ case 'E': case 'e':
+ piExp = eatIntegerLiteral(null);
+ break;
+
+ case 'P': case 'p':
+ piExp = eatIntegerLiteral(null);
+ mustBeBinary = true;
+ break;
+
+ default:
+ // anything else should be whitespace or some type of operator/separator
+ long lEndPos = source.getPosition();
+ source.rewind();
+ if (isIdentifierPart(ch))
+ {
+ log(Severity.ERROR, ILLEGAL_NUMBER, lStartPos,
+ source.getPosition(), lEndPos);
+ }
+ break;
+ }
+ }
+
+ long lPosEnd = source.getPosition();
+ if (piFraction == null && piExp == null)
+ {
+ return new Token(lStartPos, lPosEnd, Id.LIT_INT, piWhole);
+ }
+ else if (!mustBeBinary && mantissaRadix == 10)
+ {
+ // convert to IEEE-754 decimal floating point format
+ // note: for now it is simply stored in the literal token as a BigDecimal
+ BigInteger biWhole = piWhole.getBigInteger();
+ BigDecimal dec;
+ if (piFraction == null)
+ {
+ dec = new BigDecimal(biWhole);
+ }
+ else
+ {
+ if (biWhole.signum() < 0)
+ {
+ biWhole = biWhole.negate();
+ }
+ dec = new BigDecimal(biWhole.multiply(BigInteger.TEN.pow(fractionalDigits))
+ .add(piFraction.getBigInteger()), fractionalDigits);
+ if (signScalar < 0)
+ {
+ // the unfortunate side-effect of not having a -0
+ dec = dec.negate();
+ }
+ }
+ if (piExp != null)
+ {
+ long lExp = piExp.getLong();
+ if (lExp > 6144 || lExp < (1-6144))
+ {
+ log(Severity.ERROR, ILLEGAL_NUMBER, lStartPos,
+ lStartPos, lPosEnd);
+ }
+ else
+ {
+ dec = dec.scaleByPowerOfTen((int) lExp);
+ }
+ }
+ return new Token(lStartPos, lPosEnd, Id.LIT_DEC, dec);
+ }
+ else
+ {
+ // convert to IEEE-754 binary floating point format
+ // note: for now it is simply stored in the literal token as a String
+ return new Token(lStartPos, lPosEnd, Id.LIT_FLOAT, extractSource(lStartPos, lPosEnd));
+ }
+ }
+
+ /**
+ * The next character must begin an integer literal. Parse it and return it as a PackedInteger.
+ *
+ * @param otherResults either null, or an array of up to two "int" elements, to allow for
+ * multiple "out" values from this method, the first of which is the radix
+ * and the second is the sign (<tt>-1</tt> or <tt>+1</tt>)
+ *
+ * @return a PackedInteger
+ */
+ protected PackedInteger eatIntegerLiteral(int[] otherResults)
+ {
+ Source source = m_source;
+
+ // the first character could be a sign (+ or -)
+ boolean fNeg = false;
+ char ch = needCharOrElse(ILLEGAL_NUMBER);
+ if (ch == '+' || ch == '-')
+ {
+ fNeg = (ch == '-');
+ ch = needCharOrElse(ILLEGAL_NUMBER);
+ }
+
+ // if the next character is '0', it is potentially part of a prefix denoting a radix
+ int radix = 10;
+ if (ch == '0' && source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case 'B':
+ case 'b':
+ radix = 2;
+ break;
+ case 'o':
+ radix = 8;
+ break;
+ case 'X':
+ case 'x':
+ radix = 16;
+ break;
+ default:
+ source.rewind();
+ source.rewind();
+ break;
+ }
+ }
+ else
+ {
+ source.rewind();
+ }
+
+ // don't you just wish that Java had multiple return values?
+ if (otherResults != null)
+ {
+ if (otherResults.length > 0)
+ {
+ otherResults[0] = radix;
+ }
+ if (otherResults.length > 1)
+ {
+ otherResults[1] = fNeg ? -1 : 1;
+ }
+ }
+
+ PackedInteger pi = eatDigits(fNeg, radix, null);
+
+ PossibleSuffix: if (radix == 10 && source.hasNext())
+ {
+ // allow KI/KB, MI/MB, GI/GB TI/TB, PI/PB, EI/EB, ZI/ZB, YI/YB
+ int iMul;
+ switch (nextChar())
+ {
+ case 'K': case 'k':
+ iMul = 0;
+ break;
+
+ case 'M': case 'm':
+ iMul = 1;
+ break;
+
+ case 'G': case 'g':
+ iMul = 2;
+ break;
+
+ case 'T': case 't':
+ iMul = 3;
+ break;
+
+ case 'P': case 'p':
+ iMul = 4;
+ break;
+
+ case 'E': case 'e':
+ iMul = 5;
+ break;
+
+ case 'Z': case 'z':
+ iMul = 6;
+ break;
+
+ case 'Y': case 'y':
+ iMul = 7;
+ break;
+
+ default:
+ source.rewind();
+ break PossibleSuffix;
+ }
+
+ PackedInteger[] factors = PackedInteger.xB_FACTORS;
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case 'B': case 'b':
+ break;
+
+ case 'I': case 'i':
+ if (source.hasNext())
+ {
+ char optionalB = source.next();
+ if (!(optionalB == 'B' || optionalB == 'b'))
+ {
+ source.rewind();
+ }
+ }
+ factors = PackedInteger.xI_FACTORS;
+ break;
+
+ case '+': case '-':
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ if (iMul == 4 || iMul == 5)
+ {
+ // the "P" or the "E" is for an exponent
+ source.rewind();
+ source.rewind();
+ break PossibleSuffix;
+ }
+ default:
+ source.rewind();
+ break;
+ }
+ }
+
+ pi = pi.mul(factors[iMul]);
+ }
+
+ return pi;
+ }
+
+ /**
+ * @return the value, or a negative value if there was no number or the number overflowed
+ */
+ protected long eatUnsignedLong()
+ {
+ long n = 0;
+ if (isNextCharDigit(10))
+ {
+ do
+ {
+ if (n < 0)
+ {
+ return n;
+ }
+
+ n = n * 10 + (nextChar() - '0');
+ }
+ while (isNextCharDigit(10));
+
+ return n;
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ protected boolean isNextCharDigit(int radix)
+ {
+ boolean fDigit = false;
+
+ Source source = m_source;
+ if (source.hasNext())
+ {
+ switch (nextChar())
+ {
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+ fDigit = radix >= 16;
+ break;
+
+ case '9': case '8':
+ fDigit = radix >= 10;
+ break;
+
+ case '7': case '6': case '5': case '4': case '3': case '2':
+ fDigit = radix >= 8;
+ break;
+
+ case '1':
+ case '0':
+ fDigit = true;
+ break;
+
+ default:
+ break;
+ }
+ source.rewind();
+ }
+
+ return fDigit;
+ }
+
+ /**
+ * The next character must begin a sequence of digits of the specified radix. Parse it and
+ * return it as a PackedInteger.
+ *
+ * @return a PackedInteger
+ */
+ protected PackedInteger eatDigits(boolean fNeg, int radix, int[] digitCount)
+ {
+ long lValue = 0;
+ BigInteger bigint = null; // just in case
+ boolean fError = false;
+ int cDigits = 0;
+
+ Source source = m_source;
+ long lStartPos = source.getPosition();
+ Parsing: while (source.hasNext())
+ {
+ long lPos = source.getPosition();
+ char ch = nextChar();
+ switch (ch)
+ {
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+ if (radix < 16)
+ {
+ // "e" is used as the decimal exponent indicator
+ if (ch != 'E' && ch != 'e')
+ {
+ if (!fError)
+ {
+ log(Severity.ERROR, ILLEGAL_NUMBER, lStartPos, lPos);
+ fError = true;
+ }
+ }
+ source.rewind();
+ break Parsing;
+ }
+ break;
+
+ case '9': case '8':
+ if (radix < 10)
+ {
+ if (!fError)
+ {
+ log(Severity.ERROR, ILLEGAL_NUMBER, lStartPos, lPos);
+ fError = true;
+ }
+ // while an error was encountered, it was at least a digit, so continue
+ // parsing those digits (even if they are bad)
+ continue; // Parsing;
+ }
+ break;
+
+ case '7': case '6': case '5': case '4': case '3': case '2':
+ if (radix < 8)
+ {
+ if (!fError)
+ {
+ log(Severity.ERROR, ILLEGAL_NUMBER, lStartPos, lPos);
+ fError = true;
+ }
+ // while an error was encountered, it was at least a digit, so continue
+ // parsing those digits (even if they are bad)
+ continue; // Parsing;
+ }
+ break;
+
+ case '1': case '0':
+ break;
+
+ case '_':
+ if (cDigits == 0 && !fError)
+ {
+ // it's an error to start the sequence of digits with an underscore
+ log(Severity.ERROR, ILLEGAL_NUMBER, lStartPos, lPos);
+ fError = true;
+ }
+ continue; // Parsing;
+
+ default:
+ // anything else (including '.') means go to the next step
+ source.rewind();
+ break Parsing;
+ }
+
+ if (bigint == null)
+ {
+ lValue = lValue * radix + hexitValue(ch);
+ if (lValue > 0x00FFFFFFFFFFFFFFL)
+ {
+ bigint = BigInteger.valueOf(fNeg ? -lValue : lValue);
+ }
+ }
+ else
+ {
+ bigint = bigint.multiply(BigInteger.valueOf(radix)).add(BigInteger.valueOf(hexitValue(ch)));
+ }
+ ++cDigits;
+ }
+
+ if (!fError && cDigits == 0)
+ {
+ log(Severity.ERROR, ILLEGAL_NUMBER, lStartPos, lStartPos);
+ }
+
+ if (digitCount != null && digitCount.length > 0)
+ {
+ digitCount[0] = cDigits;
+ }
+ return bigint == null ? new PackedInteger(fNeg ? -lValue : lValue) : new PackedInteger(bigint);
+ }
+
+ /**
+ * Eat a literal ISO-8601 datetime value (the Ecstasy "Time" class).
+ *
+ * @param lInitPos the location of the start of the literal token
+ *
+ * @return the literal as a Token
+ */
+ protected Token eatTime(long lInitPos)
+ {
+ Token tokDate = eatDate(lInitPos, true);
+ if (!(match('t') || expect('T')))
+ {
+ log(Severity.ERROR, BAD_TIME, new Object[] {tokDate.getValue()},
+ tokDate.getStartPosition(), tokDate.getEndPosition());
+ return tokDate;
+ }
+
+ Token tokTimeOfDay = eatTimeOfDay(lInitPos, true);
+ long lEndPos = tokTimeOfDay.getEndPosition();
+
+ String sDT = tokDate.getValue() + "T" + tokTimeOfDay.getValue();
+ if (match('Z') || match('z') || match('+') || match('-'))
+ {
+ m_source.rewind();
+ Token tokZone = eatTimeZone(lInitPos);
+ sDT += tokZone.getValue();
+ lEndPos = tokZone.getEndPosition();
+ }
+
+ return new Token(lInitPos, lEndPos, Id.LIT_TIME, sDT);
+ }
+
+ /**
+ * Eat a literal ISO-8601 timezone value.
+ *
+ * @param lInitPos the location of the start of the literal token
+ *
+ * @return the literal as a Token
+ */
+ protected Token eatTimeZone(long lInitPos)
+ {
+ if (match('Z') || match('z'))
+ {
+ peekNotIdentifierOrNumber();
+ return new Token(lInitPos, m_source.getPosition(), Id.LIT_TIMEZONE, "Z");
+ }
+
+ long lStart = m_source.getPosition();
+ int nHour = 0;
+ int nMin = 0;
+ if (match('-') || expect('+'))
+ {
+ if ((nHour = eatDigits(2)) >= 0)
+ {
+ if (match(':') || isNextCharDigit(10))
+ {
+ if ((nMin = eatDigits(2)) >= 0)
+ {
+ peekNotIdentifierOrNumber();
+ }
+ }
+ else
+ {
+ peekNotIdentifierOrNumber();
+ }
+ }
+ }
+ long lEnd = m_source.getPosition();
+ String sZone = extractSource(lStart, lEnd);
+
+ if (nHour >= 0 && nMin >= 0)
+ {
+ if (nHour > 16 || nMin > 59)
+ {
+ log(Severity.ERROR, BAD_TIMEZONE, new Object[] {sZone}, lStart, lEnd);
+ }
+ }
+
+ return new Token(lInitPos, lEnd, Id.LIT_TIMEZONE, sZone);
+ }
+
+ /**
+ * Eat a specified number of decimal digits, and convert them to an integer.
+ *
+ * @param digitCount the number of digits to eat
+ *
+ * @return the parsed integer value, or the negative thereof if an error was encountered
+ */
+ protected int eatDigits(int digitCount)
+ {
+ long lPosStart = m_source.getPosition();
+
+ int n = 0;
+ for (int i = 0; i < digitCount; ++i)
+ {
+ if (!isNextCharDigit(10))
+ {
+ log(Severity.ERROR, EXPECTED_DIGITS, new Object[] {digitCount, i}, lPosStart);
+ return -n;
+ }
+
+ n = n * 10 + (nextChar() - '0');
+ }
+ return n;
+ }
+
+ /**
+ * Eat a literal ISO-8601 date value.
+ *
+ * @param lInitPos the location of the start of the literal token
+ * @param fContinued true if this is just part of a larger literal
+ *
+ * @return the literal as a Token
+ */
+ protected Token eatDate(long lInitPos, boolean fContinued)
+ {
+ Source source = m_source;
+ long lLitPos = source.getPosition();
+
+ int nYear;
+ int nMonth = 0;
+ int nDay = 0;
+
+ if ((nYear = eatDigits(4)) >= 0)
+ {
+ boolean fSep = match('-');
+ if ((nMonth = eatDigits(2)) >= 0)
+ {
+ if (fSep)
+ {
+ expect('-');
+ }
+ if ((nDay = eatDigits(2)) >= 0)
+ {
+ if (!fContinued)
+ {
+ peekNotIdentifierOrNumber();
+ }
+ }
+ }
+ }
+
+ long lEndPos = source.getPosition();
+ String sDate = extractSource(lLitPos, lEndPos);
+ if (nYear >= 0 && nMonth >= 0 && nDay >= 0)
+ {
+ if (nYear < 1582 || nMonth < 1 || nMonth > 12 || nDay < 1 || nDay > 31)
+ {
+ log(Severity.ERROR, BAD_DATE, new Object[] {sDate}, lLitPos, lEndPos);
+ }
+ }
+
+ return new Token(lInitPos, lEndPos, Id.LIT_DATE, sDate);
+ }
+
+ /**
+ * Eat a literal ISO-8601 time value (the Ecstasy "TimeOfDay" class).
+ *
+ * @param lInitPos the location of the start of the literal token
+ * @param fContinued true if this is just part of a larger literal
+ *
+ * @return the literal as a Token
+ */
+ protected Token eatTimeOfDay(long lInitPos, boolean fContinued)
+ {
+ Source source = m_source;
+ long lStart = source.getPosition();
+
+ int nHour;
+ int nMin = 0;
+ int nSec = 0;
+ long nPicos = 0;
+ int cDigits;
+
+ if ((nHour = eatDigits(2)) >= 0)
+ {
+ boolean fSep = match(':');
+ if ((nMin = eatDigits(2)) >= 0)
+ {
+ if (fSep && match(':') || !fSep && isNextCharDigit(10))
+ {
+ nSec = eatDigits(2);
+ if (match('.'))
+ {
+ if (isNextCharDigit(10))
+ {
+ cDigits = 0;
+ do
+ {
+ char ch = nextChar();
+ if (++cDigits <= 12)
+ {
+ nPicos = nPicos * 10 + (ch - '0');
+ }
+ }
+ while (isNextCharDigit(10));
+
+ // scale the integer value up to picos (trillionths)
+ while (++cDigits <= 12)
+ {
+ nPicos *= 10;
+ }
+ }
+ else
+ {
+ // the dot isn't part of the time-of-day
+ source.rewind();
+ }
+ }
+ }
+ if (!fContinued)
+ {
+ peekNotIdentifierOrNumber();
+ }
+ }
+ }
+
+ long lEnd = source.getPosition();
+ String sTime = extractSource(lStart, lEnd);
+ if (nHour >= 0 && nMin >= 0 && nSec >= 0)
+ {
+ if (nHour > 23 || nMin > 59 || nSec > 59 && !(nHour == 23 && nMin == 59 && nSec == 60))
+ {
+ log(Severity.ERROR, BAD_TIME_OF_DAY, new Object[] {sTime}, lStart, lEnd);
+ }
+ }
+
+ return new Token(lInitPos, lEnd, Id.LIT_TIMEOFDAY, sTime);
+ }
+
+ /**
+ * Obtain a cookie that represents the Lexer's current location.
+ *
+ * @return a position cookie
+ */
+ public long mark()
+ {
+ // this adds a bit of information to the source's position info
+ long lPos = m_source.getPosition();
+ assert (lPos & (1L << 63)) == 0L;
+ if (m_fWhitespace)
+ {
+ lPos |= (1L << 63);
+ }
+ return lPos;
+ }
+
+ /**
+ * Using a previously returned Lexer position cookie, restore that state of the lexer.
+ *
+ * @param lPos a position cookie previously returned from {@link #mark()}
+ */
+ public void restore(long lPos)
+ {
+ m_fWhitespace = (lPos & (1L << 63)) != 0L;
+ m_source.setPosition(lPos & ~(1L << 63));
+ }
+
+ /**
+ * Obtain the character located at a previously returned position cookie, but the state of the
+ * Lexer after this call must match that from before the call.
+ *
+ * @param lPos a previously returned position cookie
+ *
+ * @return the character at the specified position
+ */
+ public char charAt(long lPos)
+ {
+ long lPrev = mark();
+ restore(lPos);
+ char ch = nextChar();
+ restore(lPrev);
+ return ch;
+ }
+
+ /**
+ * Eat a literal ISO-8601 duration value.
+ *
+ * @param lInitPos the location of the start of the literal token
+ *
+ * @return the literal as a Token
+ */
+ protected Token eatDuration(long lInitPos)
+ {
+ Source source = m_source;
+ long lStart = source.getPosition();
+
+ // Stages:
+ // Y, M, D, (T), H, M, S
+ // 1 2 3 4 5 6 7 . 8
+
+ boolean fErr = false;
+ if (!match('P'))
+ {
+ match('p');
+ }
+
+ int nPrevStage = match('T') ? 4 : 0;
+ Loop: while (true)
+ {
+ long lPos = source.getPosition();
+ long lVal = eatUnsignedLong();
+ if (lVal < 0)
+ {
+ source.setPosition(lPos);
+ break;
+ }
+
+ char ch = nextChar();
+ switch (ch)
+ {
+ case 'Y':
+ case 'y':
+ if (nPrevStage >= 1)
+ {
+ fErr = true;
+ }
+ nPrevStage = Math.max(1, nPrevStage);
+ break;
+
+ case 'M':
+ case 'm':
+ if (nPrevStage >= 2)
+ {
+ // parse as minute
+ if (nPrevStage < 4)
+ {
+ fErr = true;
+ }
+ nPrevStage = Math.max(6, nPrevStage);
+ }
+ else
+ {
+ // parse as month
+ nPrevStage = Math.max(2, nPrevStage);
+ }
+ break;
+
+ case 'D':
+ case 'd':
+ if (nPrevStage >= 3)
+ {
+ fErr = true;
+ }
+ nPrevStage = Math.max(3, nPrevStage);
+ break;
+
+ case 'H':
+ case 'h':
+ if (nPrevStage >= 5)
+ {
+ fErr = true;
+ }
+ nPrevStage = Math.max(5, nPrevStage);
+ break;
+
+ case 'S':
+ case 's':
+ break Loop;
+
+ case '.':
+ if (nPrevStage >= 7)
+ {
+ fErr = true;
+ }
+ nPrevStage = Math.max(7, nPrevStage);
+ break;
+
+ default:
+ source.rewind();
+ fErr = true;
+ break Loop;
+ }
+
+ if (match('T') || match('t'))
+ {
+ if (nPrevStage >= 4)
+ {
+ fErr = true;
+ }
+ else
+ {
+ nPrevStage = 4;
+ }
+ }
+ }
+
+ long lEnd = source.getPosition();
+ String sDuration = extractSource(lStart, lEnd).toUpperCase();
+
+ if (fErr)
+ {
+ log(Severity.ERROR, BAD_DURATION, new Object[] {sDuration}, lStart, lEnd);
+ }
+ else
+ {
+ peekNotIdentifierOrNumber();
+ }
+
+ return new Token(lStart, lEnd, Id.LIT_DURATION, sDuration);
+ }
+
+ /**
+ * Eat a single line aka end-of-line comment.
+ *
+ * @return the comment as a Token
+ */
+ protected Token eatSingleLineComment(long lPosTokenStart)
+ {
+ Source source = m_source;
+ long lPosTextStart = source.getPosition();
+ while (source.hasNext())
+ {
+ if (isLineTerminator(nextChar()))
+ {
+ source.rewind();
+ break;
+ }
+ }
+ long lPosEnd = source.getPosition();
+ return new Token(lPosTokenStart, lPosEnd, Id.EOL_COMMENT,
+ extractSource(lPosTextStart, lPosEnd));
+ }
+
+ /**
+ * Eat a multi-line aka enclosed comment.
+ *
+ * @return the comment as a Token
+ */
+ protected Token eatEnclosedComment(long lPosTokenStart)
+ {
+ Source source = m_source;
+ long lPosTextStart = source.getPosition();
+
+ boolean fAsterisk = false;
+ while (source.hasNext())
+ {
+ char chNext = nextChar();
+ if (chNext == '*')
+ {
+ fAsterisk = true;
+ }
+ else if (fAsterisk && chNext == '/')
+ {
+ long lPosTokenEnd = source.getPosition();
+ source.rewind();
+ source.rewind();
+ long lPosTextEnd = source.getPosition();
+ source.setPosition(lPosTokenEnd);
+ return new Token(lPosTokenStart, lPosTokenEnd, Id.ENC_COMMENT,
+ extractSource(lPosTextStart, lPosTextEnd));
+ }
+ else
+ {
+ fAsterisk = false;
+ }
+ }
+
+ // missing the enclosing "*/"
+ log(Severity.ERROR, EXPECTED_ENDCOMMENT, null, lPosTextStart);
+
+ // just pretend that the rest of the file was all one big comment
+ long lPosEnd = source.getPosition();
+ return new Token(lPosTokenStart, lPosEnd, Id.ENC_COMMENT,
+ extractSource(lPosTextStart, lPosEnd));
+ }
+
+ /**
+ * Get the next character of source code, making sure that it is the specified character.
+ *
+ * @param ch the desired character
+ *
+ * @return true iff the next character was matched
+ */
+ protected boolean match(char ch)
+ {
+ char chActual;
+ try
+ {
+ chActual = nextChar();
+ }
+ catch (NoSuchElementException e)
+ {
+ log(Severity.ERROR, UNEXPECTED_EOF, null, m_source.getPosition());
+ return false;
+ }
+
+ if (chActual != ch)
+ {
+ m_source.rewind();
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the next character of source code, making sure that it is the specified character.
+ *
+ * @param ch the expected character
+ *
+ * @return true iff the next character was found
+ */
+ protected boolean expect(char ch)
+ {
+ char chActual;
+ try
+ {
+ chActual = nextChar();
+ }
+ catch (NoSuchElementException e)
+ {
+ log(Severity.ERROR, UNEXPECTED_EOF, null, m_source.getPosition());
+ return false;
+ }
+
+ if (chActual != ch)
+ {
+ log(Severity.ERROR, EXPECTED_CHAR,
+ new Object[] {String.valueOf(ch), String.valueOf(chActual)},
+ m_source.getPosition());
+ return false;
+ }
+
+ return true;
+ }
+
+ protected boolean matchCaseInsens(String s)
+ {
+ for (int i = 0, c = s.length(); i < c; ++i)
+ {
+ char ch = s.charAt(i);
+ if (match(ch) || match(Character.toLowerCase(ch)) || match(Character.toUpperCase(ch)))
+ {
+ continue;
+ }
+ return false;
+ }
+
+ char ch = peekChar();
+ return !(isIdentifierPart(ch) && !isDigit(ch));
+ }
+
+ /**
+ * Verify that the next character is NOT a number or an identifier char.
+ */
+ protected void peekNotIdentifierOrNumber()
+ {
+ char ch = nextChar();
+ long lEnd = m_source.getPosition();
+ m_source.rewind();
+ if (ch >= '0' && ch <= '9' || isIdentifierPart(ch))
+ {
+ log(Severity.ERROR, UNEXPECTED_CHAR, new Object[] {ch}, m_source.getPosition(), lEnd);
+ }
+ }
+
+ protected String extractSource(long lPosStart)
+ {
+ return extractSource(lPosStart, m_source.getPosition());
+ }
+
+ protected String extractSource(long lPosStart, long lPosEnd)
+ {
+ return m_source.toString(lPosStart, lPosEnd);
+ }
+
+ /**
+ * Log an error (without error params) using the current position in the script as the end
+ * position for the error.
+ */
+ public void log(Severity severity, String sCode, long lPosPrev, long lPosStart)
+ {
+ log(severity, sCode, lPosPrev, lPosStart, m_source.getPosition());
+ }
+
+ /**
+ * Log an error (without error params).
+ */
+ protected void log(Severity severity, String sCode, long lPosPrev, long lPosStart, long lPosEnd)
+ {
+ if (lPosPrev == lPosStart)
+ {
+ m_source.rewind();
+ lPosPrev = m_source.getPosition();
+ m_source.next();
+ }
+
+ log(severity, sCode, new Object[]{extractSource(lPosPrev, lPosEnd)}, lPosStart, lPosEnd);
+ }
+
+ /**
+ * Log an error using the current position in the script as the end position for the error.
+ */
+ protected void log(Severity severity, String sCode, Object[] aoParam, long lPosStart)
+ {
+ log(severity, sCode, aoParam, lPosStart, m_source.getPosition());
+ }
+
+ /**
+ * Log an error.
+ */
+ protected void log(Severity severity, String sCode, Object[] aoParam, long lPosStart, long lPosEnd)
+ {
+ m_errorListener.log(severity, sCode, aoParam, m_source, lPosStart, lPosEnd);
+ }
+
+
+ // ----- helper methods ------------------------------------------------------------------------
+
+ /**
+ * Determine if the specified character is white-space.
+ *
+ * @param ch the character to evaluate
+ *
+ * @return true iff the character is defined as an XTC <i>SpacingElement</i>
+ */
+ public static boolean isWhitespace(char ch)
+ {
+ // optimize for the ASCII range
+ if (ch < 128)
+ {
+ // this handles the following cases:
+ // U+0009 9 HT Horizontal Tab
+ // U+000A 10 LF Line Feed
+ // U+000B 11 VT Vertical Tab
+ // U+000C 12 FF Form Feed
+ // U+000D 13 CR Carriage Return
+ // U+001A 26 SUB End-of-File, or “control-Z”
+ // U+001C 28 FS File Separator
+ // U+001D 29 GS Group Separator
+ // U+001E 30 RS Record Separator
+ // U+001F 31 US Unit Separator
+ // U+0020 32 SP Space
+ // 2 1 0
+ // 0FEDCBA9876543210FEDCBA9
+ return ch <= 32 && ch >= 9 && ((1 << (ch-9)) & 0b111110100000000000011111) != 0;
+ }
+
+ // this handles the following cases:
+ // U+0085 133 NEL Next Line
+ // U+00A0 160 &nbsp; Non-breaking space
+ // U+1680 5760 Ogham Space Mark
+ // U+2000 8192 En Quad
+ // U+2001 8193 Em Quad
+ // U+2002 8194 En Space
+ // U+2003 8195 Em Space
+ // U+2004 8196 Three-Per-Em Space
+ // U+2005 8197 Four-Per-Em Space
+ // U+2006 8198 Six-Per-Em Space
+ // U+2007 8199 Figure Space
+ // U+2008 8200 Punctuation Space
+ // U+2009 8201 Thin Space
+ // U+200A 8202 Hair Space
+ // U+2028 8232 LS Line Separator
+ // U+2029 8233 PS Paragraph Separator
+ // U+202F 8239 Narrow No-Break Space
+ switch (ch)
+ {
+ case 0x0085:
+ case 0x00A0:
+ case 0x1680:
+ case 0x2000:
+ case 0x2001:
+ case 0x2002:
+ case 0x2003:
+ case 0x2004:
+ case 0x2005:
+ case 0x2006:
+ case 0x2007:
+ case 0x2008:
+ case 0x2009:
+ case 0x200A:
+ case 0x2028:
+ case 0x2029:
+ case 0x202F:
+ case 0x205F:
+ case 0x3000:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Determine if the specified character is a line terminator.
+ *
+ * @param ch the character to evaluate
+ *
+ * @return true iff the character is defined as an XTC <i>LineTerminator</i>
+ */
+ public static boolean isLineTerminator(char ch)
+ {
+ // optimize for the ASCII range
+ if (ch < 128)
+ {
+ // this handles the following cases:
+ // U+000A 10 LF Line Feed
+ // U+000B 11 VT Vertical Tab
+ // U+000C 12 FF Form Feed
+ // U+000D 13 CR Carriage Return
+ return ch >= 10 && ch <= 13;
+ }
+
+ // this handles the following cases:
+ // U+0085 133 NEL Next Line
+ // U+2028 8232 LS Line Separator
+ // U+2029 8233 PS Paragraph Separator
+ return ch == 0x0085 | ch == 0x2028 | ch == 0x2029;
+ }
+
+ /**
+ * Determine if the specified character can be used as the first character of an identifier.
+ *
+ * @param ch the character to evaluate
+ *
+ * @return true iff the specified character can be the start of an
+ * identifier
+ */
+ public static boolean isIdentifierStart(char ch)
+ {
+ return Character.isUnicodeIdentifierStart(ch) || ch == '_';
+ }
+
+ /**
+ * Determine if the specified character can be part of an identifier.
+ *
+ * @param ch the character to evaluate
+ *
+ * @return true iff the specified character can be part of an identifier
+ */
+ public static boolean isIdentifierPart(char ch)
+ {
+ return Character.isUnicodeIdentifierPart(ch) || ch == '_';
+ }
+
+ /**
+ * Get the next character of source code, but do not move the current location.
+ */
+ protected char peekChar()
+ {
+ char ch = m_source.next();
+ m_source.rewind();
+ return ch;
+ }
+
+ /**
+ * Get the next character of source code, but do some additional checks on the character to make
+ * sure it's legal, such as checking for an illegal SUB character.
+ */
+ protected char nextChar()
+ {
+ Source source = m_source;
+ char ch = source.next();
+
+ // it is illegal for the SUB aka EOF character to occur
+ // anywhere in the source other than at the end
+ if (ch == EOF && source.hasNext())
+ {
+ // back up to get the location of the SUB character
+ long lPos = source.getPosition();
+ source.rewind();
+ long lStartPos = source.getPosition();
+ source.setPosition(lPos);
+
+ log(Severity.ERROR, UNEXPECTED_EOF, null, lStartPos);
+ }
+
+ return ch;
+ }
+
+ /**
+ * Get the next character of source code, but do some additional checks on the character to make
+ * sure it's legal, such as checking for an illegal SUB character.
+ */
+ protected char needCharOrElse(String sError)
+ {
+ try
+ {
+ return nextChar();
+ }
+ catch (NoSuchElementException e)
+ {
+ log(Severity.ERROR, sError, m_source.getPosition(), m_source.getPosition());
+ }
+
+ // already logged an error; just pretend we hit a closing brace (since all roads should have
+ // gone there)
+ return '}';
+ }
+
+
+ // ----- constants -----------------------------------------------------------------------------
+
+ /**
+ * Unicode: Horizontal Tab.
+ */
+ public static final char HT = 0x0009;
+ /**
+ * Unicode: Line Feed.
+ */
+ public static final char LF = 0x000A;
+ /**
+ * Unicode: Vertical Tab.
+ */
+ public static final char VT = 0x000B;
+ /**
+ * Unicode: Form Feed.
+ */
+ public static final char FF = 0x000C;
+ /**
+ * Unicode: Carriage Return.
+ */
+ public static final char CR = 0x000D;
+ /**
+ * Unicode: End-Of-File aka control-z aka SUB.
+ */
+ public static final char EOF = 0x001A;
+ /**
+ * Unicode: File Separator.
+ */
+ public static final char FS = 0x001C;
+ /**
+ * Unicode: Group Separator.
+ */
+ public static final char GS = 0x001D;
+ /**
+ * Unicode: Record Separator.
+ */
+ public static final char RS = 0x001E;
+ /**
+ * Unicode: Unit Separator.
+ */
+ public static final char US = 0x001F;
+ /**
+ * Unicode: Next Line.
+ */
+ public static final char NEL = 0x0085;
+ /**
+ * Unicode: Non-Breaking Space aka "&nbsp".
+ */
+ public static final char NBS = 0x00A0;
+ /**
+ * Unicode: Line Separator.
+ */
+ public static final char LS = 0x2028;
+ /**
+ * Unicode: Paragraph Separator.
+ */
+ public static final char PS = 0x2029;
+
+ /**
+ * Unexpected End-Of-File (SUB character).
+ */
+ public static final String UNEXPECTED_EOF = "LEXER-01";
+ /**
+ * Expected a comment-ending "star slash" but never found one.
+ */
+ public static final String EXPECTED_ENDCOMMENT = "LEXER-02";
+ /**
+ * A character was encountered that cannot be the start of a valid token.
+ */
+ public static final String ILLEGAL_CHAR = "LEXER-03";
+ /**
+ * Number format exception.
+ */
+ public static final String ILLEGAL_NUMBER = "LEXER-04";
+ /**
+ * An illegal character literal, missing closing quote.
+ */
+ public static final String CHAR_NO_TERM = "LEXER-05";
+ /**
+ * An illegally escaped character literal.
+ */
+ public static final String CHAR_BAD_ESC = "LEXER-06";
+ /**
+ * An illegal character literal missing the character.
+ */
+ public static final String CHAR_NO_CHAR = "LEXER-07";
+ /**
+ * An illegally terminated string literal.
+ */
+ public static final String STRING_NO_TERM = "LEXER-08";
+ /**
+ * An illegally escaped string literal.
+ */
+ public static final String STRING_BAD_ESC = "LEXER-09";
+ /**
+ * An illegal hex (binary) literal.
+ */
+ public static final String ILLEGAL_HEX = "LEXER-10";
+ /**
+ * Expected {0}; found {1}.
+ */
+ public static final String EXPECTED_CHAR = "LEXER-11";
+ /**
+ * {0} digits were required; only {1} digits were found.
+ */
+ public static final String EXPECTED_DIGITS = "LEXER-12";
+ /**
+ * Invalid ISO-8601 date {0}; ...
+ */
+ public static final String BAD_DATE = "LEXER-13";
+ /**
+ * Invalid ISO-8601 time {0}; ...
+ */
+ public static final String BAD_TIME_OF_DAY = "LEXER-14";
+ /**
+ * Invalid ISO-8601 datetime {0}; ...
+ */
+ public static final String BAD_TIME = "LEXER-15";
+ /**
+ * Invalid ISO-8601 timezone {0}; ...
+ */
+ public static final String BAD_TIMEZONE = "LEXER-16";
+ /**
+ * Invalid ISO-8601 duration {0}; ...
+ */
+ public static final String BAD_DURATION = "LEXER-17";
+ /**
+ * Unexpected character: {0}
+ */
+ public static final String UNEXPECTED_CHAR = "LEXER-18";
+
+
+ // ----- data members --------------------------------------------------------------------------
+
+ /**
+ * The Source to parse.
+ */
+ private final Source m_source;
+
+ /**
+ * The ErrorListener to report errors to.
+ */
+ private final ErrorListener m_errorListener;
+
+ /**
+ * Keeps track of whether whitespace was encountered.
+ */
+ private boolean m_fWhitespace;
+ } \ No newline at end of file
diff --git a/common/src/main/java/com/seaofnodes/simple/lexer/ecstasy/Source.java b/common/src/main/java/com/seaofnodes/simple/lexer/ecstasy/Source.java
new file mode 100644
index 00000000..6800138a
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/simple/lexer/ecstasy/Source.java
@@ -0,0 +1,826 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * The Initial Developer of the Original Software is
+ * The Ecstasy Project (https://github.com/xtclang/).
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+package com.seaofnodes.simple.lexer.ecstasy;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.nio.file.Path;
+
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+
+import com.seaofnodes.util.Handy;
+
+import static com.seaofnodes.simple.lexer.ecstasy.Lexer.isLineTerminator;
+import static com.seaofnodes.util.Handy.*;
+
+
+/**
+ * A representation of an XTC source code file, handling the first two phases of lexical analysis
+ * (line termination, location and unicode escapes).
+ */
+public class Source implements Cloneable
+ {
+ // ----- constructors --------------------------------------------------------------------------
+
+ /**
+ * Construct a Source directly from a String of XTC source code.
+ *
+ * @param sScript the XTC source code, as a String
+ */
+ public Source(String sScript)
+ {
+ this(sScript.toCharArray());
+ }
+
+ /**
+ * Construct a Source directly from a String of XTC source code.
+ *
+ * @param sScript the XTC source code, as a String
+ */
+ public Source(String sScript, int cDepth)
+ {
+ this(sScript.toCharArray());
+ m_cDirDepth = cDepth;
+ }
+
+ /**
+ * Construct a Source by reading the XTC source code from a file.
+ *
+ * @param file the File containing the XTC source code to load
+ *
+ * @param cDepth the number of directories up the source code can include files from
+ *
+ * @throws IOException
+ */
+ public Source(File file, int cDepth)
+ throws IOException
+ {
+ this(readFileChars(file));
+ m_file = file;
+ m_cDirDepth = cDepth;
+ setFileName(file.getPath());
+ }
+
+ /**
+ * Construct a Source from an input stream.
+ *
+ * @param stream the InputStream
+ */
+ public Source(InputStream stream)
+ {
+ this(fromInputStream(stream));
+ }
+
+ /**
+ * Read a char[] from a stream. Inefficient, but predictable.
+ *
+ * @param stream the stream to vacuum
+ *
+ * @return the contents of the stream, as a char[]
+ */
+ private static char[] fromInputStream(InputStream stream)
+ {
+ try
+ {
+ StringBuilder sb = new StringBuilder();
+ int n;
+ while ((n = stream.read()) >= 0)
+ {
+ sb.append((char) n);
+ }
+ return sb.toString().toCharArray();
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Construct a Source directly from a character array containing the XTC
+ * source code. Note that the passed array is retained -- not copied! -- by
+ * this constructor.
+ *
+ * @param ach the XTC source code, as a character array
+ */
+ Source(char[] ach)
+ {
+ assert ach != null;
+ m_ach = ach;
+ m_cch = ach.length;
+ }
+
+
+ // ----- public API ----------------------------------------------------------------------------
+
+ /**
+ * @return the file name, if one has been configured
+ */
+ public String getFileName()
+ {
+ return m_sFile;
+ }
+
+ /**
+ * @return the simple file name, if a file is available
+ */
+ public String getSimpleFileName()
+ {
+ return m_file == null ? "<no file>" : m_file.getName();
+ }
+
+ /**
+ * Specify the name of the file that the source comes from. This is automatically configured by
+ * the File-based constructor.
+ *
+ * @param sFile the file name
+ */
+ public void setFileName(String sFile)
+ {
+ if (m_sFile == null)
+ {
+ m_sFile = sFile;
+ }
+ else if (!m_sFile.equals(sFile))
+ {
+ throw new IllegalStateException("file name cannot be modified");
+ }
+ }
+
+ /**
+ * Determine the File referenced from inside another source file using the "File" or "Dir"
+ * BNF constructions.
+ *
+ * @param sFile a string put together from identifier tokens, "." tokens, "/" tokens, "./"
+ * tokens, and "../" tokens, following the rules defined in the Ecstasy BNF
+ *
+ * @return the File for the file or directory, or null if unresolvable
+ */
+ public File resolvePath(String sFile)
+ {
+ if (m_file == null || sFile.length() == 0)
+ {
+ return null;
+ }
+
+ File file = m_file.getParentFile();
+ boolean fRoot = sFile.startsWith("/");
+ m_cUp = 0;
+ if (fRoot)
+ {
+ // it's not absolute; it's relative to the directory containing the module
+ while (m_cUp < m_cDirDepth)
+ {
+ file = file.getParentFile();
+ assert file != null;
+ ++m_cUp;
+ }
+ sFile = sFile.substring(1);
+ }
+
+ if (sFile.length() == 0)
+ {
+ return file.isDirectory() ? file : null;
+ }
+
+ boolean fRequireDir = sFile.endsWith("/");
+ if (fRequireDir)
+ {
+ sFile = sFile.substring(0, sFile.length()-1);
+ }
+
+ String[] segments = Handy.parseDelimitedString(sFile, '/');
+ for (String segment : segments)
+ {
+ if (segment.length() == 0)
+ {
+ return null;
+ }
+
+ if (segment.equals("."))
+ {
+ // nothing to do
+ }
+ else if (segment.equals(".."))
+ {
+ file = file.getParentFile();
+ if (file == null || ++m_cUp > m_cDirDepth)
+ {
+ return null;
+ }
+ }
+ else
+ {
+ if (file.exists() && !file.isDirectory())
+ {
+ return null;
+ }
+
+ file = new File(file, segment);
+ --m_cUp;
+ }
+ }
+
+ if (!file.exists())
+ {
+ // the file may be in the "symmetrical" resources directory
+ File fileSrc = getSubDir("x");
+ File fileRes = getSubDir("resources");
+ if (fileSrc != null && fileRes != null)
+ {
+ if (fRoot)
+ {
+ file = new File(fileRes, sFile);
+ }
+ else
+ {
+ Path path = file.toPath().normalize();
+ Path pathSrc = fileSrc.toPath().normalize();
+ Path pathRes = fileRes.toPath().normalize();
+ Path pathRel = pathSrc.relativize(path);
+ file = pathRes.resolve(pathRel).toFile();
+ }
+ if (!file.exists())
+ {
+ return null;
+ }
+ }
+ }
+
+ return fRequireDir && !file.isDirectory() ? null : file;
+ }
+ private transient int m_cUp; // Java needs multiple return values. This is a hack.
+
+ /**
+ * @return the directory containing the module, or null if it cannot be determined
+ */
+ private File getTopDir()
+ {
+ if (m_file == null)
+ {
+ return null;
+ }
+
+ File file = m_file.getAbsoluteFile().getParentFile();
+ for (int i = 0, c = m_cDirDepth; i < c; ++i)
+ {
+ file = file.getParentFile();
+ if (file == null)
+ {
+ return null;
+ }
+ }
+ return file;
+ }
+
+ /**
+ * @param sChild the name of the child directory
+ *
+ * @return the specified directory at the same level as the top directory, if it exists
+ */
+ private File getSubDir(String sChild)
+ {
+ File file = getTopDir();
+ if (file != null && file.exists() && file.isDirectory())
+ {
+ file = file.getParentFile();
+ if (file != null && file.exists() && file.isDirectory())
+ {
+ file = new File(file, sChild);
+ if (file.exists() && file.isDirectory())
+ {
+ return file;
+ }
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Load a file (as text) referenced from inside another source file.
+ *
+ * @param sFile a string put together from identifier tokens, "/" tokens, "./" tokens, and
+ * "../" tokens
+ *
+ * @return the Source, or null
+ */
+ public Source includeString(String sFile)
+ throws IOException
+ {
+ File file = resolvePath(sFile);
+ if (file == null)
+ {
+ return null;
+ }
+
+ if (file.equals(m_file))
+ {
+ return clone();
+ }
+
+ if (file.exists() && file.isFile() && file.canRead())
+ {
+ return new Source(file, m_cDirDepth - m_cUp);
+ }
+
+ return null;
+ }
+
+ /**
+ * Load a file (as binary) referenced from inside another source file.
+ *
+ * @param sFile a string put together from identifier tokens, "/" tokens, "./" tokens, and
+ * "../" tokens
+ *
+ * @return the binary contents of the file, or null
+ */
+ public byte[] includeBinary(String sFile)
+ throws IOException
+ {
+ File file = resolvePath(sFile);
+ if (file == null)
+ {
+ return null;
+ }
+
+ if (file.exists() && file.isFile() && file.canRead())
+ {
+ return Handy.readFileBytes(file);
+ }
+
+ return null;
+ }
+
+ /**
+ * Determine if there are more characters in the source.
+ *
+ * @return true iff there are still more characters
+ */
+ public boolean hasNext()
+ {
+ return m_of < m_cch;
+ }
+
+ /**
+ * Obtain the next character of the source. Note that unicode escapes are
+ * processed such that the sequence of characters "U+0024" in the source
+ * would return only the single character '$'. Similarly, the combination
+ * of CR+LF ("\r\n") is returned as the single character LF ('\n').
+ *
+ * @return the next character from the source
+ *
+ * @throws NoSuchElementException if an attempt is made to advance past
+ * the end
+ */
+ public char next()
+ {
+ final char[] ach = m_ach;
+ final int cch = m_cch;
+
+ int of = m_of;
+ if (of >= cch)
+ {
+ throw new NoSuchElementException();
+ }
+
+ char ch = ach[of++];
+
+ // check for new line
+ if (Lexer.isLineTerminator(ch))
+ {
+ // handle the special case of CR:LF by treating it as a single LF
+ // character
+ if (ch == '\r' && of < cch && ach[of] == '\n')
+ {
+ ++of;
+ ch = '\n';
+ }
+
+ ++m_iLine;
+ m_iLineOffset = 0;
+ }
+ else
+ {
+ int cchAdjust = 1;
+
+ // check for a unicode escape
+ if (ch == '\\' && of + 4 < cch
+ && isHexit(ach[of+1])
+ && isHexit(ach[of+2])
+ && isHexit(ach[of+3])
+ && isHexit(ach[of+4]))
+ {
+ final char chU = ach[of];
+ if (chU == 'u')
+ {
+ // 4-hexit unicode escape
+ int nch = hexitValue(ach[of+1]) << 12
+ | hexitValue(ach[of+2]) << 8
+ | hexitValue(ach[of+3]) << 4
+ | hexitValue(ach[of+4]);
+
+ ch = (char) nch;
+ of += 5;
+ cchAdjust += 5;
+ m_fEscapesEncountered = true;
+ }
+ else if (chU == 'U' && of + 8 < cch
+ && isHexit(ach[of+5])
+ && isHexit(ach[of+6])
+ && isHexit(ach[of+7])
+ && isHexit(ach[of+8]))
+ {
+ // 8-hexit unicode escape
+ int nch = hexitValue(ach[of+1]) << 28
+ | hexitValue(ach[of+2]) << 24
+ | hexitValue(ach[of+3]) << 20
+ | hexitValue(ach[of+4]) << 16
+ | hexitValue(ach[of+5]) << 12
+ | hexitValue(ach[of+6]) << 8
+ | hexitValue(ach[of+7]) << 4
+ | hexitValue(ach[of+8]);
+
+ // unfortunately Java does not support characters with code-
+ // points beyond 16 bits; we could theoretically split this
+ // up into a Unicode "surrogate pair", but that is not
+ // supported by this class at this time
+ ch = nch > Character.MAX_VALUE ? '?' : (char) nch;
+ of += 9;
+ cchAdjust += 9;
+ m_fEscapesEncountered = true;
+ }
+ }
+
+ // a negative line offset indicates that the offset is not currently
+ // being tracked because a line terminator was put back
+ if (m_iLineOffset >= 0)
+ {
+ m_iLineOffset += cchAdjust;
+ }
+ }
+
+ m_of = of;
+ return ch;
+ }
+
+ /**
+ * Undo a previously made call to the {@link #next()} method by "rewinding"
+ * one character. In parsing terminology, this is often referred to as a
+ * "put back char" operation.
+ *
+ * @throws NoSuchElementException if an attempt is made to rewind past the
+ * beginning
+ */
+ public void rewind()
+ {
+ int of = m_of;
+ if (of <= 0)
+ {
+ throw new NoSuchElementException();
+ }
+
+ // determine what the immediately preceding character was
+ --of;
+ final char[] ach = m_ach;
+ final char ch = ach[of];
+ // if the line feed is immediately preceded by a carriage return, then
+ // back up past them both (they are treated as a single line feed)
+ if (isLineTerminator(ch))
+ {
+ if (ch == '\n' && of > 0 && ach[of-1] == '\r')
+ {
+ --of;
+ }
+
+ --m_iLine;
+
+ // instead of determining the line offset in the previous line that
+ // we just rewound to, defer the calculation until it is needed
+ m_iLineOffset = -1;
+ }
+ else
+ {
+ int cchAdjust = 1;
+
+ if (m_fEscapesEncountered
+ && isHexit(ch) && of >= 5
+ && isHexit(ach[of-1])
+ && isHexit(ach[of-2])
+ && isHexit(ach[of-3]))
+ {
+ if (ach[of-5] == '\\' && ach[of-4] == 'u')
+ {
+ of -= 5;
+ cchAdjust += 5;
+ }
+ else if (of >= 9 && ach[of-9] == '\\' && ach[of-8] == 'U'
+ && isHexit(ach[of-7])
+ && isHexit(ach[of-6])
+ && isHexit(ach[of-5])
+ && isHexit(ach[of-4]))
+ {
+ of -= 9;
+ cchAdjust += 9;
+ }
+ }
+
+ // a negative line offset indicates that the offset is not currently
+ // being tracked because a line terminator was put back
+ if (m_iLineOffset >= 0)
+ {
+ m_iLineOffset -= cchAdjust;
+ }
+ }
+
+ m_of = of;
+ }
+
+ /**
+ * Determine the current line number within the source.
+ *
+ * @return the zero-based line number
+ */
+ public int getLine()
+ {
+ return m_iLine;
+ }
+
+ /**
+ * Determine the offset within the current line of the source.
+ *
+ * @return the zero-based offset within the current line
+ */
+ public int getOffset()
+ {
+ int iLineOffset = m_iLineOffset;
+ if (iLineOffset < 0)
+ {
+ // go back until a line terminator is found
+ iLineOffset = 0;
+ for (int of = m_of; of > 0; )
+ {
+ if (isLineTerminator(m_ach[--of]))
+ {
+ break;
+ }
+
+ ++iLineOffset;
+ }
+
+ m_iLineOffset = iLineOffset;
+ }
+
+ return iLineOffset;
+ }
+
+ /**
+ * Obtain a token that represents the current location within the source.
+ *
+ * @return a position token
+ */
+ public long getPosition()
+ {
+ // use the line-offset accessor to force the re-calculation if necessary
+ final int iLineOffset = getOffset();
+
+ // up to 24 bits for offset, 20 bits for line and line offset
+ assert m_of >= 0;
+ assert m_iLine >= 0;
+ assert iLineOffset >= 0;
+ if (m_of > 0xFFFFFF || m_iLine > 0xFFFFF || iLineOffset >= 0xFFFFF)
+ {
+ throw new IllegalStateException();
+ }
+
+ return (((long) m_of & 0xFFFFFF) << 40)
+ | (((long) m_iLine & 0x0FFFFF) << 20)
+ | (((long) iLineOffset & 0x0FFFFF) );
+ }
+
+ /**
+ * Using a previously returned position token, set the current position
+ * within the source to the position indicated by the token.
+ *
+ * @param lPosition a previously returned position token
+ */
+ public void setPosition(long lPosition)
+ {
+ m_of = ((int) (lPosition >>> 40)) & 0xFFFFFF;
+ m_iLine = ((int) (lPosition >>> 20)) & 0x0FFFFF;
+ m_iLineOffset = ((int) (lPosition )) & 0x0FFFFF;
+ }
+
+ /**
+ * Determine the line number from a previously returned position token.
+ *
+ * @param lPosition a position token
+ *
+ * @return the zero-based line number of the specified position
+ */
+ public static int calculateLine(long lPosition)
+ {
+ return ((int) (lPosition >>> 20)) & 0x0FFFFF;
+ }
+
+ /**
+ * Determine the offset within the line from a previously returned position
+ * token.
+ *
+ * @param lPosition a position token
+ *
+ * @return the zero-based offset within the line of the specified position
+ */
+ public static int calculateOffset(long lPosition)
+ {
+ return ((int) lPosition) & 0x0FFFFF;
+ }
+
+ /**
+ * Obtain the string of characters starting from one position and proceeding
+ * to another position. The current position is not affected by this method.
+ *
+ * @param lPositionFrom a position token to start from (inclusive)
+ * @param lPositionTo a position token to end at (exclusive)
+ *
+ * @return the String of characters from the first to the second specified
+ * position
+ */
+ public String toString(long lPositionFrom, long lPositionTo)
+ {
+ long lPositionSave = getPosition();
+
+ setPosition(lPositionFrom);
+ final int ofEnd = ((int) (lPositionTo >>> 40)) & 0xFFFFFF;
+ assert ofEnd >= m_of;
+ char[] ach = new char[ofEnd - m_of];
+ int cch = 0;
+ while (m_of < ofEnd)
+ {
+ ach[cch++] = next();
+ }
+
+ setPosition(lPositionSave);
+ return new String(ach, 0, cch);
+ }
+
+ /**
+ * @return a clone of this Source, but with the position reset to the beginning of the source
+ * code
+ */
+ public Source clone()
+ {
+ try
+ {
+ Source that = (Source) super.clone();
+ that.reset();
+ return that;
+ }
+ catch (CloneNotSupportedException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Reset the position to the very beginning of the source code.
+ */
+ public void reset()
+ {
+ m_of = 0;
+ m_iLine = 0;
+ m_iLineOffset = 0;
+ }
+
+ /**
+ * @return a String intended to make this usable in a debugger
+ */
+ public String toString()
+ {
+ long lCur = getPosition();
+
+ String sIntro = "...";
+ for (int i = 0; i < 20; ++i)
+ {
+ try
+ {
+ rewind();
+ }
+ catch (NoSuchElementException e)
+ {
+ sIntro = "";
+ break;
+ }
+ }
+ long lPre = getPosition();
+ setPosition(lCur);
+
+ String sEpilogue = "...";
+ for (int i = 0; i < 20; ++i)
+ {
+ try
+ {
+ next();
+ }
+ catch (NoSuchElementException e)
+ {
+ sEpilogue = "(EOF)";
+ break;
+ }
+ }
+ long lPost = getPosition();
+ setPosition(lCur);
+
+ String sPre = toString(lPre, lCur);
+ String sPost = toString(lCur, lPost);
+
+ StringBuilder sb = new StringBuilder(sIntro);
+ appendString(sb, sPre);
+ char[] achIndent = new char[sb.length()];
+ Arrays.fill(achIndent, ' ');
+
+ appendString(sb, sPost)
+ .append(sEpilogue)
+ .append('\n')
+ .append(new String(achIndent))
+ .append('^');
+ return sb.toString();
+ }
+
+ /**
+ * Obtain the underlying data that the source represents.
+ *
+ * @return the whole of the unprocessed source, as a String
+ */
+ public String toRawString()
+ {
+ return new String(m_ach);
+ }
+
+
+ // ----- data members --------------------------------------------------------------------------
+
+ /**
+ * The source code.
+ */
+ private final char[] m_ach;
+
+ /**
+ * The length of the source code.
+ */
+ private final int m_cch;
+
+ /**
+ * The current offset within the source code.
+ */
+ private int m_of;
+
+ /**
+ * The current source code line number.
+ */
+ private int m_iLine;
+
+ /**
+ * The current offset within the current line of source code.
+ */
+ private int m_iLineOffset;
+
+ /**
+ * Set to true iff Unicode escapes have been encountered.
+ */
+ private boolean m_fEscapesEncountered;
+
+ /**
+ * The file name that the source comes from.
+ */
+ private String m_sFile;
+
+ /**
+ * The file that the source comes from.
+ */
+ private File m_file;
+
+ /**
+ * The directory depth within the module of the file that the source comes from.
+ */
+ private int m_cDirDepth;
+ } \ No newline at end of file
diff --git a/common/src/main/java/com/seaofnodes/simple/lexer/ecstasy/Token.java b/common/src/main/java/com/seaofnodes/simple/lexer/ecstasy/Token.java
new file mode 100644
index 00000000..271d8fbc
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/simple/lexer/ecstasy/Token.java
@@ -0,0 +1,870 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * The Initial Developer of the Original Software is
+ * The Ecstasy Project (https://github.com/xtclang/).
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package com.seaofnodes.simple.lexer.ecstasy;
+
+
+import com.seaofnodes.simple.common.ErrorListener;
+import com.seaofnodes.simple.common.Severity;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.seaofnodes.util.Handy.appendChar;
+import static com.seaofnodes.util.Handy.appendString;
+
+
+/**
+ * Representation of a language token.
+ */
+public class Token
+ implements Cloneable
+ {
+ // ----- constructors --------------------------------------------------------------------------
+
+ /**
+ * Construct an XTC token.
+ *
+ * @param lStartPos starting position in the Source (inclusive)
+ * @param lEndPos ending position in the Source (exclusive)
+ * @param id identity of the token
+ */
+ public Token(long lStartPos, long lEndPos, Id id)
+ {
+ this(lStartPos, lEndPos, id, null);
+ }
+
+ /**
+ * Construct an XTC token.
+ *
+ * @param lStartPos starting position in the Source (inclusive)
+ * @param lEndPos ending position in the Source (exclusive)
+ * @param id identity of the token
+ * @param oValue value of the token (if it is a literal)
+ */
+ public Token(long lStartPos, long lEndPos, Id id, Object oValue)
+ {
+ m_lStartPos = lStartPos;
+ m_lEndPos = lEndPos;
+ m_id = id;
+ m_oValue = oValue;
+ }
+
+ /**
+ * Record whitespace information onto the token.
+ *
+ * @param fWhitespaceBefore
+ * @param fWhitespaceAfter
+ */
+ public void noteWhitespace(boolean fWhitespaceBefore, boolean fWhitespaceAfter)
+ {
+ m_fLeadingWhitespace = fWhitespaceBefore;
+ m_fTrailingWhitespace = fWhitespaceAfter;
+ }
+
+
+ // ----- accessors -----------------------------------------------------------------------------
+
+ /**
+ * Determine the starting position in the source at which this token occurs.
+ *
+ * @return the Source position of the token
+ */
+ public long getStartPosition()
+ {
+ return m_lStartPos;
+ }
+
+ /**
+ * Determine the ending position (exclusive) in the source for this token.
+ *
+ * @return the Source position of the end of the token
+ */
+ public long getEndPosition()
+ {
+ return m_lEndPos;
+ }
+
+ /**
+ * Determine if this token follows whitespace in the source.
+ *
+ * @return true iff this token follows whitespace
+ */
+ public boolean hasLeadingWhitespace()
+ {
+ return m_fLeadingWhitespace;
+ }
+
+ /**
+ * Determine if this token precedes whitespace in the source.
+ *
+ * @return true iff this token is followed by whitespace
+ */
+ public boolean hasTrailingWhitespace()
+ {
+ return m_fTrailingWhitespace;
+ }
+
+ /**
+ * Determine the identity of the token.
+ *
+ * @return the identity of the token
+ */
+ public Id getId()
+ {
+ return m_id;
+ }
+
+ /**
+ * Determine the value of the token.
+ *
+ * @return the value of the token, or null
+ */
+ public Object getValue()
+ {
+ return m_oValue;
+ }
+
+ /**
+ * Helper to get the value as text, if there is a value, otherwise to get the token id's text.
+ * Use only for debugging or when the token is known to be a String.
+ *
+ * @return the String form of the token
+ */
+ public String getValueText()
+ {
+ return m_oValue == null ? m_id.TEXT : m_oValue.toString();
+ }
+
+ /**
+ * @return true iff this is an identifier is a "special name"
+ */
+ public boolean isSpecial()
+ {
+ if (m_id == Id.IDENTIFIER)
+ {
+ Id keywordId = Id.valueByContextSensitiveText((String) getValue());
+ return keywordId != null && keywordId.Special;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return true iff this is an identifier is a context-sensitive name
+ */
+ public boolean isContextSensitive()
+ {
+ return m_id == Id.IDENTIFIER && Id.valueByContextSensitiveText((String) getValue()) != null;
+ }
+
+ public boolean isComment()
+ {
+ return m_id == Id.ENC_COMMENT || m_id == Id.EOL_COMMENT;
+ }
+
+ /**
+ * If the token is an identifier that is also a context sensitive keyword, obtain that keyword.
+ *
+ * @return a keyword token that represents the same text as this identifier token
+ */
+ public Token convertToKeyword()
+ {
+ if (m_id != Id.IDENTIFIER)
+ {
+ throw new IllegalStateException("not an identifier! (" + this + ")");
+ }
+
+ Id id = Id.valueByContextSensitiveText((String) getValue());
+ if (id == null)
+ {
+ throw new IllegalStateException("missing context sensitive keyword for: " + getValue());
+ }
+
+ return new Token(m_lStartPos, m_lEndPos, id, m_oValue);
+ }
+
+ /**
+ * If the token is a context sensitive keyword that was an identifier, obtain the identifier.
+ *
+ * @return a token that is not a context sensitive keyword
+ */
+ public Token desensitize()
+ {
+ return m_id.ContextSensitive ? new Token(m_lStartPos, m_lEndPos, Id.IDENTIFIER, m_oValue) : this;
+ }
+
+ /**
+ * Refine the token identity to a more specific identity.
+ *
+ * @param id the refined identity
+ */
+ Token refine(Id id)
+ {
+ m_id = id;
+ return this;
+ }
+
+ /**
+ * Allow a token to be "peeled off" the front of this token, if possible. This mutates this
+ * token, such that it is what remains after the returned token was peeled off.
+ *
+ * @param id the token to peel off of this token
+ * @param source the source from which this token was created, which is necessary in order to
+ * calculate the exact position between any peeled-off token and this token
+ *
+ * @return the requested token that was peeled off of this token, if possible, otherwise null
+ */
+ public Token peel(Id id, Source source)
+ {
+ Id newId = null;
+
+ if (id == Id.COMP_GT)
+ {
+ switch (m_id)
+ {
+ default:
+ return null;
+
+ case USHR_ASN:
+ newId = Id.SHR_ASN;
+ break;
+
+ case USHR:
+ newId = Id.SHR;
+ break;
+
+ case SHR_ASN:
+ newId = Id.COMP_GTEQ;
+ break;
+
+ case SHR:
+ newId = Id.COMP_GT;
+ break;
+
+ case COMP_GTEQ:
+ newId = Id.ASN;
+ break;
+ }
+ }
+ else if (id == Id.COMP_LT)
+ {
+ switch (m_id)
+ {
+ default:
+ return null;
+
+ case SHL_ASN:
+ newId = Id.COMP_LTEQ;
+ break;
+
+ case SHL:
+ newId = Id.COMP_LT;
+ break;
+
+ // note: there are no legitimate use cases for peeling "<" off of "<=" or "<=>"
+ // case COMP_LTEQ:
+ // newId = Id.ASN;
+ // break;
+ }
+ }
+
+ if (newId == null)
+ {
+ return null;
+ }
+
+ // get the location of "this" token
+ long start = m_lStartPos;
+
+ // get the location of the end of the new peeled token / start of this token (adjusted)
+ long current = source.getPosition();
+ source.setPosition(start);
+ source.next();
+ long middle = source.getPosition();
+ source.setPosition(current);
+
+ // adjust this token
+ m_lStartPos = middle;
+ m_id = newId;
+
+ // return the new token
+ return new Token(start, middle, id);
+ }
+
+ /**
+ * Allow a token to be "un-peeled off" from the front of that following token, if possible.
+ *
+ * @param that the token following this token
+ *
+ * @return the new token, or null if the two tokens cannot be annealed
+ */
+ public Token anneal(Token that)
+ {
+ if (m_id == Id.COMP_LT && this.m_lEndPos == that.m_lStartPos)
+ {
+ switch (that.m_id)
+ {
+ case COMP_LTEQ:
+ return new Token(this.m_lStartPos, that.m_lEndPos, Id.SHL_ASN);
+
+ case COMP_LT:
+ return new Token(this.m_lStartPos, that.m_lEndPos, Id.SHL);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Obtain the actual string of characters from the source code for this token.
+ *
+ * @param source the source code
+ *
+ * @return the string of characters corresponding to this token, extracted from the source
+ */
+ public String getString(Source source)
+ {
+ return source.toString(m_lStartPos, m_lEndPos);
+ }
+
+ /**
+ * Helper to log an error related to this Token.
+ *
+ * @param severity the severity level of the error; one of {@link Severity#INFO},
+ * {@link Severity#WARNING}, {@link Severity#ERROR}, or {@link Severity#FATAL}
+ * @param sCode the error code that identifies the error message
+ * @param aoParam the parameters for the error message; may be null
+ *
+ * @return true to attempt to abort the process that reported the error, or false to attempt to
+ * continue the process
+ */
+ public void log(ErrorListener errs, Source source, Severity severity, String sCode, Object... aoParam)
+ {
+ if (aoParam == null || aoParam.length == 0)
+ {
+ aoParam = new Object[] {source == null ? toString() : getString(source)};
+ }
+
+ errs.log(severity, sCode, aoParam, source,
+ source == null ? 0L : getStartPosition(), source == null ? 0L : getEndPosition());
+ }
+
+
+ // ----- Object methods ------------------------------------------------------------------------
+
+ public String toDebugString()
+ {
+ return "[" +
+ Source.calculateLine(m_lStartPos) +
+ "," +
+ Source.calculateOffset(m_lStartPos) +
+ " - " +
+ Source.calculateLine(m_lEndPos) +
+ "," +
+ Source.calculateOffset(m_lEndPos) +
+ "] " + this;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ switch (m_id)
+ {
+ case LIT_CHAR:
+ {
+ sb.append('\'');
+ appendChar(sb, (Character) m_oValue);
+ sb.append('\'');
+ }
+ break;
+
+ case LIT_STRING:
+ sb.append('\"');
+ appendString(sb, (String) m_oValue);
+ sb.append('\"');
+ break;
+
+ case LIT_INT:
+ case LIT_DEC:
+ case LIT_FLOAT:
+ sb.append(m_oValue);
+ break;
+
+ case LIT_BIT:
+ case LIT_NIBBLE:
+ case LIT_INT8:
+ case LIT_INT16:
+ case LIT_INT32:
+ case LIT_INT64:
+ case LIT_INT128:
+ case LIT_INTN:
+ case LIT_UINT8:
+ case LIT_UINT16:
+ case LIT_UINT32:
+ case LIT_UINT64:
+ case LIT_UINT128:
+ case LIT_UINTN:
+ case LIT_DEC32:
+ case LIT_DEC64:
+ case LIT_DEC128:
+ case LIT_DECN:
+ case LIT_FLOAT16:
+ case LIT_FLOAT32:
+ case LIT_FLOAT64:
+ case LIT_FLOAT128:
+ case LIT_FLOATN:
+ case LIT_BFLOAT16:
+ sb.append(m_id.TEXT)
+ .append(':')
+ .append(m_oValue);
+ break;
+
+ case IDENTIFIER:
+ sb.append("name:")
+ .append(m_oValue);
+ break;
+
+ case ENC_COMMENT:
+ {
+ String sComment = (String) m_oValue;
+ if (sComment.length() > 47)
+ {
+ sComment = sComment.substring(0, 44) + "...";
+ }
+ appendString(sb.append("/*"), sComment).append("*/");
+ }
+ break;
+
+ case EOL_COMMENT:
+ {
+ String sComment = (String) m_oValue;
+ if (sComment.length() > 50)
+ {
+ sComment = sComment.substring(0, 47) + "...";
+ }
+ appendString(sb.append("//"), sComment);
+ }
+ break;
+
+ default:
+ sb.append(m_id.TEXT);
+ break;
+ }
+
+ return sb.toString();
+ }
+
+ public Token clone()
+ {
+ try
+ {
+ return (Token) super.clone();
+ }
+ catch (CloneNotSupportedException e)
+ {
+ throw new IllegalStateException(e);
+ }
+ }
+
+
+ // ----- Token identities ----------------------------------------------------------------------
+
+ /**
+ * Token Identity.
+ */
+ public enum Id
+ {
+ COLON (":" ),
+ SEMICOLON (";" ),
+ COMMA ("," ),
+ DOT ("." ),
+ I_RANGE_I (".." ), // inclusive range
+ E_RANGE_I (">.." ), // exclusive start
+ I_RANGE_E ("..<" ), // exclusive end
+ E_RANGE_E (">..<" ), // exclusive range
+ DIR_CUR ("./" ),
+ DIR_PARENT ("../" ),
+ STR_FILE ("$" ),
+ BIN_FILE ("#" ),
+ AT ("@" ),
+ COND ("?" ),
+ L_PAREN ("(" ),
+ ASYNC_PAREN ("^(" ),
+ R_PAREN (")" ),
+ L_CURLY ("{" ),
+ R_CURLY ("}" ),
+ L_SQUARE ("[" ),
+ R_SQUARE ("]" ),
+ ADD ("+" ),
+ SUB ("-" ),
+ MUL ("*" ),
+ DIV ("/" ),
+ MOD ("%" ),
+ DIVREM ("/%" ),
+ SHL ("<<" ),
+ SHR (">>" ),
+ USHR (">>>" ),
+ BIT_AND ("&" ),
+ BIT_OR ("|" ),
+ BIT_XOR ("^" ),
+ BIT_NOT ("~" ),
+ NOT ("!" ),
+ COND_AND ("&&" ),
+ COND_XOR ("^^" ),
+ COND_OR ("||" ),
+ COND_ELSE ("?:" ),
+ ASN ("=" ),
+ ADD_ASN ("+=" ),
+ SUB_ASN ("-=" ),
+ MUL_ASN ("*=" ),
+ DIV_ASN ("/=" ),
+ MOD_ASN ("%=" ),
+ SHL_ASN ("<<=" ),
+ SHR_ASN (">>=" ),
+ USHR_ASN (">>>=" ),
+ BIT_AND_ASN ("&=" ),
+ BIT_OR_ASN ("|=" ),
+ BIT_XOR_ASN ("^=" ),
+ COND_AND_ASN ("&&=" ),
+ COND_OR_ASN ("||=" ),
+ COND_ASN (":=" ),
+ COND_NN_ASN ("?=" ), // NN -> Not Null
+ COND_ELSE_ASN("?:=" ),
+ COMP_EQ ("==" ),
+ COMP_NEQ ("!=" ),
+ COMP_ORD ("<=>" ),
+ COMP_LT ("<" ),
+ COMP_LTEQ ("<=" ),
+ COMP_GT (">" ),
+ COMP_GTEQ (">=" ),
+ INC ("++" ),
+ DEC ("--" ),
+ LAMBDA ("->" ),
+ ASN_EXPR ("<-" ),
+ ANY ("_" ),
+ ALLOW ("allow" , true),
+ AS ("as" ),
+ ASSERT ("assert" ),
+ ASSERT_RND ("assert:rnd" ),
+ ASSERT_ARG ("assert:arg" ),
+ ASSERT_BOUNDS("assert:bounds" ),
+ ASSERT_TODO ("assert:TODO" ),
+ ASSERT_ONCE ("assert:once" ),
+ ASSERT_TEST ("assert:test" ),
+ ASSERT_DBG ("assert:debug" ),
+ AVOID ("avoid" , true),
+ BREAK ("break" ),
+ CASE ("case" ),
+ CATCH ("catch" ),
+ CLASS ("class" ),
+ CONDITIONAL ("conditional" ),
+ CONST ("const" ),
+ CONSTRUCT ("construct" ),
+ CONTINUE ("continue" ),
+ DEFAULT ("default" ),
+ DELEGATES ("delegates" , true),
+ DESIRED ("desired" , true),
+ DO ("do" ),
+ ELSE ("else" ),
+ EMBEDDED ("embedded" , true),
+ ENUM ("enum" ),
+ EXTENDS ("extends" , true),
+ FINALLY ("finally" ),
+ FOR ("for" ),
+ FUNCTION ("function" ),
+ IF ("if" ),
+ IMMUTABLE ("immutable" ),
+ IMPLEMENTS ("implements" , true),
+ IMPORT ("import" ),
+ INCORPORATES ("incorporates" , true),
+ INJECT ("inject" , true),
+ INTERFACE ("interface" ),
+ INTO ("into" , true),
+ IS ("is" ),
+ MIXIN ("mixin" ),
+ MODULE ("module" ),
+ NEW ("new" ),
+ OPTIONAL ("optional" , true),
+ OUTER ("outer" , true, true),
+ PACKAGE ("package" ),
+ PREFER ("prefer" , true),
+ PRIVATE ("private" ),
+ PROTECTED ("protected" ),
+ PUBLIC ("public" ),
+ REQUIRED ("required" , true),
+ RETURN ("return" ),
+ SERVICE ("service" ),
+ STATIC ("static" ),
+ STRUCT ("struct" ),
+ SUPER ("super" , true, true),
+ SWITCH ("switch" ),
+ THIS ("this" , true, true),
+ THIS_CLASS ("this:class" , true, true),
+ THIS_MODULE ("this:module" , true, true),
+ THIS_PRI ("this:private" , true, true),
+ THIS_PRO ("this:protected" , true, true),
+ THIS_PUB ("this:public" , true, true),
+ THIS_SERV ("this:service" , true, true),
+ THIS_STRUCT ("this:struct" , true, true),
+ THIS_TARGET ("this:target" , true, true),
+ THROW ("throw" ),
+ TODO ("TODO" ),
+ TRY ("try" ),
+ TYPEDEF ("typedef" ),
+ USING ("using" ),
+ VAL ("val" , true),
+ VAR ("var" , true),
+ VOID ("void" ),
+ WHILE ("while" ),
+ IDENTIFIER (null ),
+ EOL_COMMENT (null ),
+ ENC_COMMENT (null ),
+ LIT_BIT (null ),
+ LIT_NIBBLE (null ),
+ LIT_CHAR (null ),
+ LIT_STRING (null ),
+ LIT_BINSTR (null ),
+ LIT_INT (null ), // integer literal
+ LIT_INTA ("Int" , true, true), // automatically sized integer
+ LIT_INT8 ("Int8" , true, true),
+ LIT_INT16 ("Int16" , true, true),
+ LIT_INT32 ("Int32" , true, true),
+ LIT_INT64 ("Int64" , true, true),
+ LIT_INT128 ("Int128" , true, true),
+ LIT_INTN ("IntN" , true, true),
+ LIT_UINT8 ("UInt8" , true, true),
+ LIT_UINTA ("UInt" , true, true), // automatically sized unsigned integer
+ LIT_UINT16 ("UInt16" , true, true),
+ LIT_UINT32 ("UInt32" , true, true),
+ LIT_UINT64 ("UInt64" , true, true),
+ LIT_UINT128 ("UInt128" , true, true),
+ LIT_UINTN ("UIntN" , true, true),
+ LIT_DEC (null ), // decimal floating point literal
+ LIT_DECA ("Dec" , true, true), // automatically sized decimal
+ LIT_DEC32 ("Dec32" , true, true),
+ LIT_DEC64 ("Dec64" , true, true),
+ LIT_DEC128 ("Dec128" , true, true),
+ LIT_DECN ("DecN" , true, true),
+ LIT_FLOAT (null ), // binary floating point literal
+ LIT_FLOAT8E4 ("Float8e4" , true, true),
+ LIT_FLOAT8E5 ("Float8e5" , true, true),
+ LIT_BFLOAT16 ("BFloat16" , true, true),
+ LIT_FLOAT16 ("Float16" , true, true),
+ LIT_FLOAT32 ("Float32" , true, true),
+ LIT_FLOAT64 ("Float64" , true, true),
+ LIT_FLOAT128 ("Float128" , true, true),
+ LIT_FLOATN ("FloatN" , true, true),
+ LIT_DATE (null ),
+ LIT_TIMEOFDAY(null ),
+ LIT_TIME (null ),
+ LIT_TIMEZONE (null ),
+ LIT_DURATION (null ),
+ LIT_VERSION (null ),
+ LIT_PATH (null ), // generated by the Parser, not by the Lexer
+ TEMPLATE ("{...}" ), // not a real token
+ EOF ("EOF" ), // end of token stream
+ ENUM_VAL ("enum-value" ); // not a real token
+
+ /**
+ * Constructor.
+ *
+ * @param sText a textual representation of the token, or null
+ */
+ Id(final String sText)
+ {
+ this(sText, false);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param sText a textual representation of the token, or null
+ */
+ Id(final String sText, boolean fContextSensitive)
+ {
+ this(sText, fContextSensitive, false);
+ }
+
+ Id(final String sText, boolean fContextSensitive, boolean fSpecial)
+ {
+ TEXT = sText;
+ this.ContextSensitive = fContextSensitive;
+ this.Special = fSpecial;
+ }
+
+ @Override
+ public String toString()
+ {
+ return TEXT == null ? super.toString() : '\"' + TEXT + '"';
+ }
+
+ /**
+ * Look up an Id enum by its ordinal.
+ *
+ * @param i the ordinal
+ *
+ * @return the Format enum for the specified ordinal
+ */
+ public static Id valueOf(int i)
+ {
+ return IDs[i];
+ }
+
+ /**
+ * Look up an Id enum by its {@link #TEXT}.
+ *
+ * @param sText the textual representation of the Id
+ *
+ * @return an instance of Id, or null if there is no matching
+ * {@link #TEXT}
+ */
+ public static Id valueByText(String sText)
+ {
+ return KEYWORDS.get(sText);
+ }
+
+ /**
+ * Look up an Id enum by its {@link #TEXT}, including context-sensitive keywords.
+ *
+ * @param sText the textual representation of the Id
+ *
+ * @return an instance of Id, or null if there is no matching
+ * {@link #TEXT}
+ */
+ public static Id valueByContextSensitiveText(String sText)
+ {
+ return ALL_KEYWORDS.get(sText);
+ }
+
+ /**
+ * Look up an Id enum by its {@link #TEXT}, and if it is one of the keywords that has both
+ * a normal and one-or-more suffixed forms, then return the Id of the normal form.
+ *
+ * @param sText the possible keyword
+ *
+ * @return the Id of the "normal form" of the keyword, iff suffixed forms also exist
+ */
+ public static Id valueByPrefix(String sText)
+ {
+ return PREFIXES.get(sText);
+ }
+
+ /**
+ * All of the Format enums.
+ */
+ private static final Id[] IDs = Id.values();
+
+ /**
+ * String representations of tokens that have constant representations, excluding context-
+ * sensitive keywords.
+ */
+ private static final Map<String, Id> KEYWORDS = new HashMap<>();
+ /**
+ * String representations of all tokens that have constant representations.
+ */
+ private static final Map<String, Id> ALL_KEYWORDS = new HashMap<>();
+
+ /**
+ * String representations of tokens that have both "normal" and "suffixed" representations.
+ */
+ private static final Map<String, Id> PREFIXES = new HashMap<>();
+
+ static
+ {
+ for (Id id : IDs)
+ {
+ String sText = id.TEXT;
+ if (sText != null && sText.length() > 0)
+ {
+ char ch = sText.charAt(0);
+ if (ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch == '_')
+ {
+ ALL_KEYWORDS.put(sText, id);
+
+ if (!id.ContextSensitive)
+ {
+ KEYWORDS.put(sText, id);
+ }
+
+ int ofColon = sText.indexOf(':');
+ if (ofColon > 0)
+ {
+ Id prefix = ALL_KEYWORDS.get(sText.substring(0, ofColon));
+ if (prefix != null)
+ {
+ PREFIXES.put(prefix.TEXT, prefix);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * A textual representation of the token, if it has a constant textual representation;
+ * otherwise null.
+ */
+ final public String TEXT;
+
+ /**
+ * True if the token is context-sensitive, i.e. if it is not always a reserved word.
+ */
+ final boolean ContextSensitive;
+
+ /**
+ * True if the token is a special name.
+ */
+ final boolean Special;
+ }
+
+
+ // ----- data members --------------------------------------------------------------------------
+
+ /**
+ * Starting position (inclusive) in the source of this token.
+ */
+ private long m_lStartPos;
+
+ /**
+ * Ending position (exclusive) in the source of this token.
+ */
+ private final long m_lEndPos;
+
+ /**
+ * Identifier of the token.
+ */
+ private Id m_id;
+
+ /**
+ * Value of the Token (if it is a literal).
+ */
+ private final Object m_oValue;
+
+ /**
+ * Each token knows if it follows whitespace.
+ */
+ private boolean m_fLeadingWhitespace;
+
+ /**
+ * Each token knows if it has whitespace following.
+ */
+ private boolean m_fTrailingWhitespace;
+ }
diff --git a/common/src/main/java/com/seaofnodes/util/Ary.java b/common/src/main/java/com/seaofnodes/util/Ary.java
new file mode 100644
index 00000000..36654a7f
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/util/Ary.java
@@ -0,0 +1,367 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * The Initial Developer of the Original Software is Cliff Click.
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package com.seaofnodes.util;
+
+import java.lang.reflect.Array;
+import java.util.*;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+// ArrayList with saner syntax
+public class Ary<E> implements Iterable<E> {
+ public E[] _es;
+ public int _len;
+ public Ary(E[] es) { this(es,es.length); }
+ public Ary(E[] es, int len) { if( es.length==0 ) es=Arrays.copyOf(es,1); _es=es; _len=len; }
+ @SuppressWarnings("unchecked")
+ public Ary(Class<E> clazz) { this((E[]) Array.newInstance(clazz, 1),0); }
+
+ /** @return list is empty */
+ public boolean isEmpty() { return _len==0; }
+ /** @return active list length */
+ public int len() { return _len; }
+ /** @param i element index
+ * @return element being returned; throws if OOB
+ * @exception ArrayIndexOutOfBoundsException if !(0 <= i < _len)
+ */
+ public E at( int i ) {
+ range_check(i);
+ return _es[i];
+ }
+ /** @param i element index
+ * @return element being returned, or null if OOB */
+ public E atX( int i ) {
+ return i < _len ? _es[i] : null;
+ }
+ /** @return last element */
+ public E last( ) {
+ range_check(0);
+ return _es[_len-1];
+ }
+ public void last(int i) {
+ range_check(i);
+ E tmp = _es[i];
+ _es[i] = _es[_len-1];
+ _es[_len-1] = tmp;
+ }
+
+ /** @return remove and return last element */
+ public E pop( ) {
+ range_check(0);
+ return _es[--_len];
+ }
+
+ /** Add element in amortized constant time
+ * @param e Element to add at end of list
+ * @return 'this' for flow-coding */
+ public Ary<E> add( E e ) {
+ if( _len >= _es.length ) _es = Arrays.copyOf(_es,Math.max(1,_es.length<<1));
+ _es[_len++] = e;
+ return this;
+ }
+
+ /** Add element in amortized constant time
+ * @param e Element to add at end of list
+ **/
+ public E push( E e ) {
+ if( _len >= _es.length ) _es = Arrays.copyOf(_es,Math.max(1,_es.length<<1));
+ return (_es[_len++] = e);
+ }
+
+ /** Slow, linear-time, element insert. Preserves order.
+ * @param i index to insert at, between 0 and _len inclusive.
+ * @param e Element to insert
+ */
+ public void insert( int i, E e ) {
+ if( i < 0 || i>_len )
+ throw new ArrayIndexOutOfBoundsException(""+i+" >= "+_len);
+ if( _len >= _es.length ) _es = Arrays.copyOf(_es,Math.max(1,_es.length<<1));
+ System.arraycopy(_es,i,_es,i+1,(_len++)-i);
+ _es[i] = e;
+ }
+
+ /** Fast, constant-time, element removal. Does not preserve order
+ * @param i element to be removed
+ * @return element removed */
+ public E del( int i ) {
+ range_check(i);
+ E tmp = _es[i];
+ _es[i]=_es[--_len];
+ return tmp;
+ }
+
+ /** Element removal, using '=='. Does not preserve order.
+ * @param e element to be removed
+ * @return element removed */
+ public E del( E e ) {
+ for( int i=0; i<_len; i++ ) {
+ E tmp = _es[i];
+ if( tmp==e ) {
+ _es[i]=_es[--_len];
+ return tmp;
+ }
+ }
+ return null;
+ }
+
+ /** Slow, linear-time, element removal. Preserves order.
+ * @param i element to be removed
+ * @return element removed */
+ public E remove( int i ) {
+ range_check(i);
+ E e = _es[i];
+ System.arraycopy(_es,i+1,_es,i,(--_len)-i);
+ return e;
+ }
+
+ /** Remove all elements */
+ public void clear( ) { _len=0; }
+
+ public void fill( E e ) { Arrays.fill(_es,0,_len,e); }
+
+ /** Extend and set. null fills as needed and does not throw AIOOBE.
+ * @param i element to set
+ * @param e value to set
+ * @return old value
+ */
+ public E setX( int i, E e ) {
+ if( i >= _len ) Arrays.fill(_es,_len,_es.length,null);
+ while( i>= _es.length ) _es = Arrays.copyOf(_es,_es.length<<1);
+ if( i >= _len ) _len = i+1;
+ return (_es[i] = e);
+ }
+
+ /** Clear element. Does nothing if element is OOB, since these are clear by
+ * default.
+ * @param i element to clear
+ */
+ public void clear( int i ) { if( i<_len ) _es[i]=null; }
+
+ /** Set existing element
+ * @param i element to set
+ * @param e value to set
+ * @return old value
+ * @exception AIOOBE if !(0 <= i < _len)
+ */
+ public E set( int i, E e ) {
+ range_check(i);
+ E old = _es[i];
+ _es[i] = e;
+ return old;
+ }
+
+ // Increment length, revealing the E behind the _len, or null
+ public E inc_len( ) {
+ if( _len >= _es.length ) _es = Arrays.copyOf(_es,_es.length<<1);
+ return _es[_len++];
+ }
+ public Ary<E> set_len( int len ) {
+ if( len > _len )
+ while( len>= _es.length ) _es = Arrays.copyOf(_es,_es.length<<1);
+ _len = len;
+ while( _es.length > (len<<1) ) // Shrink if hugely too large
+ _es = Arrays.copyOf(_es,_es.length>>1);
+ Arrays.fill(_es,len,_es.length,null);
+ return this;
+ }
+
+ /** @param c Collection to be added */
+ public Ary<E> addAll( Collection<? extends E> c ) { if( c!=null ) for( E e : c ) add(e); return this; }
+
+ /** @param es Array to be added */
+ public <F extends E> Ary<E> addAll( F[] es ) {
+ if( es==null || es.length==0 ) return this;
+ while( _len+es.length > _es.length ) _es = Arrays.copyOf(_es,_es.length<<1);
+ System.arraycopy(es,0,_es,_len,es.length);
+ _len += es.length;
+ return this;
+ }
+
+ /** @param c Collection to be added */
+ public Ary<E> addAll( Ary<? extends E> c ) {
+ if( c==null || c._len==0 ) return this;
+ while( _len+c._len > _es.length ) _es = Arrays.copyOf(_es,_es.length<<1);
+ System.arraycopy(c._es,0,_es,_len,c._len);
+ _len += c._len;
+ return this;
+ }
+
+ /** @return compact array version */
+ public E[] asAry() { return Arrays.copyOf(_es,_len); }
+
+ /** @param f function to apply to each element. Updates in-place. */
+ public Ary<E> map_update( Function<E,E> f ) { for( int i = 0; i<_len; i++ ) _es[i] = f.apply(_es[i]); return this; }
+ /** @param P filter out elements failing to pass the predicate; updates in
+ * place and shuffles list.
+ * @return this, for flow-coding */
+ public Ary<E> filter_update( Predicate<E> P ) {
+ for( int i=0; i<_len; i++ )
+ if( !P.test(_es[i]) )
+ del(i--);
+ return this;
+ }
+ /** Sorts in-place
+ * @param c Comparator to sort by */
+ public void sort_update(Comparator<? super E> c ) { Arrays.sort(_es, 0, _len, c); }
+ /** Find the first matching element using ==, or -1 if none. Note that
+ * most del calls shuffle the list, so the first element might be random.
+ * @param e Element to find
+ * @return index of first matching element, or -1 if none */
+ public int find( E e ) {
+ for( int i=0; i<_len; i++ ) if( _es[i]==e ) return i;
+ return -1;
+ }
+ /** Find the first element matching predicate P, or -1 if none. Note that
+ * most del calls shuffle the list, so the first element might be random.
+ * @param P Predicate to match
+ * @return index of first matching element, or -1 if none */
+ public int find( Predicate<E> P ) {
+ for( int i=0; i<_len; i++ ) if( P.test(_es[i]) ) return i;
+ return -1;
+ }
+ /** Find and replace the first matching element using ==.
+ * @param old Element to find
+ * @param nnn Element replacing old
+ * @return true if replacement happened */
+ public boolean replace( E old, E nnn ) {
+ for( int i=0; i<_len; i++ ) if( _es[i]==old ) { _es[i]=nnn; return true; }
+ return false;
+ }
+
+
+ /** Merge-Or. Merge 2 sorted Arys, tossing out duplicates. Return a new
+ * sorted Ary with the merged list. Undefined if the original arrays are
+ * not sorted. Error if they are not of the same type. Elements must
+ * implement Comparable.
+ * @param a0 Sorted Ary to merge
+ * @param a1 Sorted Ary to merge
+ * @return A new sorted merged Ary
+ */
+ public static <X extends Comparable<X>> Ary<X> merge_or( Ary<X> a0, Ary<X> a1 ) {
+ int i=0, j=0;
+ Ary<X> res = new Ary<>(Arrays.copyOf(a0._es,a0._len+a1._len),0);
+
+ while( i<a0._len && j<a1._len ) {
+ X x = a0._es[i];
+ X y = a1._es[j];
+ int cmp = x.compareTo(y);
+ if( cmp<0 ) { res.add(x); i++; }
+ else if( cmp>0 ) { res.add(y); j++; }
+ else { res.add(x); i++; j++; }
+ }
+ while( i<a0._len ) res.add(a0._es[i++]);
+ while( j<a1._len ) res.add(a1._es[j++]);
+ return res;
+ }
+
+ /** Merge-Or. Merge two sorted Arys, tossing out duplicates and elements not
+ * passing the filter. Return a new sorted Ary with the merged list.
+ * Undefined if the original arrays are not sorted. Error if they are not
+ * of the same type. Elements must implement Comparable.
+ * @param a0 Sorted Ary to merge
+ * @param a1 Sorted Ary to merge
+ * @param cmpr Comparator
+ * @param filter Predicate, null means no filter
+ * @return A new sorted merged Ary
+ */
+ public static <X> Ary<X> merge_or( Ary<X> a0, Ary<X> a1, Comparator<X> cmpr, Predicate<X> filter) {
+ int i=0, j=0;
+ Ary<X> res = new Ary<>(Arrays.copyOf(a0._es,a0._len+a1._len),0);
+
+ while( i<a0._len && j<a1._len ) {
+ X x = a0._es[i]; if( !filter.test(x) ) { i++; continue; }
+ X y = a1._es[j]; if( !filter.test(y) ) { j++; continue; }
+ int cmp = cmpr.compare(x,y);
+ if( cmp<0 ) { res.add(x); i++; }
+ else if( cmp>0 ) { res.add(y); j++; }
+ else { res.add(x); i++; j++; }
+ }
+ while( i<a0._len ) if( filter.test(a0._es[i++]) ) res.add(a0._es[i-1]);
+ while( j<a1._len ) if( filter.test(a1._es[j++]) ) res.add(a1._es[j-1]);
+ return res;
+ }
+
+
+ /** Merge-And. Merge 2 sorted Arys, keeping only duplicates. Return a new
+ * sorted Ary with the merged list. Undefined if the original arrays are
+ * not sorted. Error if they are not of the same type. Elements must
+ * implement Comparable.
+ * @param a0 Sorted Ary to merge
+ * @param a1 Sorted Ary to merge
+ * @return A new sorted merged Ary
+ */
+ public static <X extends Comparable<X>> Ary<X> merge_and( Ary<X> a0, Ary<X> a1 ) {
+ int i=0, j=0;
+ Ary<X> res = new Ary<>(Arrays.copyOf(a0._es,Math.min(a0._len,a1._len)),0);
+ while( i<a0._len && j<a1._len ) {
+ X x = a0._es[i];
+ X y = a1._es[j];
+ int cmp = x.compareTo(y);
+ if( cmp<0 ) { i++; }
+ else if( cmp>0 ) { j++; }
+ else { res.add(x); i++; j++; }
+ }
+ return res;
+ }
+
+ /** @return an iterator */
+ @Override public Iterator<E> iterator() { return new Iter(); }
+ private class Iter implements Iterator<E> {
+ int _i=0;
+ @Override public boolean hasNext() { return _i<_len; }
+ @Override public E next() { return _es[_i++]; }
+ }
+
+ @Override public String toString() {
+ SB sb = new SB().p('{');
+ for( int i=0; i<_len; i++ ) {
+ if( i>0 ) sb.p(',');
+ if( _es[i] != null ) sb.p(_es[i].toString());
+ }
+ return sb.p('}').toString();
+ }
+
+ private void range_check( int i ) {
+ if( i < 0 || i>=_len )
+ throw new ArrayIndexOutOfBoundsException(""+i+" >= "+_len);
+ }
+
+ // Note that the hashCode() and equals() are not invariant to changes in the
+ // underlying array. If the hashCode() is used (e.g., inserting into a
+ // HashMap) and the then the array changes, the hashCode() will change also.
+ @Override public boolean equals( Object o ) {
+ if( this==o ) return true;
+ if( !(o instanceof Ary ary) ) return false;
+ if( _len != ary._len ) return false;
+ if( _es == ary._es ) return true;
+ for( int i=0; i<_len; i++ )
+ if( !(_es[i]==null ? (ary._es[i] == null) : _es[i].equals(ary._es[i])) )
+ return false;
+ return true;
+ }
+ @Override public int hashCode( ) {
+ int sum=_len;
+ for( int i=0; i<_len; i++ )
+ sum += _es[i]==null ? 0 : _es[i].hashCode();
+ return sum;
+ }
+
+ public Ary<E> deepCopy() {
+ return new Ary<>(_es.clone(),_len);
+ }
+}
diff --git a/common/src/main/java/com/seaofnodes/util/AryInt.java b/common/src/main/java/com/seaofnodes/util/AryInt.java
new file mode 100644
index 00000000..000f4809
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/util/AryInt.java
@@ -0,0 +1,203 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * The Initial Developer of the Original Software is Cliff Click.
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package com.seaofnodes.util;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.function.IntUnaryOperator;
+
+// ArrayList with saner syntax
+public class AryInt {
+ public int[] _es;
+ public int _len;
+ public AryInt(int[] es) { this(es,es.length); }
+ public AryInt(int[] es, int len) { _es=es; _len=len; }
+ public AryInt() { this(new int[1],0); }
+
+ /** @return list is empty */
+ public boolean isEmpty() { return _len==0; }
+ /** @return active list length */
+ public int len() { return _len; }
+ /** @param i element index
+ * @return element being returned; throws if OOB */
+ public int at( int i ) {
+ range_check(i);
+ return _es[i];
+ }
+ /** @param i element index
+ * @return element being returned, or 0 if OOB */
+ public int atX( int i ) {
+ return i < _len ? _es[i] : 0;
+ }
+ /** @return last element */
+ public int last( ) {
+ range_check(0);
+ return _es[_len-1];
+ }
+
+ /** @return remove and return last element */
+ public int pop( ) {
+ range_check(0);
+ return _es[--_len];
+ }
+
+ /** Add element in amortized constant time
+ * @param e element to add at end of list
+ * @return 'this' for flow-coding */
+ public AryInt push( int e ) {
+ if( _len >= _es.length ) _es = Arrays.copyOf(_es,Math.max(1,_es.length<<1));
+ _es[_len++] = e;
+ return this;
+ }
+
+ /** Slow, linear-time, element insert. Preserves order.
+ * @param i index to insert at, between 0 and _len inclusive.
+ * @param e intlement to insert
+ */
+ public void insert( int i, int e ) {
+ if( i < 0 || i>_len )
+ throw new ArrayIndexOutOfBoundsException(""+i+" >= "+_len);
+ if( _len >= _es.length ) _es = Arrays.copyOf(_es,Math.max(1,_es.length<<1));
+ System.arraycopy(_es,i,_es,i+1,(_len++)-i);
+ _es[i] = e;
+ }
+
+ /** Fast, constant-time, element removal. Does not preserve order
+ * @param i element to be removed
+ * @return element removed */
+ public int del( int i ) {
+ range_check(i);
+ int tmp = _es[i];
+ _es[i]=_es[--_len];
+ return tmp;
+ }
+
+ /** Slow, linear-time, element removal. Preserves order.
+ * @param i element to be removed
+ * @return element removed */
+ public int remove( int i ) {
+ range_check(i);
+ int e = _es[i];
+ System.arraycopy(_es,i+1,_es,i,(--_len)-i);
+ return e;
+ }
+
+ /** Remove all elements */
+ public void clear( ) { Arrays.fill(_es,0,_len,0); _len=0; }
+
+ // Extend and set
+ public int setX( int i, int e ) {
+ while( i>= _es.length ) _es = Arrays.copyOf(_es,_es.length<<1);
+ if( i >= _len ) _len = i+1;
+ return (_es[i] = e);
+ }
+
+ public int set( int i, int e ) {
+ range_check(i);
+ return (_es[i] = e);
+ }
+
+ public AryInt set_as( int e ) { _es[0] = e; _len=1; return this; }
+ public AryInt set_len( int len ) {
+ if( len > _len )
+ while( len>= _es.length ) _es = Arrays.copyOf(_es,_es.length<<1);
+ _len = len;
+ while( _es.length > (len<<1) ) // Shrink if hugely too large
+ _es = Arrays.copyOf(_es,_es.length>>1);
+ return this;
+ }
+
+ /** @param c Collection to be added */
+ public AryInt addAll( Collection<? extends Integer> c ) { for( int e : c ) push(e); return this; }
+
+ /** @param es Array to be added */
+ public AryInt addAll( int[] es ) {
+ if( es.length==0 ) return this;
+ while( _len+es.length > _es.length ) _es = Arrays.copyOf(_es,_es.length<<1);
+ System.arraycopy(es,0,_es,_len,es.length);
+ _len += es.length;
+ return this;
+ }
+
+ public AryInt map_update( IntUnaryOperator f ) { for( int i = 0; i<_len; i++ ) _es[i] = f.applyAsInt(_es[i]); return this; }
+
+ /** @return compact array version, using the internal base array where possible. */
+ public int[] asAry() { return _len==_es.length ? _es : Arrays.copyOf(_es,_len); }
+
+ /** Sorts in-place */
+ public void sort_update() { Arrays.sort(_es, 0, _len); }
+ /** Find the first matching element using ==, or -1 if none. Note that
+ * most del calls shuffle the list, so the first element might be random.
+ * @param e intlement to find
+ * @return index of first matching element, or -1 if none */
+ public int find( int e ) {
+ for( int i=0; i<_len; i++ ) if( _es[i]==e ) return i;
+ return -1;
+ }
+
+ @Override public String toString() {
+ SB sb = new SB().p('[');
+ for( int i=0; i<_len; i++ )
+ sb.p(_es[i]).p(',');
+ return sb.unchar().p(']').toString();
+ }
+
+ private void range_check( int i ) {
+ if( i < 0 || i>=_len )
+ throw new ArrayIndexOutOfBoundsException(""+i+" >= "+_len);
+ }
+
+ // Binary search sorted _es. Returns insertion point.
+ // Undefined results if _es is not sorted.
+ public int binary_search( int e ) {
+ int lo=0, hi=_len-1;
+ while( lo <= hi ) {
+ int mid = (hi + lo) >>> 1; // midpoint, rounded down
+ int mval = _es[mid];
+ if( e==mval ) {
+ // If dups, get to the first.
+ while( mid>0 && e==_es[mid-1] ) mid--;
+ return mid;
+ }
+ if( e >mval ) lo = mid+1;
+ else hi = mid-1;
+ }
+ return lo;
+ }
+
+ // Note that the hashCode() and equals() are not invariant to changes in the
+ // underlying array. If the hashCode() is used (e.g., inserting into a
+ // HashMap) and the then the array changes, the hashCode() will change also.
+ @Override public boolean equals( Object o ) {
+ if( this==o ) return true;
+ if( !(o instanceof AryInt) ) return false;
+ AryInt ary = (AryInt)o;
+ if( _len != ary._len ) return false;
+ if( _es == ary._es ) return true;
+ for( int i=0; i<_len; i++ )
+ if( _es[i] != ary._es[i] )
+ return false;
+ return true;
+ }
+ @Override public int hashCode( ) {
+ int sum=_len;
+ for( int i=0; i<_len; i++ )
+ sum += _es[i];
+ return sum;
+ }
+}
diff --git a/common/src/main/java/com/seaofnodes/util/Handy.java b/common/src/main/java/com/seaofnodes/util/Handy.java
new file mode 100644
index 00000000..f3496f8c
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/util/Handy.java
@@ -0,0 +1,1778 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * The Initial Developer of the Original Software is
+ * The Ecstasy Project (https://github.com/xtclang/).
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+package com.seaofnodes.util;
+
+
+import java.io.BufferedInputStream;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.UTFDataFormatException;
+
+import java.lang.reflect.Array;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+
+import java.nio.charset.Charset;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+
+/**
+ * Handy static methods.
+ */
+public class Handy
+ {
+ // ----- String formatting ---------------------------------------------------------------------
+
+ /**
+ * Take the passed nibble (the low order 4 bits of the int) and return the corresponding hexit
+ * (hexadecimal digit) in the range of '0' - 'F'.
+ *
+ * @param n the nibble value
+ *
+ * @return the hexadecimal representation of the passed nibble
+ */
+ public static char nibbleToChar(int n)
+ {
+ n = n & 0xF;
+ return (n <= 9)
+ ? (char) ('0' + n)
+ : (char) ('A' - 0xA + n);
+ }
+
+ /**
+ * Render the passed byte (the low order 8 bits of the int) into the passed StringBuilder as
+ * two hexadecimal digits.
+ *
+ * @param sb the StringBuilder to append to
+ * @param n the byte value
+ *
+ * @return the StringBuilder
+ */
+ public static StringBuilder appendByteAsHex(StringBuilder sb, int n)
+ {
+ return sb.append(nibbleToChar(n >> 4))
+ .append(nibbleToChar(n));
+ }
+
+ /**
+ * Format the passed byte (the low order 8 bits of the int) into a String of the form "0xFF".
+ *
+ * @param n the byte value
+ *
+ * @return a String representation of the byte
+ */
+ public static String byteToHexString(int n)
+ {
+ return appendByteAsHex(new StringBuilder(4).append("0x"), n).toString();
+ }
+
+ /**
+ * Append a portion of a byte array to a StringBuilder as a sequence of hex digits.
+ *
+ * @param sb the StringBuilder to append to
+ * @param ab the byte array
+ * @param of the offset of the first byte in the byte array to render
+ * @param cb the number bytes from the byte array to render
+ *
+ * @return the StringBuilder
+ */
+ public static StringBuilder appendByteArrayAsHex(StringBuilder sb, byte[] ab, int of, int cb)
+ {
+ sb.ensureCapacity(sb.length() + cb * 2);
+ while (--cb >= 0)
+ {
+ int n = ab[of++];
+ sb.append(nibbleToChar(n >> 4))
+ .append(nibbleToChar(n));
+ }
+ return sb;
+ }
+
+ /**
+ * Append the contents of a byte array to a StringBuilder as a sequence of hex digits.
+ *
+ * @param sb the StringBuilder to append to
+ * @param ab the byte array
+ *
+ * @return the StringBuilder
+ */
+ public static StringBuilder appendByteArrayAsHex(StringBuilder sb, byte[] ab)
+ {
+ return appendByteArrayAsHex(sb, ab, 0, ab.length);
+ }
+
+ /**
+ * Format a portion of the passed byte array into a String of the form "0x01234FF".
+ *
+ * @param ab the byte array
+ * @param of the offset of the first byte in the byte array to render
+ * @param cb the number bytes from the byte array to render
+ *
+ * @return a String representation of the byte array
+ */
+ public static String byteArrayToHexString(byte[] ab, int of, int cb)
+ {
+ return appendByteArrayAsHex(new StringBuilder(2 + cb * 2).append("0x"),
+ ab, of, cb).toString();
+ }
+
+ /**
+ * Format the passed byte array into a String of the form "0x01234FF".
+ *
+ * @param ab the byte array
+ *
+ * @return a String representation of the byte array
+ */
+ public static String byteArrayToHexString(byte[] ab)
+ {
+ return byteArrayToHexString(ab, 0, ab.length);
+ }
+
+ /**
+ * Convert a String of hex digits into a <tt>byte[]</tt>.
+ *
+ * @param s a String of hexadecimal digits; the leading "0x" is optional
+ *
+ * @return a byte array
+ */
+ public static byte[] hexStringToByteArray(String s)
+ {
+ int ofch = 0;
+ int cch = s.length();
+ int ofb = 0;
+ int cb = (cch + 1) / 2;
+
+ // check for (and skip) the "0x" prefix
+ if (s.charAt(0) == '0' && (s.charAt(1) == 'x' || s.charAt(1) == 'X'))
+ {
+ ofch += 2;
+ --cb;
+ }
+
+ final byte[] ab = new byte[cb];
+
+ // handle the case where the first nibble is an implied (missing) '0'
+ if ((cch & 0x1) != 0)
+ {
+ ab[ofb++] = (byte) hexitValue(s.charAt(ofch++));
+ }
+
+ // convert the remaining pairs of hexits to bytes
+ while (ofch < cch)
+ {
+ ab[ofb++] = (byte) (hexitValue(s.charAt(ofch)) << 4 | hexitValue(s.charAt(ofch+1)));
+ ofch += 2;
+ }
+
+ return ab;
+ }
+
+ /**
+ * Format bytes from the passed byte array as a "hex dump", useful for debugging.
+ *
+ * @param ab the byte array
+ * @param of the offset of the first byte to show in the hex
+ * dump
+ * @param cb the number of bytes to show in the hex dump
+ * @param cBytesPerLine the number of bytes to show in each line of the hex
+ * dump
+ *
+ * @return a String containing the hex dump
+ */
+ public static String byteArrayToHexDump(byte[] ab, int of, int cb, int cBytesPerLine)
+ {
+ assert ab != null;
+ assert of >= 0 && cb >= 0 && of + cb <= ab.length;
+ assert cBytesPerLine > 0;
+
+ // first figure out how many digits it will take to show the address
+ final int cchAddr = countHexDigits(of + cb) + 1 & ~0x01;
+
+ // format is 12F0: 00 12 32 A0 ????\n
+ // line length is addressLen + 4*cbPerLine + 3
+ final int cch = cchAddr + cBytesPerLine * 4 + 3;
+ final char[] ach = new char[cch];
+ Arrays.fill(ach, ' ');
+ ach[cchAddr] = ':';
+ ach[cch-1] = '\n';
+
+ final int cLines = Math.max((cb + cBytesPerLine - 1) / cBytesPerLine, 1);
+ final StringBuilder sb = new StringBuilder(cLines * cch);
+ final int ofHex = cchAddr + 2;
+ final int ofChar = ofHex + cBytesPerLine * 3;
+
+ for (int iLine = 0; iLine < cLines; ++iLine)
+ {
+ // format the address
+ renderIntToHex(of, ach, 0, cchAddr);
+
+ for (int iByte = 0; iByte < cBytesPerLine; ++iByte)
+ {
+ if (cb > 0)
+ {
+ int b = ab[of];
+ renderByteToHex(b, ach, ofHex + iByte * 3);
+
+ char ch = (char) (b & 0xFF);
+ ach[ofChar + iByte] = Character.isISOControl(ch) ? '.' : ch;
+ }
+ else
+ {
+ ach[ofHex + iByte * 3 ] = ' ';
+ ach[ofHex + iByte * 3 + 1] = ' ';
+ ach[ofChar + iByte ] = ' ';
+ }
+
+ ++of;
+ --cb;
+ }
+
+ sb.append(ach, 0, iLine < cLines - 1 ? cch : cch - 1);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Format bytes from the passed byte array as a "hex dump", useful for debugging.
+ *
+ * @param ab the byte array
+ * @param cBytesPerLine the number of bytes to show in each line of the hex
+ * dump
+ *
+ * @return a String containing the hex dump
+ */
+ public static String byteArrayToHexDump(byte[] ab, int cBytesPerLine)
+ {
+ return byteArrayToHexDump(ab, 0, ab.length, cBytesPerLine);
+ }
+
+ /**
+ * Convert an array of bytes to a long value.
+ *
+ * @param ab the array of bytes
+ * @param of the starting index
+ *
+ * @return the long value
+ */
+ public static long byteArrayToLong(byte[] ab, int of)
+ {
+ return ((long) (ab[of++]) << 56)
+ + ((long) (ab[of++] & 0xFF) << 48)
+ + ((long) (ab[of++] & 0xFF) << 40)
+ + ((long) (ab[of++] & 0xFF) << 32)
+ + ((long) (ab[of++] & 0xFF) << 24)
+ + ( (ab[of++] & 0xFF) << 16)
+ + ( (ab[of++] & 0xFF) << 8 )
+ + ( ab[of ] & 0xFF );
+ }
+
+ /**
+ * Produce an array of bytes for the specified long value.
+ *
+ * @param l the long value
+ *
+ * @return the byte array
+ */
+ static public byte[] toByteArray(long l)
+ {
+ return new byte[]
+ {
+ (byte) (l >> 56),
+ (byte) (l >> 48),
+ (byte) (l >> 40),
+ (byte) (l >> 32),
+ (byte) (l >> 24),
+ (byte) (l >> 16),
+ (byte) (l >> 8 ),
+ (byte) l,
+ };
+ }
+
+ /**
+ * Render the bytes of the specified long value into an array of bytes at the specified location.
+ *
+ * @param l the long value
+ * @param ab the byte array to copy into
+ * @param ab the byte array offset to write the long value at
+ *
+ * @return the byte array
+ */
+ static public void toByteArray(long l, byte[] ab, int of)
+ {
+ ab[of++] = (byte) (l >> 56);
+ ab[of++] = (byte) (l >> 48);
+ ab[of++] = (byte) (l >> 40);
+ ab[of++] = (byte) (l >> 32);
+ ab[of++] = (byte) (l >> 24);
+ ab[of++] = (byte) (l >> 16);
+ ab[of++] = (byte) (l >> 8 );
+ ab[of ] = (byte) l;
+ }
+
+ /**
+ * Determine how many hex digits it will take to render the passed <tt>int</tt> value as an
+ * <b>unsigned</b> hex value.
+ *
+ * @param n the int value, which is treated as an unsigned 32-bit value
+ *
+ * @return the number of hex digits required to render the int value
+ */
+ public static int countHexDigits(int n)
+ {
+ return Math.max((32 - Integer.numberOfLeadingZeros(n) + 3) / 4, 1);
+ }
+
+ /**
+ * Render the passed <tt>int</tt> into the passed StringBuilder as the specified number of hex
+ * digits.
+ *
+ * @param sb the StringBuilder to append to
+ * @param n the int value
+ * @param cch the number of hex digits to render
+ *
+ * @return the StringBuilder
+ */
+ public static StringBuilder appendIntAsHex(StringBuilder sb, int n, int cch)
+ {
+ assert cch >= 0 && cch <= 8;
+ for (int cBits = (cch-1) * 4; cBits >= 0; cBits -= 4)
+ {
+ sb.append(nibbleToChar(n >> cBits));
+ }
+ return sb;
+ }
+
+ /**
+ * Render the passed <tt>int</tt> into the passed StringBuilder as 8 hexadecimal digits.
+ *
+ * @param sb the StringBuilder to append to
+ * @param n the int value
+ *
+ * @return the StringBuilder
+ */
+ public static StringBuilder appendIntAsHex(StringBuilder sb, int n)
+ {
+ return appendIntAsHex(sb, n, 8);
+ }
+
+ /**
+ * Format the passed <tt>int</tt> into a String of the form "0x12345678".
+ *
+ * @param n the byte value
+ *
+ * @return a hexadecimal String representation of the value
+ */
+ public static String intToHexString(int n)
+ {
+ return appendIntAsHex(new StringBuilder(10).append("0x"), n).toString();
+ }
+
+ /**
+ * Determine how many hex digits it will take to render the passed <tt>long</tt> value as an
+ * <b>unsigned</b> hex value.
+ *
+ * @param n the long value, which is treated as an unsigned 64-bit value
+ *
+ * @return the number of hex digits required to render the long value
+ */
+ public static int countHexDigits(long n)
+ {
+ return Math.max((64 - Long.numberOfLeadingZeros(n) + 3) / 4, 1);
+ }
+
+ /**
+ * Render the passed <tt>long</tt> into the passed StringBuilder as the specified number of hex
+ * digits.
+ *
+ * @param sb the StringBuilder to append to
+ * @param n the long value
+ * @param cch the number of hex digits to render
+ *
+ * @return the StringBuilder
+ */
+ public static StringBuilder appendLongAsHex(StringBuilder sb, long n, int cch)
+ {
+ assert cch >= 0 && cch <= 16;
+ for (int cBits = (cch-1) * 4; cBits >= 0; cBits -= 4)
+ {
+ sb.append(nibbleToChar((int) (n >> cBits)));
+ }
+ return sb;
+ }
+
+ /**
+ * Render the passed <tt>long</tt> into the passed StringBuilder as 16 hexadecimal digits.
+ *
+ * @param sb the StringBuilder to append to
+ * @param n the long value
+ *
+ * @return the StringBuilder
+ */
+ public static StringBuilder appendLongAsHex(StringBuilder sb, long n)
+ {
+ return appendLongAsHex(sb, n, 16);
+ }
+
+
+ /**
+ * Format the passed <tt>int</tt> into a String of the form "0x12345678".
+ *
+ * @param n the byte value
+ *
+ * @return a hexadecimal String representation of the value
+ */
+ public static String longToHexString(long n)
+ {
+ return appendLongAsHex(new StringBuilder(18).append("0x"), n).toString();
+ }
+
+ /**
+ * Format the specified byte value into the passed char array as two hex digits.
+ *
+ * @param n the byte value
+ * @param ach the char array to render into
+ * @param of the offset into the char array for the first hex digit
+ *
+ * @return the offset of the first character after the two hex digits
+ */
+ public static int renderByteToHex(int n, char[] ach, int of)
+ {
+ ach[of ] = nibbleToChar(n >> 4);
+ ach[of+1] = nibbleToChar(n);
+ return of + 2;
+ }
+
+ /**
+ * Format the specified int value into the passed char array as the specified number of hex
+ * digits.
+ *
+ * @param n the int value
+ * @param ach the char array to render into
+ * @param of the offset into the char array for the first hex digit
+ * @param cch the number of hex digits to render
+ *
+ * @return the offset of the first character after the last hex digit
+ */
+ public static int renderIntToHex(int n, char[] ach, int of, int cch)
+ {
+ for (int i = 0; i < cch; ++i)
+ {
+ ach[of+cch-i-1] = nibbleToChar(n);
+ n >>>= 4;
+ }
+ return of + cch;
+ }
+
+ /**
+ * Format the specified long value into the passed char array as the specified number of hex
+ * digits.
+ *
+ * @param n the long value
+ * @param ach the char array to render into
+ * @param of the offset into the char array for the first hex digit
+ * @param cch the number of hex digits to render
+ *
+ * @return the offset of the first character after the last hex digit
+ */
+ public static int renderLongToHex(long n, char[] ach, int of, int cch)
+ {
+ for (int i = 0; i < cch; ++i)
+ {
+ ach[of+cch-i-1] = nibbleToChar((int) n);
+ n >>>= 4;
+ }
+ return of + cch;
+ }
+
+ /**
+ * Determine if the specified character is a decimal digit.
+ *
+ * @param ch a character
+ *
+ * @return true iff the character is a decimal digit
+ */
+ public static boolean isDigit(char ch)
+ {
+ return ch >= '0' & ch <= '9';
+ }
+
+ /**
+ * For a decimal digit, determine its integer value.
+ *
+ * @param ch a decimal digit
+ *
+ * @return the integer value of the specified decimal digit
+ */
+ public static int digitValue(char ch)
+ {
+ assert isDigit(ch);
+ return ch - '0';
+ }
+
+ /**
+ * Determine if the specified character is a hexadecimal digit.
+ *
+ * @param ch a character
+ *
+ * @return true iff the character is a hexadecimal digit
+ */
+ public static boolean isHexit(char ch)
+ {
+ // hexits in the unicode range appear in this order:
+ // {..., 0, ..., 9, ..., A, ..., F, ..., a, ..., f, ...}
+ // so a hexit has to be in the range 0..f, and the entire character
+ // range from 0..f is only 0x36 (so it fits inside a long mask)
+ return ch >= '0' & ch <= 'f' & ((1L << ('f' - ch)) &
+ // 3 4 5 6
+ // 0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456
+ // 0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdef
+ 0b01111111111000000011111100000000000000000000000000111111L) != 0;
+ }
+
+ /**
+ * For a hexadecimal digit, determine its integer value.
+ *
+ * @param ch a hexadecimal digit
+ *
+ * @return the integer value of the specified hexadecimal digit
+ */
+ public static int hexitValue(char ch)
+ {
+ assert isHexit(ch);
+ return ch <= '9' ? ch - '0' : (ch | 0x20) - ('a' - 0xA);
+ }
+
+ /**
+ * Determine if the specified character is an ASCII letter, which is in the range 'A' to 'Z',
+ * or 'a' to 'z'.
+ *
+ * @param ch the character to test
+ *
+ * @return true iff the character is an ASCII letter
+ */
+ public static boolean isAsciiLetter(char ch)
+ {
+ // ASCII letters in the unicode range appear in this order:
+ // {..., A, ..., Z, ..., a, ..., z, ...}
+ return ch >= 'A' & ch <= 'z' & ((1L << ('z' - ch)) &
+ // 4 5 6 7
+ // 123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789A
+ // ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz
+ 0b01111111111111111111111111100000011111111111111111111111111L) != 0;
+ }
+
+
+ // ----- String manipulation -------------------------------------------------------------------
+
+ /**
+ * Count the number occurrences of the specified character in the passed String.
+ *
+ * @param s the String to scan
+ * @param ch the character to scan for
+ *
+ * @return the number of occurrences of the character
+ */
+ public static int countChar(String s, char ch)
+ {
+ int c = 0;
+ int of = s.indexOf(ch);
+ while (of >= 0)
+ {
+ ++c;
+ of = s.indexOf(ch, of + 1);
+ }
+ return c;
+ }
+
+ /**
+ * Using the specified delimiter, parse the passed String into an array of Strings, each of
+ * which occurred in the original String but separated by the specified delimiter.
+ *
+ * @param s the String to parse
+ * @param chDelim the character delimiter
+ *
+ * @return the array of Strings parsed from the passed String
+ */
+ public static String[] parseDelimitedString(String s, char chDelim)
+ {
+ if (s == null)
+ {
+ return null;
+ }
+
+ if (s.length() == 0)
+ {
+ return NO_ARGS;
+ }
+
+ int of = s.indexOf(chDelim);
+ if (of < 0)
+ {
+ return new String[] {s};
+ }
+
+ ArrayList<String> list = new ArrayList<>();
+ int ofPrev = 0;
+ do
+ {
+ list.add(s.substring(ofPrev, of));
+ ofPrev = of + 1;
+ of = s.indexOf(chDelim, ofPrev);
+ }
+ while (of >= 0);
+ list.add(s.substring(ofPrev));
+ return list.toArray(NO_ARGS);
+ }
+
+ /**
+ * Create a string containing the specified number of the specified character.
+ *
+ * @param ch the character to duplicate
+ * @param cch the size of the resulting string
+ *
+ * @return a string containing the specified number of the specified character
+ */
+ public static String dup(char ch, int cch)
+ {
+ StringBuilder sb = new StringBuilder(cch);
+ while (cch-- > 0)
+ {
+ sb.append(ch);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Indent each line of the passed text by prepending the specified indentation String.
+ *
+ * @param sText the text to indent
+ * @param sIndent the indentation to use
+ *
+ * @return the indented String
+ */
+ public static String indentLines(String sText, String sIndent)
+ {
+ int cchOld = sText.length();
+ if (cchOld == 0)
+ {
+ return "";
+ }
+
+ int cLines = countChar(sText, '\n') + 1;
+ int cchNew = cchOld + cLines * sIndent.length();
+ StringBuilder sb = new StringBuilder(cchNew);
+
+ int ofLine = 0;
+ int ofNewline = sText.indexOf('\n');
+ while (ofNewline >= 0)
+ {
+ if (ofNewline > ofLine)
+ {
+ sb.append(sIndent);
+ }
+ sb.append(sText, ofLine, ofNewline + 1);
+
+ ofLine = ofNewline + 1;
+ ofNewline = sText.indexOf('\n', ofLine);
+ }
+
+ if (ofLine < cchOld)
+ {
+ sb.append(sIndent)
+ .append(sText, ofLine, cchOld);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Determine if the specified character needs to be escaped in order to be displayed.
+ *
+ * @param ch the character to evaluate
+ *
+ * @return true iff the character should be escaped in order to be displayed
+ */
+ public static boolean isCharEscaped(char ch)
+ {
+ if (ch < 0x80)
+ {
+ return ch < 0x20 || ch == '\'' || ch == '\"' || ch == '\\' || ch == 0x7F;
+ }
+ else
+ {
+ return !Character.isValidCodePoint(ch) ||
+ Character.getType(ch) == Character.CONTROL ||
+ ch == '\u2028' || ch == '\u2029';
+ }
+ }
+
+ /**
+ * Append the specified character to the StringBuilder, escaping if necessary.
+ *
+ * @param sb the StringBuilder to append to
+ * @param ch the character to escape
+ *
+ * @return the StringBuilder
+ */
+ public static StringBuilder appendChar(StringBuilder sb, char ch)
+ {
+ if (isCharEscaped(ch))
+ {
+ switch (ch)
+ {
+ case '\\':
+ return sb.append("\\\\");
+ case '\b':
+ return sb.append("\\b");
+ case '\f':
+ return sb.append("\\f");
+ case '\n':
+ return sb.append("\\n");
+ case '\r':
+ return sb.append("\\r");
+ case '\t':
+ return sb.append("\\t");
+ case '\'':
+ return sb.append("\\'");
+ case '\"':
+ return sb.append("\\\"");
+ case 0x7F: // DEL
+ return sb.append("\\d");
+ default:
+ return appendIntAsHex(sb.append('\\').append('u'), ch, 4);
+ }
+ }
+ else
+ {
+ return sb.append(ch);
+ }
+ }
+
+ /**
+ * Render a character as it would appear as a constant in source code.
+ *
+ * @param ch the character to quote
+ *
+ * @return a string showing the specified character, escaped if necessary
+ */
+ public static String quotedChar(char ch)
+ {
+ return appendChar(new StringBuilder(9).append('\''), ch).append('\'').toString();
+ }
+
+ /**
+ * Append the specified String to the StringBuilder, escaping if and as necessary.
+ *
+ * @param sb the StringBuilder to append to
+ * @param s the String to escape as necessary
+ *
+ * @return the StringBuilder
+ */
+ public static StringBuilder appendString(StringBuilder sb, String s)
+ {
+ for (int of = 0, cch = s.length(); of < cch; ++of)
+ {
+ appendChar(sb, s.charAt(of));
+ }
+ return sb;
+ }
+
+ /**
+ * Render a String as it would appear as a constant in source code.
+ *
+ * @param s the String to quote
+ *
+ * @return a String showing the quoted version of the passed String,
+ * escaping characters if and when necessary
+ */
+ public static String quotedString(String s)
+ {
+ return appendString(new StringBuilder(s.length() + 2).append('\"'), s).append(
+ '\"').toString();
+ }
+
+
+ // ----- packed integers -----------------------------------------------------------------------
+
+ /**
+ * Read a variable-length encoded integer value from a stream.
+ *
+ * @param in a <tt>DataInput</tt> stream to read from
+ *
+ * @return a <tt>long</tt> value
+ *
+ * @throws IOException if an I/O exception occurs
+ * @throws NumberFormatException if the integer does not fit into
+ * a <tt>long</tt> value
+ */
+ public static long readPackedLong(DataInput in)
+ throws IOException
+ {
+ return PackedInteger.readLong(in);
+ }
+
+ /**
+ * Write a signed 64-bit integer to a stream using variable-length encoding.
+ *
+ * @param out the <tt>DataOutput</tt> stream to write to
+ * @param n the <tt>long</tt> value to write
+ *
+ * @throws IOException if an I/O exception occurs
+ */
+ public static void writePackedLong(DataOutput out, long n)
+ throws IOException
+ {
+ PackedInteger.writeLong(out, n);
+ }
+
+ /**
+ * Read a variable-length encoded 32-bit integer from a stream.
+ *
+ * @param in a <tt>DataInput</tt> stream to read from
+ *
+ * @return an <tt>int</tt> value in the range <tt>Integer.MIN_VALUE..Integer.MAX_VALUE</tt>
+ *
+ * @throws java.io.IOException if an I/O exception occurs
+ */
+ public static int readPackedInt(DataInput in)
+ throws IOException
+ {
+ long n = readPackedLong(in);
+ if (n < Integer.MIN_VALUE || n > Integer.MAX_VALUE)
+ {
+ throw new IOException("value (" + n + ") exceeds 32-bit range");
+ }
+
+ return (int) n;
+ }
+
+ /**
+ * Read a variable-length encoded 32-bit integer magnitude from a stream.
+ * <p>
+ * Note that while the XVM itself is a 64-bit machine, Java does not support 64-bit (or even
+ * unsigned 32-bit) magnitudes. This method is a convenience method that verifies that the
+ * magnitude is within a range supported by Java.
+ *
+ * @param in a <tt>DataInput</tt> stream to read from
+ *
+ * @return an <tt>int</tt> value in the range <tt>0..Integer.MAX_VALUE</tt>
+ *
+ * @throws java.io.IOException if an I/O exception occurs
+ */
+ public static int readMagnitude(DataInput in)
+ throws IOException
+ {
+ long n = readPackedLong(in);
+ if (n > Integer.MAX_VALUE)
+ {
+ // this is unsupported in Java; arrays are limited in size
+ // by their use of signed 32-bit magnitudes and indexes
+ throw new IOException("magnitude (" + n + ") exceeds 32-bit maximum");
+ }
+ else if (n < 0)
+ {
+ throw new IOException("negative magnitude (" + n + ") is illegal");
+ }
+
+ return (int) n;
+ }
+
+ /**
+ * Read a variable-length encoded 32-bit integer index from a stream.
+ * <p>
+ * Note that while the XVM itself is a 64-bit machine, Java does not support 64-bit (or even
+ * unsigned 32-bit) indexes. This method is a convenience method that verifies that the index is
+ * within a range supported by Java.
+ *
+ * @param in a <tt>DataInput</tt> stream to read from
+ *
+ * @return an <tt>int</tt> value in the range <tt>0..Integer.MAX_VALUE</tt>,
+ * or <tt>-1</tt>
+ *
+ * @throws java.io.IOException if an I/O exception occurs
+ */
+ public static int readIndex(DataInput in)
+ throws IOException
+ {
+ long n = readPackedLong(in);
+ if (n > Integer.MAX_VALUE)
+ {
+ // this is unsupported in Java; arrays are limited in size
+ // by their use of signed 32-bit magnitudes and indexes
+ throw new IOException("index (" + n + ") exceeds 32-bit maximum");
+ }
+ else if (n < -1)
+ {
+ throw new IOException("negative index (" + n + ") is illegal");
+ }
+
+ return (int) n;
+ }
+
+
+ // ----- unicode -------------------------------------------------------------------------------
+
+ /**
+ * Read a single unicode character (code-point) from the passed DataInput, encoded using the
+ * UTF-8 format.
+ *
+ * @param in the DataInput to read from
+ *
+ * @return a Unicode code-point (which can exceed the 16-bit <tt>char</tt>
+ * type in Java)
+ *
+ * @throws IOException if an I/O exception occurs while reading the data,
+ * or if a UTF-8 format error is detected
+ */
+ public static int readUtf8Char(DataInput in)
+ throws IOException
+ {
+ int b = in.readUnsignedByte();
+ if ((b & 0x80) == 0)
+ {
+ // ASCII - single byte 0xxxxxxx format
+ return b;
+ }
+
+ // otherwise the format is based on the number of high-order 1-bits:
+ // #1s first byte trailing # trailing bits code-points
+ // --- ---------- -------- ---------- ---- -----------------------
+ // 2 110xxxxx 10xxxxxx 1 11 U+0080 - U+07FF
+ // 3 1110xxxx 10xxxxxx 2 16 U+0800 - U+FFFF
+ // 4 11110xxx 10xxxxxx 3 21 U+10000 - U+1FFFFF
+ // 5 111110xx 10xxxxxx 4 26 U+200000 - U+3FFFFFF
+ // 6 1111110x 10xxxxxx 5 31 U+4000000 - U+7FFFFFFF
+ switch (Integer.highestOneBit(~(0xFFFFFF00 | b)))
+ {
+ case 0b00100000: // 2-byte format
+ return (b & 0b00011111) << 6 | nextCharBits(in);
+ case 0b00010000: // 3-byte format
+ return (b & 0b00001111) << 12 | nextCharBits(in) << 6 | nextCharBits(in);
+ case 0b00001000: // 4-byte format
+ return (b & 0b00000111) << 18 | nextCharBits(in) << 12 | nextCharBits(in) << 6 | nextCharBits(in);
+ case 0b00000100: // 5-byte format
+ return (b & 0b00000011) << 24 | nextCharBits(in) << 18 | nextCharBits(in) << 12 | nextCharBits(in) << 6 | nextCharBits(in);
+ case 0b00000010: // 6-byte format
+ return (b & 0b00000001) << 30 | nextCharBits(in) << 24 | nextCharBits(in) << 18 | nextCharBits(in) << 12 | nextCharBits(in) << 6 | nextCharBits(in);
+ default:
+ throw new UTFDataFormatException("initial byte: " + byteToHexString(b));
+ }
+ }
+
+ /**
+ * Internal: Read a UTF-8 continuation byte and validate/decode it.
+ *
+ * @param in the DataInput to read from
+ *
+ * @return the 6 bits of UTF-8 data from the continuation byte
+ *
+ * @throws IOException if an I/O exception occurs while reading the data,
+ * or if a UTF-8 format error is detected
+ */
+ private static int nextCharBits(DataInput in)
+ throws IOException
+ {
+ int n = in.readUnsignedByte();
+ if ((n & 0b11000000) != 0b10000000)
+ {
+ throw new UTFDataFormatException("trailing unicode byte does not match 10xxxxxx");
+ }
+ return n & 0b00111111;
+ }
+
+ /**
+ * Write a single unicode character (code-point) to the passed DataOutput, encoding using the
+ * UTF-8 format.
+ *
+ * @param out the DataOutput to write to
+ * @param ch the char to write out as UTF-8
+ *
+ * @return a Unicode code-point (which can exceed the 16-bit <tt>char</tt>
+ * type in Java)
+ *
+ * @throws IOException if an I/O exception occurs while writing the data,
+ * or if a UTF-8 format error is detected
+ */
+ public static void writeUtf8Char(DataOutput out, int ch)
+ throws IOException
+ {
+ if ((ch & ~0x7F) == 0)
+ {
+ // ASCII - single byte 0xxxxxxx format
+ out.write(ch);
+ return;
+ }
+
+ // otherwise the format is based on the number of significant bits:
+ // bits code-points first byte trailing # trailing
+ // ---- ----------------------- ---------- -------- ----------
+ // 11 U+0080 - U+07FF 110xxxxx 10xxxxxx 1
+ // 16 U+0800 - U+FFFF 1110xxxx 10xxxxxx 2
+ // 21 U+10000 - U+1FFFFF 11110xxx 10xxxxxx 3
+ // 26 U+200000 - U+3FFFFFF 111110xx 10xxxxxx 4
+ // 31 U+4000000 - U+7FFFFFFF 1111110x 10xxxxxx 5
+ int cTrail;
+ switch (Integer.highestOneBit(ch))
+ {
+ case 0b00000000000000000000000010000000:
+ case 0b00000000000000000000000100000000:
+ case 0b00000000000000000000001000000000:
+ case 0b00000000000000000000010000000000:
+ out.write(0b11000000 | ch >>> 6);
+ cTrail = 1;
+ break;
+
+ case 0b00000000000000000000100000000000:
+ case 0b00000000000000000001000000000000:
+ case 0b00000000000000000010000000000000:
+ case 0b00000000000000000100000000000000:
+ case 0b00000000000000001000000000000000:
+ out.write(0b11100000 | ch >>> 12);
+ cTrail = 2;
+ break;
+ case 0b00000000000000010000000000000000:
+ case 0b00000000000000100000000000000000:
+ case 0b00000000000001000000000000000000:
+ case 0b00000000000010000000000000000000:
+ case 0b00000000000100000000000000000000:
+ out.write(0b11110000 | ch >>> 18);
+ cTrail = 3;
+ break;
+ case 0b00000000001000000000000000000000:
+ case 0b00000000010000000000000000000000:
+ case 0b00000000100000000000000000000000:
+ case 0b00000001000000000000000000000000:
+ case 0b00000010000000000000000000000000:
+ out.write(0b11111000 | ch >>> 24);
+ cTrail = 4;
+ break;
+ case 0b00000100000000000000000000000000:
+ case 0b00001000000000000000000000000000:
+ case 0b00010000000000000000000000000000:
+ case 0b00100000000000000000000000000000:
+ case 0b01000000000000000000000000000000:
+ out.write(0b11111100 | ch >>> 30);
+ cTrail = 5;
+ break;
+
+ default:
+ throw new UTFDataFormatException("illegal character: " + intToHexString(ch));
+ }
+
+ // write out trailing bytes; each has the same "10xxxxxx" format with 6
+ // bits of data
+ while (cTrail > 0)
+ {
+ out.write(0b10_000000 | (ch >>> --cTrail * 6 & 0b00_111111));
+ }
+ }
+
+ /**
+ * Read a length-encoded string of unicode characters (code-point) from the passed DataInput,
+ * encoded using the UTF-8 format. The format is the number of bytes in the stream used for the
+ * UTF-8 character encoding, encoded as a packed integer, followed by the difference in the
+ * number of bytes and the number of characters, the difference being 0 or greater, encoded as
+ * a packed integer, followed by the specified number of characters, encoded using UTF-8.
+ *
+ * @param in the DataInput to read from
+ *
+ * @return a Unicode code-point (which can exceed the 16-bit <tt>char</tt>
+ * type in Java)
+ *
+ * @throws IOException if an I/O exception occurs while reading the data,
+ * or if a UTF-8 format error is detected
+ */
+ public static String readUtf8String(DataInput in)
+ throws IOException
+ {
+ int cb = readMagnitude(in);
+ int cch = cb - readMagnitude(in);
+ StringBuilder sb = new StringBuilder(cch);
+ for (int ofch = 0; ofch < cch; ++ofch)
+ {
+ int ch = readUtf8Char(in);
+ if (ch >= 0xFFFF)
+ {
+ if (!Character.isSupplementaryCodePoint(ch))
+ {
+ throw new UTFDataFormatException(
+ "Character is outside of UTF-16 (including supplemental) range: " +
+ intToHexString(ch));
+ }
+
+ // the "supplemental" range is composed of two UTF-16 characters
+ sb.append(Character.highSurrogate(ch)) // "leading"
+ .append(Character.lowSurrogate(ch)); // "trailing"
+ }
+ else
+ {
+ sb.append((char) ch);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Write a length-encoded string of unicode characters (code-points) to the passed DataOutput,
+ * encoding using the UTF-8 format. The length encoding is the number of bytes in the stream
+ * used for the UTF-8 character encoding, encoded as a packed integer, followed by the
+ * difference in the number of bytes and the number of characters, the difference being 0 or
+ * greater, encoded as a packed integer.
+ *
+ * @param out the DataOutput to write to
+ * @param s the String to write out as UTF-8
+ *
+ * @throws IOException if an I/O exception occurs while writing the data,
+ * or if a UTF-8 format error is detected
+ */
+ public static void writeUtf8String(DataOutput out, String s)
+ throws IOException
+ {
+ // figure out the actual number of unicode code-points (not the number
+ // of "Java chars") and how many bytes the UTF-8 format will use
+ final int cch = s.length(); // count of UTF-16 "Java chars"
+ final int[] anch = new int[cch]; // array of unicode code points
+ int cb = 0; // count of bytes
+ int cnch = 0; // count of unicode code points
+ for (int ofch = 0; ofch < cch; ++ofch)
+ {
+ char ch = s.charAt(ofch);
+ int nch;
+ if (Character.isSurrogate(ch))
+ {
+ // first character must be a high surrogate
+ if (!Character.isHighSurrogate(ch))
+ {
+ throw new UTFDataFormatException(
+ "low surrogate unexpected: " + intToHexString(ch));
+ }
+
+ // next character must be a low surrogate
+ char ch2;
+ if (++ofch >= cch || !Character.isLowSurrogate(ch2 = s.charAt(ofch)))
+ {
+ throw new UTFDataFormatException(
+ "low surrogate expected after: " + intToHexString(ch));
+ }
+
+ nch = Character.toCodePoint(ch, ch2);
+ }
+ else
+ {
+ nch = ch;
+ }
+
+ anch[cnch++] = nch;
+ cb += calcUtf8Length(nch);
+ }
+
+ // write out the UTF-8 form of the String
+ assert cb >= cnch;
+ writePackedLong(out, cb);
+ writePackedLong(out, cb - cnch);
+ for (int of = 0; of < cnch; ++of)
+ {
+ writeUtf8Char(out, anch[of]);
+ }
+ }
+
+ /**
+ * Internal: Calculate the UTF-8 length for a particular code-point.
+ *
+ * @param ch the unicode character
+ *
+ * @return the number of bytes required to format the unicode character to
+ * UTF-8
+ *
+ * @throws UTFDataFormatException if an invalid character is passed
+ */
+ private static int calcUtf8Length(int ch)
+ throws IOException
+ {
+ if ((ch & ~0x7F) == 0)
+ {
+ // ASCII - single byte 0xxxxxxx format
+ return 1;
+ }
+
+ switch (Integer.highestOneBit(ch))
+ {
+ case 0b00000000000000000000000010000000:
+ case 0b00000000000000000000000100000000:
+ case 0b00000000000000000000001000000000:
+ case 0b00000000000000000000010000000000:
+ return 2;
+
+ case 0b00000000000000000000100000000000:
+ case 0b00000000000000000001000000000000:
+ case 0b00000000000000000010000000000000:
+ case 0b00000000000000000100000000000000:
+ case 0b00000000000000001000000000000000:
+ return 3;
+
+ case 0b00000000000000010000000000000000:
+ case 0b00000000000000100000000000000000:
+ case 0b00000000000001000000000000000000:
+ case 0b00000000000010000000000000000000:
+ case 0b00000000000100000000000000000000:
+ return 4;
+
+ case 0b00000000001000000000000000000000:
+ case 0b00000000010000000000000000000000:
+ case 0b00000000100000000000000000000000:
+ case 0b00000001000000000000000000000000:
+ case 0b00000010000000000000000000000000:
+ return 5;
+
+ case 0b00000100000000000000000000000000:
+ case 0b00001000000000000000000000000000:
+ case 0b00010000000000000000000000000000:
+ case 0b00100000000000000000000000000000:
+ case 0b01000000000000000000000000000000:
+ return 6;
+
+ default:
+ throw new UTFDataFormatException("illegal character: " + intToHexString(ch));
+ }
+ }
+
+ // ----- file I/O ------------------------------------------------------------------------------
+
+ /**
+ * Open the specified file as an InputStream.
+ *
+ * @param file the file to open
+ *
+ * @return an InputStream to the specified file
+ *
+ * @throws IOException if an IOException occurs while reading the
+ * FileStructure
+ */
+ public static InputStream toInputStream(File file)
+ throws IOException
+ {
+ if (!file.exists())
+ {
+ throw new IOException("file does not exist: " + file);
+ }
+
+ if (!file.isFile() || !file.canRead())
+ {
+ throw new IOException("not a readable file: " + file);
+ }
+
+ return new BufferedInputStream(new FileInputStream(file));
+ }
+
+ /**
+ * Read the raw bytes contained in the specified file.
+ *
+ * @param file the file to read
+ *
+ * @return the binary contents of the file
+ *
+ * @throws IOException indicates a failure to read the binary contents
+ * of the specified file
+ */
+ public static byte[] readFileBytes(File file)
+ throws IOException
+ {
+ if (file == null)
+ {
+ throw new IllegalArgumentException("file required");
+ }
+
+ if (!file.exists() || !file.isFile())
+ {
+ throw new FileNotFoundException(file.toString());
+ }
+
+ final long lcb = file.length();
+ if (lcb == 0L)
+ {
+ return EMPTY_BYTE_ARRAY;
+ }
+ else if (lcb > Integer.MAX_VALUE - 1024)
+ {
+ // assume a maximum byte array size just under 2GB
+ throw new IOException("file exceeds max supported length (2GB): "
+ + file + "=" + lcb + " bytes");
+ }
+
+ // read the complete contents of the file
+ int cb = (int) lcb;
+ byte[] ab = new byte[cb];
+ try (FileInputStream stream = new FileInputStream(file))
+ {
+ for (int of = 0; of < cb; )
+ {
+ int cbChunk = stream.read(ab, of, cb-of);
+ if (cbChunk < 0)
+ {
+ throw new EOFException("unexpected end-of-file: " + file);
+ }
+ of += cbChunk;
+ }
+ }
+
+ if (file.length() != lcb)
+ {
+ throw new IOException("file was concurrently modified while being read: " + file);
+ }
+
+ return ab;
+ }
+
+ /**
+ * Read the text contents of the specified file. Supported formats are ASCII, UTF-8, UTF-16, and
+ * UTF-32; failure to identify any of those formats will cause the default encoding to be used.
+ *
+ * @param file the file to read
+ *
+ * @return the character contents of the file
+ *
+ * @throws IOException indicates a failure to read the character contents
+ * of the specified file
+ */
+ public static char[] readFileChars(File file)
+ throws IOException
+ {
+ return readFileChars(file, null);
+ }
+
+ /**
+ * Read the text contents of the specified file using the specified encoding. If no encoding is
+ * specified, then determine the encoding in the manner specified by {@link #readFileChars(File)}.
+ *
+ * @param file the file to read
+ * @param sEncoding the specific encoding to use, or null to attempt to
+ * determine the encoding automatically
+ *
+ * @return the character contents of the file
+ *
+ * @throws IOException indicates a failure to read the character contents
+ * of the specified file
+ */
+ public static char[] readFileChars(File file, String sEncoding)
+ throws IOException
+ {
+ byte[] ab = readFileBytes(file);
+ int cb = ab.length;
+ if (cb == 0)
+ {
+ return EMPTY_CHAR_ARRAY;
+ }
+
+ // check to see if there is a byte order mark
+ int cbBOM = 0;
+ if (sEncoding == null)
+ {
+ switch (cb)
+ {
+ default:
+ if (ab[0] == 0x00 && ab[1] == 0x00 && (ab[2] & 0xFF) == 0xFE && (ab[3] & 0xFF) == 0xFF)
+ {
+ sEncoding = "UTF-32BE";
+ cbBOM = 4;
+ break;
+ }
+ else if ((ab[0] & 0xFF) == 0xFF && (ab[1] & 0xFF) == 0xFE && ab[2] == 0x00 && ab[3] == 0x00)
+ {
+ sEncoding = "UTF-32LE";
+ cbBOM = 4;
+ break;
+ }
+ case 3:
+ if ((ab[0] & 0xFF) == 0xEF && (ab[1] & 0xFF) == 0xBB && (ab[2] & 0xFF) == 0xBF)
+ {
+ sEncoding = "UTF-8";
+ cbBOM = 3;
+ break;
+ }
+ case 2:
+ if ((ab[0] & 0xFF) == 0xFE && (ab[1] & 0xFF) == 0xFF)
+ {
+ sEncoding = "UTF-16BE";
+ cbBOM = 2;
+ break;
+ }
+ else if ((ab[0] & 0xFF) == 0xFF && (ab[1] & 0xFF) == 0xFE)
+ {
+ sEncoding = "UTF-16LE";
+ cbBOM = 2;
+ break;
+ }
+ case 1:
+ case 0:
+ }
+ }
+
+ scan: if (sEncoding == null)
+ {
+ // quick scan to verify that the contents are ASCII
+ char[] ach = new char[cb];
+ for (int of = 0; of < cb; ++of)
+ {
+ int b = ab[of] & 0xFF;
+ if (b > 0x7F)
+ {
+ // use default encoding
+ break scan;
+ }
+ ach[of] = (char) (b & 0xFF);
+ }
+ return ach;
+ }
+
+ // use the encoding that was specified or otherwise determined, or fall
+ // back to the default encoding
+ Charset charset = sEncoding == null ? Charset.defaultCharset() : Charset.forName(sEncoding);
+ ByteBuffer bytebuf = ByteBuffer.wrap(ab, cbBOM, cb - cbBOM);
+ CharBuffer charbuf = charset.decode(bytebuf);
+ char[] ach = new char[charbuf.length()];
+ charbuf.get(ach);
+ return ach;
+ }
+
+
+ // ----- array and collection helpers ----------------------------------------------------------
+
+ /**
+ * Given an array of "base" elements and an array of elements that are being evaluated as some
+ * form of "addition" to those base elements, determine if there are any duplicates, and return
+ * an array that contains the non-duplicates.
+ *
+ * @param aoBase the array of "base" elements
+ * @param aoAdd the array of the elements being evaluated in addition to the base elements
+ *
+ * @return an array containing all of the elements from the {@code aoAdd} array that are not
+ * duplicates of elements in the {@code aoBase} array
+ */
+ public static <T> T[] dedupAdds(T[] aoBase, T[] aoAdd)
+ {
+ // there's a fair likelihood that *ALL* of the "adds" will be duplicates, and a fair
+ // likelihood that *NONE* of the "adds" will be unique, so assume both up front, and only
+ // de-optimize when *BOTH* of those two things have been proven to be false
+ int cBase = aoBase.length;
+ int cAdd = aoAdd.length;
+ boolean fAllDups = true;
+ boolean fNoDups = true;
+ ArrayList<T> listDeDup = null;
+ NextLayer: for (int iAdd = 0; iAdd < cAdd; ++iAdd)
+ {
+ T oAdd = aoAdd[iAdd];
+ for (int iBase = 0; iBase < cBase; ++iBase)
+ {
+ if (oAdd.equals(aoBase[iBase]))
+ {
+ // we found a duplicate; is it the first one?
+ if (fNoDups)
+ {
+ fNoDups = false;
+
+ // if we already know that there are some that are NOT duplicates, then we
+ // need to start maintaining a list of non-duplicates to add; since this is
+ // the first duplicate encountered, just take everything up to this point
+ if (!fAllDups)
+ {
+ assert listDeDup == null;
+ listDeDup = startList(aoAdd, iAdd);
+ }
+ }
+
+ // this one was a duplicate, so advance to the next thing to add
+ continue NextLayer;
+ }
+ }
+
+ // this one to add is NOT a duplicate; is it the first one?
+ if (fAllDups)
+ {
+ fAllDups = false;
+
+ // if we already know that there are duplicates (i.e. NOT no duplicates), then we
+ // need to start maintaining a list of non-duplicates (starting with this one)
+ if (!fNoDups)
+ {
+ assert listDeDup == null;
+ listDeDup = new ArrayList<>();
+ }
+ }
+
+ // if, for whatever reason, we are maintaining a list of non-duplicates by this point,
+ // and since we just verified that this is a non-duplicate, then add it to the list
+ if (listDeDup != null)
+ {
+ listDeDup.add(oAdd);
+ }
+ }
+
+ // at this point, we finished our check for duplicates; if there are no duplicates, then
+ // the original array of things to add is the result
+ if (fNoDups)
+ {
+ assert listDeDup == null;
+ return aoAdd;
+ }
+
+ // otherwise, there are duplicates, which boils down into two possibilities: they're all
+ // duplicates (so we return an empty array), or just some are duplicates (in which case
+ // there is a list of the ones that are NOT duplicates that we should return)
+ int cResult = listDeDup == null ? 0 : listDeDup.size();
+ T[] aoResult = (T[]) Array.newInstance(aoAdd.getClass().getComponentType(), cResult);
+ if (cResult > 0)
+ {
+ aoResult = listDeDup.toArray(aoResult);
+ }
+ return aoResult;
+ }
+
+ /**
+ * Glue two arrays together.
+ *
+ * @param aoBase the first array
+ * @param aoAdd the second array
+ *
+ * @return an array containing all of the elements of both passed arrays
+ */
+ public static <T> T[] append(T[] aoBase, T[] aoAdd)
+ {
+ int cBase = aoBase.length;
+ if (cBase == 0)
+ {
+ return aoAdd;
+ }
+
+ int cAdd = aoAdd.length;
+ if (cAdd == 0)
+ {
+ return aoBase;
+ }
+
+ int cResult = cBase + cAdd;
+ T[] aoResult = (T[]) Array.newInstance(aoAdd.getClass().getComponentType(), cResult);
+ System.arraycopy(aoBase, 0, aoResult, 0, cBase);
+ System.arraycopy(aoAdd, 0, aoResult, cBase, cAdd);
+ return aoResult;
+ }
+
+ /**
+ * Add the specified element at the top of the specified array.
+ *
+ * @param aoBase the array to add to
+ * @param oAdd the element to append at the head
+ *
+ * @return an array containing all the elements
+ */
+ public static <T> T[] appendHead(T[] aoBase, T oAdd)
+ {
+ int c = aoBase.length;
+ T[] aNew = (T[]) Array.newInstance(oAdd.getClass(), c + 1);
+
+ aNew[0] = oAdd;
+ System.arraycopy(aoBase, 0, aNew, 1, c);
+ return aNew;
+ }
+
+ /**
+ * Create an ArrayList that contains the first {@code c} elements of the array {@code ao}.
+ *
+ * @param ao an array of T
+ * @param c the number of elements of the array to put into the new list
+ *
+ * @return a new ArrayList of the first {@code c} elements of the {@code ao} array
+ */
+ public static <T> ArrayList<T> startList(T[] ao, int c)
+ {
+ return appendList(new ArrayList<>(), ao, 0, c);
+ }
+
+ /**
+ * Append the specified elements from the passed array to the list.
+ *
+ * @param list a list of T to add to
+ * @param ao an array of T to obtain the values-to-add from
+ * @param of the offset into the array
+ * @param c the number of elements from the array to add to the list
+ *
+ * @return the list
+ */
+ public static <T> ArrayList<T> appendList(ArrayList<T> list, T[] ao, int of, int c)
+ {
+ for (int ofEnd = of+c; of < ofEnd; ++of)
+ {
+ list.add(ao[of]);
+ }
+ return list;
+ }
+
+
+ // ----- hashing & equality --------------------------------------------------------------------
+
+ /**
+ * Perform a hash of the object. This performs a deep hash on arrays.
+ *
+ * @param o any object, or null
+ *
+ * @return a hashcode
+ */
+ public static int hashCode(final Object o)
+ {
+ return Hash.of(o);
+ }
+
+ /**
+ * Perform a comparison for equality. The types of the objects must be equal
+ * as well. This performs a deep comparison on arrays.
+ *
+ * @param o1 any object, or null
+ * @param o2 any object, or null
+ *
+ * @return true iff <tt>o1</tt> is equals to <tt>o2</tt>
+ */
+ public static boolean equals(final Object o1, final Object o2)
+ {
+ if (o1 == null)
+ {
+ return o2 == null;
+ }
+ if (o2 == null)
+ {
+ return false;
+ }
+
+ final Class clz1 = o1.getClass();
+ final Class clz2 = o1.getClass();
+ if (clz1 != clz2)
+ {
+ return false;
+ }
+
+ Class<?> clzComp = clz1.getComponentType();
+ return clzComp == null || clzComp != clz2.getComponentType()
+ ? o1.equals(o2)
+ : clzComp.isPrimitive()
+ ? clzComp == int .class ? Arrays.equals( (int[]) o1, (int[]) o2)
+ : clzComp == long .class ? Arrays.equals( (long[]) o1, (long[]) o2)
+ : clzComp == byte .class ? Arrays.equals( (byte[]) o1, (byte[]) o2)
+ : clzComp == char .class ? Arrays.equals( (char[]) o1, (char[]) o2)
+ : clzComp == double.class ? Arrays.equals( (double[]) o1, (double[]) o2)
+ : clzComp == float .class ? Arrays.equals( (float[]) o1, (float[]) o2)
+ : clzComp == short .class ? Arrays.equals( (short[]) o1, (short[]) o2)
+ : Arrays.equals((boolean[]) o1, (boolean[]) o2)
+ : Arrays.equals((Object[]) o1, (Object[]) o2);
+ }
+
+ /**
+ * Perform a comparison of two arrays for ordering. This performs a deep
+ * comparison on the arrays.
+ *
+ * @param ao1 an array of Comparable, or null
+ * @param ao2 an array of Comparable, or null
+ *
+ * @return negative, zero, or positive iff <tt>ao1</tt> is less than, equal
+ * to, or greater than <tt>ao2</tt>
+ */
+ public static int compareArrays(final Comparable[] ao1, final Comparable[] ao2)
+ {
+ if (ao1 == ao2)
+ {
+ return 0;
+ }
+
+ if (ao1 == null)
+ {
+ return -1;
+ }
+
+ if (ao2 == null)
+ {
+ return 1;
+ }
+
+ final int c1 = ao1.length;
+ final int c2 = ao2.length;
+ for (int i = 0, c = Math.min(c1, c2); i < c; ++i)
+ {
+ final int n = ao1[i].compareTo(ao2[i]);
+ if (n != 0)
+ {
+ return n;
+ }
+ }
+
+ return c1 - c2;
+ }
+
+ /**
+ * Verify that the array is non-null and all elements are non-null.
+ *
+ * @param ao an array of objects
+ *
+ * @return true iff all elements are non-null
+ *
+ * @throws IllegalArgumentException if the array is null, or any elements
+ * of the array are null
+ */
+ public static boolean checkElementsNonNull(Object[] ao)
+ {
+ if (ao == null)
+ {
+ throw new IllegalArgumentException("array is null");
+ }
+
+ for (Object o : ao)
+ {
+ if (o == null)
+ {
+ throw new IllegalArgumentException("array element is null");
+ }
+ }
+
+ return true;
+ }
+
+
+ // ----- constants -----------------------------------------------------------------------------
+
+ /**
+ * A constant empty array of <tt>byte</tt>.
+ */
+ public final static byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ /**
+ * A constant empty array of <tt>char</tt>.
+ */
+ public final static char[] EMPTY_CHAR_ARRAY = new char[0];
+
+ /**
+ * A constant empty array of <tt>String</tt>.
+ */
+ public final static String[] NO_ARGS = new String[0];
+ } \ No newline at end of file
diff --git a/common/src/main/java/com/seaofnodes/util/Hash.java b/common/src/main/java/com/seaofnodes/util/Hash.java
new file mode 100644
index 00000000..65626755
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/util/Hash.java
@@ -0,0 +1,299 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * The Initial Developer of the Original Software is
+ * The Ecstasy Project (https://github.com/xtclang/).
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+package com.seaofnodes.util;
+
+import java.util.Arrays;
+
+/**
+ * Helper functions for producing compound hashes.
+ *
+ * <p>
+ * For example given a class with fields {@code a, b, c, d} of any type, a compound hash can be
+ * computed via:
+ * <pre>{@code
+ * int hash = Hash.of(a,
+ * Hash.of(b,
+ * Hash.of(c,
+ * Hash.of(d))));
+ * }
+ * </pre>
+ *
+ * @author mf
+ */
+public final class Hash
+ {
+ private Hash()
+ {
+ // blocked constructor
+ }
+
+ /**
+ * Incorporate a value into a running hash.
+ *
+ * @param v the value (or hash) to incorporate into the running hash
+ * @param h the running hash to combine with
+ *
+ * @return the compound hash
+ */
+ public static int of(int v, int h)
+ {
+ return 31 * h + v;
+ }
+
+ /**
+ * Incorporate a value into a running hash.
+ *
+ * @param v the value to incorporate into the hash
+ * @param h the running hash to combine with
+ *
+ * @return the compound hash
+ */
+ public static int of(boolean v, int h)
+ {
+ return of(of(v), h);
+ }
+
+ /**
+ * Incorporate a value into a running hash.
+ *
+ * @param v the value to incorporate into the hash
+ * @param h the running hash to combine with
+ *
+ * @return the compound hash
+ */
+ public static int of(byte v, int h)
+ {
+ return of(of(v), h);
+ }
+
+ /**
+ * Incorporate a value into a running hash.
+ *
+ * @param v the value to incorporate into the hash
+ * @param h the running hash to combine with
+ *
+ * @return the compound hash
+ */
+ public static int of(char v, int h)
+ {
+ return of(of(v), h);
+ }
+
+ /**
+ * Incorporate a value into a running hash.
+ *
+ * @param v the value to incorporate into the hash
+ * @param h the running hash to combine with
+ *
+ * @return the compound hash
+ */
+ public static int of(short v, int h)
+ {
+ return of(of(v), h);
+ }
+
+ /**
+ * Incorporate a value into a running hash.
+ *
+ * @param v the value to incorporate into the hash
+ * @param h the running hash to combine with
+ *
+ * @return the compound hash
+ */
+ public static int of(float v, int h)
+ {
+ return of(of(v), h);
+ }
+
+ /**
+ * Incorporate a value into a running hash.
+ *
+ * @param v the value to incorporate into the hash
+ * @param h the running hash to combine with
+ *
+ * @return the compound hash
+ */
+ public static int of(long v, int h)
+ {
+ return of(of(v), h);
+ }
+
+ /**
+ * Incorporate a value into a running hash.
+ *
+ * @param v the value to incorporate into the hash
+ * @param h the running hash to combine with
+ *
+ * @return the compound hash
+ */
+ public static int of(double v, int h)
+ {
+ return of(of(v), h);
+ }
+
+ /**
+ * Incorporate a value into a running hash.
+ *
+ * @param v the value to incorporate into the hash
+ * @param h the running hash to combine with
+ *
+ * @return the compound hash
+ */
+ public static int of(Object v, int h)
+ {
+ return of(of(v), h);
+ }
+
+ /**
+ * Return a hash of the value.
+ *
+ * @param v the value
+ *
+ * @return the hash
+ */
+ public static int of(boolean v)
+ {
+ return Boolean.hashCode(v);
+ }
+
+ /**
+ * Return a hash of the value.
+ *
+ * @param v the value
+ *
+ * @return the hash
+ */
+ public static int of(byte v)
+ {
+ return Byte.hashCode(v);
+ }
+
+ /**
+ * Return a hash of the value.
+ *
+ * @param v the value
+ *
+ * @return the hash
+ */
+ public static int of(short v)
+ {
+ return Short.hashCode(v);
+ }
+
+ /**
+ * Return a hash of the value.
+ *
+ * @param v the value
+ *
+ * @return the hash
+ */
+ public static int of(char v)
+ {
+ return Character.hashCode(v);
+ }
+
+ /**
+ * Return a hash of the value.
+ *
+ * @param v the value
+ *
+ * @return the hash
+ */
+ public static int of(int v)
+ {
+ return Integer.hashCode(v);
+ }
+
+ /**
+ * Return a hash of the value.
+ *
+ * @param v the value
+ *
+ * @return the hash
+ */
+ public static int of(float v)
+ {
+ return Float.hashCode(v);
+ }
+
+ /**
+ * Return a hash of the value.
+ *
+ * @param v the value
+ *
+ * @return the hash
+ */
+ public static int of(long v)
+ {
+ return Long.hashCode(v);
+ }
+
+ /**
+ * Return a hash of the value.
+ *
+ * @param v the value
+ *
+ * @return the hash
+ */
+ public static int of(double v)
+ {
+ return Double.hashCode(v);
+ }
+
+ /**
+ * Return a hash of the value.
+ *
+ * <p>
+ * If the value is an array the hash will be via {@link java.util.Arrays#hashCode}.
+ *
+ * @param v the value
+ *
+ * @return the hash
+ */
+ public static int of(Object v)
+ {
+ return v == null
+ ? 0
+ : v.getClass().isArray()
+ ? ofArray(v)
+ : v.hashCode();
+ }
+
+ /**
+ * Return the hash of an array.
+ *
+ * @param v the non-null array
+ *
+ * @return the hash
+ */
+ private static int ofArray(Object v)
+ {
+ Class<?> clzComp = v.getClass().getComponentType();
+ return
+ clzComp.isPrimitive()
+ ? clzComp == int .class ? Arrays.hashCode( (int[]) v)
+ : clzComp == long .class ? Arrays.hashCode( (long[]) v)
+ : clzComp == byte .class ? Arrays.hashCode( (byte[]) v)
+ : clzComp == char .class ? Arrays.hashCode( (char[]) v)
+ : clzComp == double.class ? Arrays.hashCode( (double[]) v)
+ : clzComp == float .class ? Arrays.hashCode( (float[]) v)
+ : clzComp == short .class ? Arrays.hashCode( (short[]) v)
+ : Arrays.hashCode((boolean[]) v)
+ : Arrays.hashCode((Object[]) v);
+ }
+ }
diff --git a/common/src/main/java/com/seaofnodes/util/PackedInteger.java b/common/src/main/java/com/seaofnodes/util/PackedInteger.java
new file mode 100644
index 00000000..e032f8d0
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/util/PackedInteger.java
@@ -0,0 +1,1360 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * The Initial Developer of the Original Software is
+ * The Ecstasy Project (https://github.com/xtclang/).
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+package com.seaofnodes.util;
+
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+
+import java.math.BigInteger;
+
+
+/**
+ * A PackedInteger represents a signed, 2's-complement integer of 1-8192 bits (1-1024 bytes) in
+ * size. (The size limit is arbitrary, and temporary.)
+ * <p/>
+ * Values up to 8 bytes can be accessed as a <tt>long</tt> value, while values of any size can be
+ * accessed as a BigInteger.
+ * <p/>
+ * The storage format is compressed as much as possible. There are five storage formats:
+ * <ul><li>
+ * <b>Tiny</b>: For a value in the range -64..63 (7 bits), the value can be encoded in one byte.
+ * The least significant 7 bits of the value are shifted left by 1 bit, and the 0x1 bit is set to 1.
+ * When reading in a packed integer, if bit 0x1 of the first byte is 1, then it's Tiny.
+ * </li><li>
+ * <b>Small</b>: For a value in the range -4096..4095 (13 bits), the value can be encoded in two
+ * bytes. The first byte contains the value 0x2 (010) in the least significant 3 bits, and bits 8-12
+ * of the integer in bits 3-7; the second byte contains bits 0-7 of the integer.
+ * </li><li>
+ * <b>Medium</b>: For a value in the range -1048576..1048575 (21 bits), the value can be encoded in
+ * three bytes. The first byte contains the value 0x6 (110) in the least significant 3 bits, and
+ * bits 16-20 of the integer in bits 3-7; the second byte contains bits 8-15 of the integer; the
+ * third byte contains bits 0-7 of the integer.
+ * </li><li>
+ * <b>Large</b>: For a value in the range -(2^503)..2^503-1 (63 bytes), a value with `{@code s}`
+ * significant bits can be encoded in no less than {@code 1+max(1,(s+7)/8)} bytes; let `{@code b}`
+ * be the selected encoding length, in bytes. The first byte contains the value 0x0 (00) in the
+ * least significant 2 bits, and the least 6 significant bits of {@code (b-2)} in bits 2-7. The
+ * following {@code (b-1)} bytes contain the least significant {@code (b-1)*8} bits of the integer.
+ * </li><li>
+ * <b>Huge</b>: For any larger value (arbitrarily limited to the range -(2^8191)..2^8191-1 (1KB)),
+ * the first byte is 0b111111_00, followed by an embedded packed integer specifying the number of
+ * significant bytes of the enclosing packed integer, followed by that number of bytes of data.
+ * </li></ul>
+ * <p/>
+ * To maximize density and minimize pipeline stalls, the algorithms in this file use the smallest
+ * possible encoding for each value. Since an 8 significant-bit value can be encoded in two bytes
+ * using either a small or large encoding, we choose large to eliminate the potential for a
+ * (conditional-induced) pipeline stall. Since a 14..16 significant-bit value can be encoded in
+ * three bytes using either a medium or large encoding, we choose large for the same reason.
+ * <pre><code>
+ * significant bits encoding bytes
+ * ---------------- -------- -----
+ * <= 7 Tiny 1
+ * 8 Large 2
+ * 9-13 Small 2
+ * 14-16 Large 3
+ * 17-21 Medium 3
+ * >= 22 Large 4 or more
+ * >= 512 Huge *
+ * </code></pre>
+ * <p/>
+ * Similarly, by examining the least significant 3 bits of the first byte of an encoded value, the
+ * encoding can be determined as follows:
+ * <pre><code>
+ * first byte encoding bytes
+ * ---------- -------- -----
+ * ???????1 Tiny 1
+ * ?????010 Small 2
+ * ?????110 Medium 3
+ * ??????00 Large 2-65
+ * 11111100 Huge *
+ * </code></pre>
+ */
+public class PackedInteger
+ implements Comparable<PackedInteger>
+ {
+ // ----- constructors --------------------------------------------------------------------------
+
+ /**
+ * Construct an uninitialized PackedInteger.
+ */
+ public PackedInteger()
+ {
+ }
+
+ /**
+ * Construct a PackedInteger using a <tt>long</tt> value.
+ *
+ * @param lVal the <tt>long</tt> value for the PackedInteger
+ */
+ public PackedInteger(long lVal)
+ {
+ setLong(lVal);
+ }
+
+ /**
+ * Construct a PackedInteger using a <tt>BigInteger</tt> value.
+ *
+ * @param bigint the <tt>BigInteger</tt> value for the PackedInteger
+ */
+ public PackedInteger(BigInteger bigint)
+ {
+ setBigInteger(bigint);
+ }
+
+ /**
+ * Construct a PackedInteger by reading the packed value from a DataInput
+ * stream.
+ *
+ * @param in a DataInput stream to read from
+ *
+ * @throws IOException if an I/O exception occurs while reading the data
+ */
+ public PackedInteger(DataInput in)
+ throws IOException
+ {
+ readObject(in);
+ }
+
+
+ // ----- public methods ------------------------------------------------------------------------
+
+ /**
+ * Obtain a PackedInteger that has the specified <tt>long</tt> value. This
+ * method is useful for taking advantage of the built-in "cache" of
+ * commonly-used PackedInteger instances.
+ *
+ * @param lVal the <tt>long</tt> value for the PackedInteger
+ */
+ public static PackedInteger valueOf(long lVal)
+ {
+ if (lVal >= CACHE_MIN & lVal <= CACHE_MAX)
+ {
+ final int iCache = (int) (lVal - CACHE_MIN);
+
+ PackedInteger pint = CACHE[iCache];
+ if (pint == null)
+ {
+ pint = new PackedInteger(lVal);
+ CACHE[iCache] = pint;
+ }
+
+ return pint;
+ }
+
+ return new PackedInteger(lVal);
+ }
+
+ /**
+ * The size of the "native" 2's-complement signed integer that would be
+ * necessary to hold the value.
+ *
+ * @return a value between 1 and 32 inclusive
+ */
+ public int getSignedByteSize()
+ {
+ verifyInitialized();
+
+ int nBytes = m_fBig
+ ? calculateSignedByteCount(m_bigint)
+ : Math.max(1, (((64 - Long.numberOfLeadingZeros(Math.max(m_lValue, ~m_lValue))) & 0x3F) + 7) / 8);
+
+ assert nBytes >= 1 && nBytes <= 1024; // arbitrary limit
+ return nBytes;
+ }
+
+ /**
+ * The size of the unsigned integer that would be necessary to hold the value.
+ *
+ * @return a value between 1 and 32 inclusive
+ */
+ public int getUnsignedByteSize()
+ {
+ verifyInitialized();
+ if (m_fBig && m_bigint.signum() < 0)
+ {
+ throw new IllegalStateException("negative value");
+ }
+
+ int nBytes = m_fBig
+ ? calculateUnsignedByteCount(m_bigint)
+ : Math.max(1, (((64 - Long.numberOfLeadingZeros(m_lValue)) + 7) / 8));
+
+ assert nBytes >= 1 && nBytes <= 32; // arbitrary limit of 32 for the prototype
+ return nBytes;
+ }
+
+ /**
+ * Determine if the value of the PackedInteger is "big". The value is
+ * considered to be "big" if it cannot fit into a <tt>long</tt>.
+ *
+ * @return true if the value of the PackedInteger does not fit into a long
+ */
+ public boolean isBig()
+ {
+ verifyInitialized();
+ return m_fBig;
+ }
+
+ /**
+ * Range check the PackedInteger.
+ *
+ * @param nLo the low end of the range (inclusive)
+ * @param nHi the high end of the range (inclusive)
+ *
+ * @return true iff the PackedInteger is in the specified range
+ */
+ public boolean checkRange(long nLo, long nHi)
+ {
+ if (isBig())
+ {
+ return false;
+ }
+
+ return m_lValue >= nLo && m_lValue <= nHi;
+ }
+
+ /**
+ * @return true iff the value is less than zero
+ */
+ public boolean isNegative()
+ {
+ verifyInitialized();
+ return m_fBig
+ ? m_bigint.signum() < 0
+ : m_lValue < 0;
+ }
+
+ /**
+ * Helper to grab the value as an int, with a range check to be safe.
+ *
+ * @return the value as a 32-bit signed int
+ */
+ public int getInt()
+ {
+ long n = getLong();
+ if (n < Integer.MIN_VALUE || n > Integer.MAX_VALUE)
+ {
+ throw new IllegalStateException("too big!");
+ }
+
+ return (int) n;
+ }
+
+ /**
+ * Obtain the <tt>long</tt> value of the PackedInteger. If the PackedInteger
+ * is "big", i.e. if the {@link #isBig} method returns <tt>true</tt>, then
+ * this method will throw an IllegalStateException, because the value cannot
+ * be expressed as a <tt>long</tt> without losing data.
+ *
+ * @return the <tt>long</tt> value of this PackedInteger
+ *
+ * @throws IllegalStateException if the value of this PackedInteger does not
+ * fit into a long
+ */
+ public long getLong()
+ {
+ verifyInitialized();
+ if (m_fBig && m_lValue == 0)
+ {
+ throw new IllegalStateException("too big!");
+ }
+
+ return m_lValue;
+ }
+
+ /**
+ * Initialize the PackedInteger using a <tt>long</tt> value.
+ *
+ * @param lVal the <tt>long</tt> value for the PackedInteger
+ */
+ public void setLong(long lVal)
+ {
+ verifyUninitialized();
+ m_lValue = lVal;
+ m_fInitialized = true;
+ }
+
+ /**
+ * Obtain the BigInteger value of the PackedInteger. Whether or not the
+ * PackedInteger value is too large to be held in a <tt>long</tt>, the
+ * caller can request the BigInteger value; one will be lazily instantiated
+ * (and subsequently cached) if necessary.
+ *
+ * @return the BigInteger that represents the integer value of this
+ * PackedInteger object
+ */
+ public BigInteger getBigInteger()
+ {
+ verifyInitialized();
+ BigInteger bigint = m_bigint;
+ if (bigint == null)
+ {
+ bigint = BigInteger.valueOf(m_lValue);
+ m_bigint = bigint;
+ }
+ return bigint;
+ }
+
+ /**
+ * Initialize the PackedInteger using a BigInteger value.
+ *
+ * @param bigint the BigInteger value for the PackedInteger
+ */
+ public void setBigInteger(BigInteger bigint)
+ {
+ verifyUninitialized();
+
+ if (bigint == null)
+ {
+ throw new IllegalArgumentException("big integer value required");
+ }
+
+ // determine if the number of bytes allows the BigInteger value to be
+ // stored in a long value
+ if (!(m_fBig = calculateSignedByteCount(bigint) > 8))
+ {
+ m_lValue = bigint.longValue();
+ }
+ else
+ {
+ if (calculateUnsignedByteCount(bigint) <= 8)
+ {
+ // the unsigned value still fits the long
+ m_lValue = bigint.longValue();
+ }
+ else
+ {
+ m_lValue = 0;
+ }
+ }
+
+ m_bigint = bigint;
+ m_fInitialized = true;
+ }
+
+ /**
+ * @return the negated form of this packed integer
+ */
+ public PackedInteger negate()
+ {
+ return new PackedInteger(this.getBigInteger().negate());
+ }
+
+ /**
+ * @return the complemented form of this packed integer
+ */
+ public PackedInteger complement()
+ {
+ return new PackedInteger(this.getBigInteger().not());
+ }
+
+ /**
+ * @return the packed integer that is one less than this packed integer
+ */
+ public PackedInteger previous()
+ {
+ return sub(ONE);
+ }
+
+ /**
+ * @return the packed integer that is one more than this packed integer
+ */
+ public PackedInteger next()
+ {
+ return add(ONE);
+ }
+
+ /**
+ * Add the value of a specified PackedInteger to this PackedInteger, resulting in a new
+ * PackedInteger.
+ *
+ * @param that a second PackedInteger to add to this
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger add(PackedInteger that)
+ {
+ return new PackedInteger(this.getBigInteger().add(that.getBigInteger()));
+ }
+
+ /**
+ * Subtract the value of a specified PackedInteger from this PackedInteger, resulting in a new
+ * PackedInteger.
+ *
+ * @param that a second PackedInteger to subtract from this
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger sub(PackedInteger that)
+ {
+ return new PackedInteger(this.getBigInteger().subtract(that.getBigInteger()));
+ }
+
+ /**
+ * Multiply the value of a specified PackedInteger by this PackedInteger, resulting in a new
+ * PackedInteger.
+ *
+ * @param that a second PackedInteger to multiply this by
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger mul(PackedInteger that)
+ {
+ return new PackedInteger(this.getBigInteger().multiply(that.getBigInteger()));
+ }
+
+ /**
+ * Divide the value of this PackedInteger by the specified PackedInteger, resulting in a new
+ * PackedInteger.
+ *
+ * @param that a second PackedInteger to divide this by
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger div(PackedInteger that)
+ {
+ return new PackedInteger(this.getBigInteger().divide(that.getBigInteger()));
+ }
+
+ /**
+ * Calculate the modulo of the value of this PackedInteger and the value of a specified
+ * PackedInteger, resulting in a new PackedInteger.
+ *
+ * @param that a second PackedInteger to divide this by, resulting in a modulo
+ *
+ * @return the resulting PackedInteger (never negative - modulo not remainder!)
+ */
+ public PackedInteger mod(PackedInteger that)
+ {
+ return new PackedInteger(this.getBigInteger().mod(that.getBigInteger()));
+ }
+
+ /**
+ * Divide the value of this PackedInteger by the specified PackedInteger, resulting in a new
+ * PackedInteger quotient and a new PackedInteger remainder.
+ *
+ * @param that a second PackedInteger to divide this by
+ *
+ * @return an array of the resulting PackedInteger quotient and remainder
+ */
+ public PackedInteger[] divrem(PackedInteger that)
+ {
+ BigInteger [] aBigInt = this.getBigInteger().divideAndRemainder(that.getBigInteger());
+ PackedInteger[] aPacked = new PackedInteger[2];
+ aPacked[0] = new PackedInteger(aBigInt[0]);
+ aPacked[1] = new PackedInteger(aBigInt[1]);
+ return aPacked;
+ }
+
+ /**
+ * Calculate the bit-and of the value of this PackedInteger and the value of a specified
+ * PackedInteger, resulting in a new PackedInteger.
+ *
+ * @param that a second PackedInteger to bit-and with this PackedInteger
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger and(PackedInteger that)
+ {
+ return new PackedInteger(this.getBigInteger().and(that.getBigInteger()));
+ }
+
+ /**
+ * Calculate the bit-or of the value of this PackedInteger and the value of a specified
+ * PackedInteger, resulting in a new PackedInteger.
+ *
+ * @param that a second PackedInteger to bit-or with this PackedInteger
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger or(PackedInteger that)
+ {
+ return new PackedInteger(this.getBigInteger().or(that.getBigInteger()));
+ }
+
+ /**
+ * Calculate the bit-xor of the value of this PackedInteger and the value of a specified
+ * PackedInteger, resulting in a new PackedInteger.
+ *
+ * @param that a second PackedInteger to bit-xor with this PackedInteger
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger xor(PackedInteger that)
+ {
+ return new PackedInteger(this.getBigInteger().xor(that.getBigInteger()));
+ }
+
+ /**
+ * Shift left the bits in the value of this PackedInteger by the value of a specified
+ * PackedInteger, resulting in a new PackedInteger.
+ *
+ * @param that a second PackedInteger specifying the shift amount
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger shl(PackedInteger that)
+ {
+ return shl(that.getInt());
+ }
+
+ /**
+ * Shift left the bits in the value of this PackedInteger by the specified count,
+ * resulting in a new PackedInteger.
+ *
+ * @param count the shift count
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger shl(int count)
+ {
+ return new PackedInteger(this.getBigInteger().shiftLeft(count));
+ }
+
+ /**
+ * Logical shift right the bits in the value of this PackedInteger by the value of a specified
+ * PackedInteger, resulting in a new PackedInteger.
+ *
+ * @param that a second PackedInteger specifying the shift amount
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger shr(PackedInteger that)
+ {
+ return shr(that.getInt());
+ }
+
+ /**
+ * Logical shift right the bits in the value of this PackedInteger by the specified count,
+ * resulting in a new PackedInteger.
+ *
+ * @param count the shift count
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger shr(int count)
+ {
+ return count <= 0
+ ? this
+ : new PackedInteger(this.getBigInteger().shiftRight(count));
+ }
+
+ /**
+ * Arithmetic (aka "unsigned") shift right the bits in the value of this PackedInteger by the
+ * value of a specified PackedInteger, resulting in a new PackedInteger.
+ *
+ * @param that a second PackedInteger specifying the shift amount
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger ushr(PackedInteger that)
+ {
+ return shr(that.getInt());
+ }
+
+ /**
+ * Arithmetic (aka "unsigned") shift right the bits in the value of this PackedInteger by the
+ * specified count, resulting in a new PackedInteger.
+ *
+ * @param count the shift count
+ *
+ * @return the resulting PackedInteger
+ */
+ public PackedInteger ushr(int count)
+ {
+ return shr(count);
+ }
+
+ /**
+ * Compare the value of a specified PackedInteger with the value of this PackedInteger.
+ *
+ * @param that a second PackedInteger to compare to
+ *
+ * @return -1, 0, or 1 if this is less than, equal to, or greater than that
+ */
+ public int cmp(PackedInteger that)
+ {
+ return this.getBigInteger().compareTo(that.getBigInteger());
+ }
+
+ /**
+ * Format the PackedInteger as a String of the specified radix, including a radix prefix for
+ * any non-decimal radix.
+ *
+ * @param radix 2, 8, 10, or 16
+ *
+ * @return the String value of this PackedInteger in the format of the specified radix
+ */
+ public String toString(int radix)
+ {
+ if (radix == 10)
+ {
+ return toString();
+ }
+
+ StringBuilder sb = new StringBuilder();
+ if (isNegative())
+ {
+ sb.append('-');
+ }
+
+ switch (radix)
+ {
+ case 2:
+ sb.append("0b");
+ break;
+
+ case 8:
+ sb.append("0o");
+ break;
+
+ case 16:
+ sb.append("0x");
+ break;
+
+ default:
+ throw new IllegalArgumentException("radix=" + radix);
+ }
+
+ sb.append(isBig()
+ ? getBigInteger().abs().toString(radix)
+ : Long.toUnsignedString(Math.abs(getLong()), radix));
+
+ return sb.toString();
+ }
+
+ /**
+ * Read a PackedInteger from a stream.
+ *
+ * @param in a DataInput stream to read from
+ *
+ * @throws IOException if an I/O exception occurs while reading the data
+ */
+ public void readObject(DataInput in)
+ throws IOException
+ {
+ verifyUninitialized();
+
+ final int b = in.readByte();
+ // check for large or huge format with more than 8 trailing bytes (needs BigInteger)
+ if ((b & 0b11) == 0b00 && (b & 0b111000_00) != 0)
+ {
+ final int cBytes;
+ if ((b & 0xFF) == 0b111111_00)
+ {
+ // huge format
+ long len = readLong(in);
+ if (len > 1024)
+ {
+ throw new IOException("integer size of " + len + " bytes; maximum is 1024");
+ }
+ cBytes = (int) len;
+ }
+ else
+ {
+ // large format
+ cBytes = 1 + ((b & 0xFC) >>> 2);
+ }
+
+ final byte[] ab = new byte[cBytes];
+ in.readFully(ab);
+ setBigInteger(new BigInteger(ab));
+ }
+ else
+ {
+ // tiny, small, medium, and large (up to 8 trailing bytes) format values fit into
+ // a Java long
+ setLong(readLong(in, b));
+ }
+ }
+
+ /**
+ * Write a PackedInteger to a stream.
+ *
+ * @param out a DataOutput stream to write to
+ *
+ * @throws IOException if an I/O exception occurs while writing the data
+ */
+ public void writeObject(DataOutput out)
+ throws IOException
+ {
+ verifyInitialized();
+
+ if (isBig())
+ {
+ // the value is supposed to be "big", so figure out how many bytes
+ // (minimum) it needs to hold its significant bits
+ byte[] ab = m_bigint.toByteArray();
+ int cb = ab.length;
+ int of = 0;
+
+ // truncate any redundant bytes
+ boolean fNeg = (ab[0] & 0x80) != 0;
+ int bSkip = fNeg ? 0xFF : 0x00;
+ while (of < cb-1 && ab[of] == bSkip && (ab[of+1] & 0x80) == (bSkip & 0x80))
+ {
+ ++of;
+ }
+ cb -= of;
+ assert cb > 8 && cb <= 1024;
+
+ if (cb < 64)
+ {
+ // large format: length encoded in 6 MSBs of first byte, then the bytes of the int
+ out.writeByte((cb-1) << 2);
+ }
+ else
+ {
+ // huge format: first byte is 0, then the length as a packed int, then the bytes
+ out.writeByte(0b111111_00);
+ writeLong(out, cb);
+ }
+ out.write(ab, of, cb);
+ }
+ else
+ {
+ writeLong(out, m_lValue);
+ }
+ }
+
+ /**
+ * Verify that the PackedInteger is still uninitialized (has no integer
+ * value).
+ *
+ * @throws IllegalStateException if the value of the PackedInteger has
+ * already been set
+ */
+ public void verifyUninitialized()
+ {
+ if (m_fInitialized)
+ {
+ throw new IllegalStateException("already initialized");
+ }
+ }
+
+ /**
+ * Verify that the PackedInteger has been successfully initialized to an
+ * integer value.
+ *
+ * @throws IllegalStateException if the value of the PackedInteger has
+ * <b>not</b> yet been set
+ */
+ public void verifyInitialized()
+ {
+ if (!m_fInitialized)
+ {
+ throw new IllegalStateException("not yet initialized");
+ }
+ }
+
+
+ // ----- Object methods ------------------------------------------------------------------------
+
+ @Override
+ public int hashCode()
+ {
+ return isBig() ? m_bigint.hashCode() : Long.hashCode(m_lValue);
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (!(obj instanceof PackedInteger that))
+ {
+ return false;
+ }
+
+ return this.isBig()
+ ? that.isBig() && this.getBigInteger().equals(that.getBigInteger())
+ : !that.isBig() && this.getLong() == that.getLong();
+ }
+
+ @Override
+ public String toString()
+ {
+ return isBig() ? m_bigint.toString() : Long.toString(m_lValue);
+ }
+
+
+ // ----- Comparable methods --------------------------------------------------------------------
+
+ @Override
+ public int compareTo(PackedInteger that)
+ {
+ if (this.isBig() || that.isBig())
+ {
+ return this.getBigInteger().compareTo(that.getBigInteger());
+ }
+
+ long lThis = this.m_lValue;
+ long lThat = that.m_lValue;
+ return Long.compare(lThis, lThat);
+ }
+
+
+ // ----- public helpers ------------------------------------------------------------------------
+
+ /**
+ * Write a signed 64-bit integer to a stream using variable-length
+ * encoding.
+ *
+ * @param out the <tt>DataOutput</tt> stream to write to
+ * @param l the <tt>long</tt> value to write
+ *
+ * @throws IOException if an I/O exception occurs
+ */
+ public static void writeLong(DataOutput out, long l)
+ throws IOException
+ {
+ // test for Tiny
+ if (l <= 63 && l >= -64)
+ {
+ out.writeByte(((int) l) << 1 | 0x01);
+ return;
+ }
+
+ final int cBits = 65 - Long.numberOfLeadingZeros(Math.max(l, ~l));
+
+ // test for Small and Medium
+ int i = (int) l;
+ if (((1L << cBits) & 0x3E3E00L) != 0) // test against bits 9-13 and 17-21
+ {
+ if (cBits <= 13)
+ {
+ out.writeShort(0b010_00000000 // 0x2 marker at 0..2 in byte #1
+ | (i & 0x1F00) << 3 // bits 8..12 at 3..7 in byte #1
+ | (i & 0x00FF)); // bits 0..7 at 0..7 in byte #2
+ }
+ else
+ {
+ out.writeByte(0b110 // 0x6 marker at 0..2 in byte #1
+ | (i & 0x1F0000) >>> 13); // bits 16..20 at 3..7 in byte #1
+ out.writeShort(i); // bits 8..15 at 0..7 in byte #2
+ } // bits 0..7 at 0..7 in byte #3
+ return;
+ }
+
+ int cBytes = (cBits + 7) >>> 3;
+ out.writeByte((cBytes - 1) << 2);
+ switch (cBytes)
+ {
+ case 1:
+ out.writeByte(i);
+ break;
+ case 2:
+ out.writeShort(i);
+ break;
+ case 3:
+ out.writeByte(i >>> 16);
+ out.writeShort(i);
+ break;
+ case 4:
+ out.writeInt(i);
+ break;
+ case 5:
+ out.writeByte((int) (l >>> 32));
+ out.writeInt(i);
+ break;
+ case 6:
+ out.writeShort((int) (l >>> 32));
+ out.writeInt(i);
+ break;
+ case 7:
+ out.writeByte((int) (l >>> 48));
+ out.writeShort((int) (l >>> 32));
+ out.writeInt(i);
+ break;
+ case 8:
+ out.writeLong(l);
+ break;
+ default:
+ throw new IllegalStateException("n=" + l);
+ }
+ }
+
+ /**
+ * Read a variable-length encoded integer value from a stream.
+ *
+ * @param in a <tt>DataInput</tt> stream to read from
+ *
+ * @return a <tt>long</tt> value
+ *
+ * @throws IOException if an I/O exception occurs
+ * @throws NumberFormatException if the integer does not fit into
+ * a <tt>long</tt> value
+ */
+ public static long readLong(DataInput in)
+ throws IOException
+ {
+ return readLong(in, in.readByte());
+ }
+
+ /**
+ * Determine the number of bytes of the packed integer at the specified offset in the provided
+ * byte array.
+ *
+ * @param ab the byte array containing a packed integer
+ * @param of the byte offset at which the packed integer is located
+ *
+ * @return the number of bytes used to encode the packed integer
+ */
+ public static int packedLength(byte[] ab, int of)
+ {
+ int b = ab[of];
+ if ((b & 0x01) != 0)
+ {
+ // Tiny format
+ return 1;
+ }
+
+ if ((b & 0x02) != 0)
+ {
+ // Small or Medium format
+ return ((b & 0x04) == 0) ? 2 : 3;
+ }
+
+ if ((b & 0xFF) == 0b111111_00)
+ {
+ // Huge format
+ long lVals = unpackInt(ab, of+1);
+ return 1 + (int) (lVals >>> 32) + (int) lVals;
+ }
+
+ // Large format
+ return 2 + ((b & 0xFC) >>> 2);
+ }
+
+ /**
+ * Determine the number of bytes that the specified value would use if it were packed.
+ *
+ * @param n the long integer value to estimate a packed length for
+ *
+ * @return the smallest number of bytes necessary to encode the packed integer
+ */
+ public static int packedLength(long n)
+ {
+ // test for Tiny
+ if (n <= 63 && n >= -64)
+ {
+ return 1;
+ }
+
+ // test for Small and Medium
+ final int cBits = 65 - Long.numberOfLeadingZeros(Math.max(n, ~n));
+ if (((1L << cBits) & 0x3E3E00L) != 0) // test against bits 9-13 and 17-21
+ {
+ return cBits <= 13 ? 2 : 3;
+ }
+
+ // Large format
+ int cBytes = (cBits + 7) >>> 3;
+ return 1 + cBytes;
+ }
+
+ /**
+ * Extract an integer from a byte array, and report back both the integer value and its size in
+ * terms of the number of bytes.
+ *
+ * @param ab the byte array to unpack an integer from
+ * @param of the byte offset at which the integer is located
+ *
+ * @return the int value encoded as bits 0..31, and the number of bytes used by the value
+ * encoded as bits 32..63
+ */
+ public static long unpackInt(byte[] ab, int of)
+ {
+ int n;
+ int cb;
+
+ int b = ab[of];
+ if ((b & 0x01) != 0)
+ {
+ // Tiny format: the first bit of the first byte is used to indicate a single byte
+ // format, in which the entire value is contained in the 7 MSBs
+ n = b >> 1;
+ cb = 1;
+ }
+ else if ((b & 0x02) != 0)
+ {
+ // the third bit is used to indicate 0: 1 trailing byte, or 1: 2 trailing bytes
+ if ((b & 0x04) == 0)
+ {
+ // Small format: bits 3..7 of the first byte are bits 8..12 of the result, and the
+ // next byte provides bits 0..7 of the result (note: and then also sign-extend)
+ n = (b & 0xFFFFFFF8) << 5 | ab[of+1] & 0xFF;
+ cb = 2;
+ }
+ else
+ {
+ // Medium format: bits 3..7 of the first byte are bits 16..20 of the result, and the
+ // next byte provides bits 8..15 of the result, and the next byte provides bits 0..7
+ // of the result (note: and then also sign-extend)
+ n = (b & 0xFFFFFFF8) << 13 | (ab[of+1] & 0xFF) << 8 | ab[of+2] & 0xFF;
+ cb = 3;
+ }
+ }
+ else
+ {
+ // Large format: the first two bits of the first byte are 0, so bits 2..7 of the
+ // first byte are the trailing number of bytes minus 1
+ // Huge format: the first byte is 0b111111_00, so it is followed by a packed integer
+ // that specifies the length of this packed integer
+ final int cBytes;
+ if ((b & 0xFF) == 0b111111_00)
+ {
+ // Huge format
+ long lVals = unpackInt(ab, of+1);
+ int cbcb = (int) (lVals >>> 32);
+ cBytes = (int) lVals;
+ cb = 1 + cbcb + cBytes;
+ of += 1 + cbcb;
+ }
+ else
+ {
+ // Large format
+ cBytes = 1 + ((b & 0xFC) >>> 2);
+ cb = 1 + cBytes;
+ ++of;
+ }
+
+ switch (cBytes)
+ {
+ case 1:
+ n = ab[of];
+ break;
+ case 2:
+ n = ab[of] << 8 | ab[of+1] & 0xFF;
+ break;
+ case 3:
+ n = ab[of] << 16 | (ab[of+1] & 0xFF) << 8 | ab[of+2] & 0xFF;
+ break;
+ case 4:
+ n = ab[of] << 24 | (ab[of+1] & 0xFF) << 16 | (ab[of+2] & 0xFF) << 8 | ab[of+3] & 0xFF;
+ break;
+ default:
+ throw new IllegalStateException("# trailing bytes=" + cBytes);
+ }
+ }
+
+ return ((long) cb) << 32 | n & 0xFFFFFFFFL;
+ }
+
+
+ // ----- internal ------------------------------------------------------------------------------
+
+ private static long readLong(DataInput in, int b)
+ throws IOException
+ {
+ if ((b & 0x01) != 0)
+ {
+ // Tiny format: the first bit of the first byte is used to indicate a single byte
+ // format, in which the entire value is contained in the 7 MSBs
+ return b >> 1;
+ }
+
+ if ((b & 0x02) != 0)
+ {
+ // the third bit is used to indicate 0: 1 trailing byte, or 1: 2 trailing bytes
+ if ((b & 0x04) == 0)
+ {
+ // Small format: bits 3..7 of the first byte are bits 8..12 of the result, and the
+ // next byte provides bits 0..7 of the result (note: and then also sign-extend)
+ return (long) (b & 0xFFFFFFF8) << 5 | in.readUnsignedByte();
+ }
+ else
+ {
+ // Medium format: bits 3..7 of the first byte are bits 16..20 of the result, and the
+ // next byte provides bits 8..15 of the result, and the next byte provides bits 0..7
+ // of the result (note: and then also sign-extend)
+ return (long) (b & 0xFFFFFFF8) << 13 | in.readUnsignedShort();
+ }
+ }
+
+ // Large format: the first two bits of the first byte are 0, so bits 2..7 of the
+ // first byte are the trailing number of bytes minus 1
+ // Huge format: the first byte is 0, so it is followed by a packed integer that
+ // specifies the length of this packed integer
+ int cBytes = (b & 0xFF) == 0b111111_00 ? (int) readLong(in) : 1 + ((b & 0xFC) >>> 2);
+ switch (cBytes)
+ {
+ case 1:
+ return in.readByte();
+ case 2:
+ return in.readShort();
+ case 3:
+ return in.readByte() << 16 | in.readUnsignedShort();
+ case 4:
+ return in.readInt();
+ case 5:
+ return ((long) in.readByte()) << 32 | readUnsignedInt(in);
+ case 6:
+ return ((long) in.readShort()) << 32 | readUnsignedInt(in);
+ case 7:
+ return ((long) in.readByte()) << 48
+ | ((long) in.readUnsignedShort()) << 32
+ | readUnsignedInt(in);
+ case 8:
+ return in.readLong();
+ default:
+ throw new IllegalStateException("# trailing bytes=" + cBytes);
+ }
+ }
+
+ private static long readUnsignedInt(DataInput in)
+ throws IOException
+ {
+ return in.readInt() & 0xFFFFFFFFL;
+ }
+
+ /**
+ * Determine how many bytes is necessary to hold the specified BigInteger.
+ *
+ * @return the minimum number of bytes to hold the integer value in
+ * 2s-complement form
+ *
+ * @throws IllegalArgumentException if the BigInteger is out of range
+ */
+ private static int calculateSignedByteCount(BigInteger bigint)
+ {
+ return bigint.bitLength() / 8 + 1;
+ }
+
+ /**
+ * Determine how many bytes is necessary to hold the specified BigInteger.
+ *
+ * @return the minimum number of bytes to hold the integer value in
+ * 2s-complement form
+ *
+ * @throws IllegalArgumentException if the BigInteger is out of range
+ */
+ private static int calculateUnsignedByteCount(BigInteger bigint)
+ {
+ return (bigint.bitLength() + 7) / 8;
+ }
+
+
+ // ----- data members --------------------------------------------------------------------------
+
+ /**
+ * Set to true once the value has been set.
+ */
+ private boolean m_fInitialized;
+
+ /**
+ * Set to true if the value is too large to fit into a <tt>long</tt> (for signed values).
+ */
+ private boolean m_fBig;
+
+ /**
+ * The <tt>long</tt> value if the value fits into a <tt>long</tt>. Note, the value could be "big",
+ * as a signed one but still fit the long as unsigned.
+ */
+ private long m_lValue;
+
+ /**
+ * The <tt>BigInteger</tt> value, which is non-null in several different
+ * cases, including if the PackedInteger was constructed with a BigInteger
+ * value, if the value was set to a BigInteger, or if the BigInteger was
+ * lazily instantiated by a call to {@link #getBigInteger}.
+ */
+ private BigInteger m_bigint;
+
+ /**
+ * Cache of PackedInteger instances for small integers.
+ */
+ private static final PackedInteger[] CACHE = new PackedInteger[0x1000];
+
+
+ // ----- constants -----------------------------------------------------------------------------
+
+ /**
+ * Smallest integer value to cache. One quarter of the cache size is
+ * reserved for small negative integer values.
+ */
+ private static final long CACHE_MIN = -(CACHE.length >> 2);
+
+ /**
+ * Largest integer value to cache. Three-quarters of the cache size is
+ * reserved for small positive integer values.
+ */
+ private static final long CACHE_MAX = CACHE_MIN + CACHE.length - 1;
+
+ /**
+ * The PackedInteger for the value <tt>0</tt>. Also used as the smallest 1-, 2-, 4-,
+ * 8-, 16-, and 32-byte <b>un</b>signed integer value.
+ */
+ public static final PackedInteger ZERO = valueOf(0L);
+ /**
+ * The PackedInteger for the value <tt>1</tt>.
+ */
+ public static final PackedInteger ONE = valueOf(1L);
+ /**
+ * The PackedInteger for the value <tt>-1</tt>.
+ */
+ public static final PackedInteger NEG_ONE = valueOf(-1L);
+
+ /**
+ * Smallest 1-byte (8-bit) signed integer value.
+ */
+ public static final PackedInteger SINT1_MIN = valueOf(-0x80L);
+ /**
+ * Smallest 2-byte (16-bit) signed integer value.
+ */
+ public static final PackedInteger SINT2_MIN = valueOf(-0x8000L);
+ /**
+ * Smallest 4-byte (32-bit) signed integer value.
+ */
+ public static final PackedInteger SINT4_MIN = valueOf(-0x80000000L);
+ /**
+ * Smallest 8-byte (64-bit) signed integer value.
+ */
+ public static final PackedInteger SINT8_MIN = valueOf(-0x8000000000000000L);
+ /**
+ * Smallest 16-byte (128-bit) signed integer value.
+ */
+ public static final PackedInteger SINT16_MIN = new PackedInteger(new BigInteger("-8" + "0".repeat(31), 16));
+ /**
+ * Smallest N-byte signed integer value (arbitrary 1KB limit).
+ */
+ public static final PackedInteger SINTN_MIN = new PackedInteger(new BigInteger("-8" + "0".repeat(2047), 16));
+ /**
+ * Largest 1-byte (8-bit) signed integer value.
+ */
+ public static final PackedInteger SINT1_MAX = valueOf(0x7F);
+ /**
+ * Largest 2-byte (16-bit) signed integer value.
+ */
+ public static final PackedInteger SINT2_MAX = valueOf(0x7FFF);
+ /**
+ * Largest 4-byte (32-bit) signed integer value.
+ */
+ public static final PackedInteger SINT4_MAX = valueOf(0x7FFF_FFFF);
+ /**
+ * Largest 8-byte (64-bit) signed integer value.
+ */
+ public static final PackedInteger SINT8_MAX = valueOf(0x7FFF_FFFF_FFFF_FFFFL);
+ /**
+ * Largest 16-byte (128-bit) signed integer value.
+ */
+ public static final PackedInteger SINT16_MAX = new PackedInteger(new BigInteger("7" + "F".repeat(31), 16));
+ /**
+ * Largest N-byte signed integer value (arbitrary 1KB limit).
+ */
+ public static final PackedInteger SINTN_MAX = new PackedInteger(new BigInteger("7" + "F".repeat(2047), 16));
+ /**
+ * Largest 1-byte (8-bit) unsigned integer value.
+ */
+ public static final PackedInteger UINT1_MAX = valueOf(0xFF);
+ /**
+ * Largest 2-byte (16-bit) unsigned integer value.
+ */
+ public static final PackedInteger UINT2_MAX = valueOf(0xFFFF);
+ /**
+ * Largest 4-byte (32-bit) unsigned integer value.
+ */
+ public static final PackedInteger UINT4_MAX = valueOf(0xFFFF_FFFFL);
+ /**
+ * Largest 8-byte (64-bit) unsigned integer value.
+ */
+ public static final PackedInteger UINT8_MAX = new PackedInteger(new BigInteger("F".repeat(16), 16));
+ /**
+ * Largest 16-byte (128-bit) unsigned integer value.
+ */
+ public static final PackedInteger UINT16_MAX = new PackedInteger(new BigInteger("F".repeat(32), 16));
+ /**
+ * Largest N-byte unsigned integer value (arbitrary 1KB limit).
+ */
+ public static final PackedInteger UINTN_MAX = SINTN_MAX;
+
+ /**
+ * Decimal "Kilo".
+ */
+ public static final PackedInteger KB = valueOf(1000);
+ /**
+ * Binary "Kilo".
+ */
+ public static final PackedInteger KI = valueOf(1024);
+ /**
+ * Decimal "Mega".
+ */
+ public static final PackedInteger MB = valueOf(1000 * 1000);
+ /**
+ * Binary "Mega".
+ */
+ public static final PackedInteger MI = valueOf(1024 * 1024);
+ /**
+ * Decimal "Giga".
+ */
+ public static final PackedInteger GB = valueOf(1000 * 1000 * 1000);
+ /**
+ * Binary "Giga".
+ */
+ public static final PackedInteger GI = valueOf(1024 * 1024 * 1024);
+ /**
+ * Decimal "Tera".
+ */
+ public static final PackedInteger TB = valueOf(1000L * 1000 * 1000 * 1000);
+ /**
+ * Binary "Tera".
+ */
+ public static final PackedInteger TI = valueOf(1024L * 1024 * 1024 * 1024);
+ /**
+ * Decimal "Peta".
+ */
+ public static final PackedInteger PB = valueOf(1000L * 1000 * 1000 * 1000 * 1000);
+ /**
+ * Binary "Peta".
+ */
+ public static final PackedInteger PI = valueOf(1024L * 1024 * 1024 * 1024 * 1024);
+ /**
+ * Decimal "Exa".
+ */
+ public static final PackedInteger EB = valueOf(1000L * 1000 * 1000 * 1000 * 1000 * 1000);
+ /**
+ * Binary "Exa".
+ */
+ public static final PackedInteger EI = valueOf(1024L * 1024 * 1024 * 1024 * 1024 * 1024);
+ /**
+ * Decimal "Zetta".
+ */
+ public static final PackedInteger ZB = EB.mul(KB);
+ /**
+ * Binary "Zetta".
+ */
+ public static final PackedInteger ZI = EI.mul(KI);
+ /**
+ * Decimal "Yotta".
+ */
+ public static final PackedInteger YB = ZB.mul(KB);
+ /**
+ * Binary "Yotta".
+ */
+ public static final PackedInteger YI = ZI.mul(KI);
+
+ /**
+ * The decimal (1000x) factors.
+ */
+ public static final PackedInteger[] xB_FACTORS = {KB, MB, GB, TB, PB, EB, ZB, YB, };
+ /**
+ * The binary (1024x) factors.
+ */
+ public static final PackedInteger[] xI_FACTORS = {KI, MI, GI, TI, PI, EI, ZI, YI, };
+ } \ No newline at end of file
diff --git a/common/src/main/java/com/seaofnodes/util/SB.java b/common/src/main/java/com/seaofnodes/util/SB.java
new file mode 100644
index 00000000..4481ce71
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/util/SB.java
@@ -0,0 +1,80 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * The Initial Developer of the Original Software is Cliff Click.
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package com.seaofnodes.util;
+
+
+/** Tight/tiny StringBuilder wrapper.
+ * Short short names on purpose; so they don't obscure the printing.
+ * Can't believe this wasn't done long, long ago. */
+public final class SB {
+ private final StringBuilder _sb;
+ private int _indent = 0;
+ public SB( ) { _sb = new StringBuilder( ); }
+ public SB(String s) { _sb = new StringBuilder(s); }
+ public SB p( String s ) { _sb.append(s); return this; }
+ //public SB p( Type t ) { _sb.append(t.toString()); return this; }
+ public SB p( float s ) {
+ if( Float.isNaN(s) )
+ _sb.append( "Float.NaN");
+ else if( Float.isInfinite(s) ) {
+ _sb.append(s > 0 ? "Float.POSITIVE_INFINITY" : "Float.NEGATIVE_INFINITY");
+ } else _sb.append(s);
+ return this;
+ }
+ public SB p( double s ) {
+ if( Double.isNaN(s) )
+ _sb.append("Double.NaN");
+ else if( Double.isInfinite(s) ) {
+ _sb.append(s > 0 ? "Double.POSITIVE_INFINITY" : "Double.NEGATIVE_INFINITY");
+ } else _sb.append(s);
+ return this;
+ }
+ public SB p( char s ) { _sb.append(s); return this; }
+ public SB p( int s ) { _sb.append(s); return this; }
+ public SB p( long s ) { _sb.append(s); return this; }
+ public SB p( boolean s) { _sb.append(s); return this; }
+ // Not spelled "p" on purpose: too easy to accidentally say "p(1.0)" and
+ // suddenly call the autoboxed version.
+ public SB pobj( Object s ) { _sb.append(s.toString()); return this; }
+ public SB i( int d ) { for( int i=0; i<d+_indent; i++ ) p(" "); return this; }
+ public SB i( ) { return i(0); }
+ public SB ip(String s) { return i().p(s); }
+ public SB s() { _sb.append(' '); return this; }
+
+ // Increase indentation
+ public SB ii( int i) { _indent += i; return this; }
+ // Decrease indentation
+ public SB di( int i) { _indent -= i; return this; }
+
+ public SB nl( ) { return p(System.lineSeparator()); }
+
+ // Delete last char. Useful when doing string-joins and JSON printing and an
+ // extra seperater char needs to be removed:
+ //
+ // sb.p('[');
+ // for( Foo foo : foos )
+ // sb.p(foo).p(',');
+ // sb.unchar().p(']'); // remove extra trailing comma
+ //
+ public SB unchar() { return unchar(1); }
+ public SB unchar(int x) { _sb.setLength(_sb.length()-x); return this; }
+
+ public SB clear() { _sb.setLength(0); return this; }
+ public int len() { return _sb.length(); }
+ @Override public String toString() { return _sb.toString(); }
+}
diff --git a/common/src/main/java/com/seaofnodes/util/Util.java b/common/src/main/java/com/seaofnodes/util/Util.java
new file mode 100644
index 00000000..e559a799
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/util/Util.java
@@ -0,0 +1,151 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * The Initial Developer of the Original Software is Cliff Click.
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package com.seaofnodes.util;
+
+import java.util.Arrays;
+
+public class Util {
+ // Fast linear scan for a hit, returns index or -1
+ public static int find( int[] es, int e ) {
+ for( int i=0; i<es.length; i++ ) if( es[i]==e ) return i;
+ return -1;
+ }
+ // Fast linear scan for a hit, returns index or -1
+ public static <E> int find( E[] es, E e ) {
+ for( int i=0; i<es.length; i++ ) if( es[i]==e ) return i;
+ return -1;
+ }
+
+ // String-equals, with expected interned strings
+ public static boolean eq( String s0, String s1 ) {
+ if( s0==s1 ) return true;
+ if( s0==null || s1==null ) return false;
+ assert !s0.equals(s1) : "Not interned: "+s0;
+ return false;
+ }
+
+
+ // Copied from http://burtleburtle.net/bob/c/lookup3.c
+ // Call add_hash as many times as you like, then get_hash at the end.
+ // Uses global statics, does not nest.
+ public static long rot(long x, int k) { return (x<<k) | (x>>>(64-k)); }
+ private static long a,b,c;
+ private static int x;
+ static public void add_hash( long h ) {
+ switch( x ) {
+ case 0: a+=h; x++; return;
+ case 1: b+=h; x++; return;
+ case 2: c+=h;
+ a -= c; a ^= rot(c, 4); c += b;
+ b -= a; b ^= rot(a, 6); a += c;
+ c -= b; c ^= rot(b, 8); b += a;
+ a -= c; a ^= rot(c,16); c += b;
+ b -= a; b ^= rot(a,19); a += c;
+ c -= b; c ^= rot(b, 4); b += a;
+ x=0;
+ }
+ }
+ // Return the resulting hash, which is never 0
+ static public long get_hash() {
+ if( x!=0 ) {
+ c ^= b; c -= rot(b,14);
+ a ^= c; a -= rot(c,11);
+ b ^= a; b -= rot(a,25);
+ c ^= b; c -= rot(b,16);
+ a ^= c; a -= rot(c, 4);
+ b ^= a; b -= rot(a,14);
+ c ^= b; c -= rot(b,24);
+ }
+ long hash=c;
+ if( hash==0 ) hash=b;
+ if( hash==0 ) hash=a;
+ if( hash==0 ) hash=0xcafebabe;
+ a=b=c=x=0;
+ return hash;
+ }
+ // Single-use hash spreader
+ static public long mix_hash(long h0) {
+ add_hash(h0);
+ return get_hash();
+ }
+ static public long mix_hash( long h0, long h1 ) {
+ add_hash(h0);
+ add_hash(h1);
+ return get_hash();
+ }
+ static public long mix_hash( long h0, long h1, long h2 ) {
+ add_hash(h0);
+ add_hash(h1);
+ add_hash(h2);
+ return get_hash();
+ }
+ static public long mix_hash( long h0, long h1, long h2, long h3 ) {
+ add_hash(h0);
+ add_hash(h1);
+ add_hash(h2);
+ add_hash(h3);
+ return get_hash();
+ }
+ static public long mix_hash( long h0, long h1, long h2, long h3, long h4 ) {
+ add_hash(h0);
+ add_hash(h1);
+ add_hash(h2);
+ add_hash(h3);
+ add_hash(h4);
+ return get_hash();
+ }
+
+ static public int gcd(int x, int y) {
+ if( x==0 || y== 0 ) return 0;
+ int a = Math.max(x,y), r;
+ int b = Math.min(x,y);
+ while((r=(a % b)) != 0) { a = b; b = r; }
+ return b;
+ }
+
+ static public boolean isUpperCase( String s ) {
+ for( int i=0; i<s.length() ; i++ )
+ if( !Character.isUpperCase(s.charAt(i)) )
+ return false;
+ return true;
+ }
+
+ // Return a merged sorted array from two sorted arrays, removing dups.
+ // If the merge is the same as either array, return that array.
+ // If the merge is the same as BOTH arrays, return the array with the smaller
+ // hashCode (goal: canonicalize identical arrays, so the fast == check works
+ // more often, and we do not make lots of equals-not-== arrays).
+ static public short[] merge_sort( short[] es0, short[] es1 ) {
+ if( es0==es1 ) return es0;
+ if( Arrays.equals(es0,es1) )
+ throw new UnsupportedOperationException("Not implemented");
+ short[] xs = new short[es0.length+es1.length];
+ int i=0, j=0, k=0;
+ while( i<es0.length || j<es1.length ) {
+ short s0 = i<es0.length ? es0[i] : Short.MAX_VALUE, s1 = j<es1.length ? es1[j] : Short.MAX_VALUE, s2=s0;
+ if( s0<=s1 ) { i++; s2=s0; }
+ if( s1<=s0 ) { j++; s2=s1; }
+ xs[k++] = s2;
+ }
+ assert k>i && k>j; // Arrays were not equals, so at least one element from each copied to other
+ if( k < xs.length )
+ throw new UnsupportedOperationException("Not implemented"); // Compact
+ return xs;
+ }
+
+}
diff --git a/common/src/main/java/com/seaofnodes/util/VBitSet.java b/common/src/main/java/com/seaofnodes/util/VBitSet.java
new file mode 100644
index 00000000..5d00a9e4
--- /dev/null
+++ b/common/src/main/java/com/seaofnodes/util/VBitSet.java
@@ -0,0 +1,27 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * The Initial Developer of the Original Software is Cliff Click.
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package com.seaofnodes.util;
+
+import java.util.BitSet;
+
+public class VBitSet extends BitSet {
+ // Cannot override 'set' to return a value... :-P
+ public boolean tset(int idx) { boolean b = get(idx); set(idx); return b; }
+ public boolean test(int idx) { return get(idx); }
+ public VBitSet clr() { clear(); return this; }
+}
diff --git a/common/src/test/java/com/seaofnodes/simple/lexer/TestLexer.java b/common/src/test/java/com/seaofnodes/simple/lexer/TestLexer.java
new file mode 100644
index 00000000..ef29d963
--- /dev/null
+++ b/common/src/test/java/com/seaofnodes/simple/lexer/TestLexer.java
@@ -0,0 +1,37 @@
+/**
+ * DO NOT REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Contributor(s):
+ *
+ * This file is part of the Sea Of Nodes Simple Programming language
+ * implementation. See https://github.com/SeaOfNodes/Simple
+ *
+ * The contents of this file are subject to the terms of the
+ * Apache License Version 2 (the "APL"). You may not use this
+ * file except in compliance with the License. A copy of the
+ * APL may be obtained from:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+
+package com.seaofnodes.simple.lexer;
+
+import com.seaofnodes.simple.common.ErrorListener;
+import com.seaofnodes.simple.lexer.ecstasy.Lexer;
+import com.seaofnodes.simple.lexer.ecstasy.Source;
+import com.seaofnodes.simple.lexer.ecstasy.Token;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestLexer {
+
+ @Test
+ public void testLexLineComment()
+ {
+ final String source = "//a comment\nx";
+ Lexer lexer = new Lexer(new Source(source), new ErrorListener());
+ Token token = lexer.next();
+ Assert.assertEquals(token.getId(), Token.Id.EOL_COMMENT);
+ token = lexer.next();
+ Assert.assertEquals("x", token.getValueText());
+ }
+}