diff --git a/luna/build.gradle.kts b/luna/build.gradle.kts
new file mode 100644
index 00000000..c19dd01f
--- /dev/null
+++ b/luna/build.gradle.kts
@@ -0,0 +1,23 @@
+plugins {
+ id("java")
+}
+
+group = "org.classdump.luna"
+version = "0.4.2"
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ testImplementation(platform("org.junit:junit-bom:5.9.1"))
+ testImplementation("org.junit.jupiter:junit-jupiter")
+
+ implementation("org.ow2.asm:asm:9.2")
+ implementation("org.ow2.asm:asm-tree:9.2")
+ implementation("org.ow2.asm:asm-util:9.2")
+}
+
+tasks.test {
+ useJUnitPlatform()
+}
diff --git a/luna/src/main/java/org/classdump/luna/Arithmetic.java b/luna/src/main/java/org/classdump/luna/Arithmetic.java
new file mode 100644
index 00000000..e7bc8d7a
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/Arithmetic.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+/**
+ * A representation of one of the two arithmetic modes (integer or float).
+ *
+ *
This class serves as a bridge between the representation of Lua numbers as
+ * {@link java.lang.Number} and the appropriate dispatch of arithmetic operations
+ * via the methods of {@link org.classdump.luna.LuaMathOperators}.
+ *
+ *
There are two concrete instances of this class, {@link #FLOAT} (the float
+ * arithmetic) and {@link #INTEGER} (the integer arithmetic). In order to obtain
+ * the correct instance for the given numbers, use the static methods
+ * {@link #of(Number, Number)} (for lookup based on two arguments, i.e. for binary
+ * operations) or {@link #of(Number)} (for lookup based on a single argument, i.e.
+ * for unary minus).
+ *
+ *
The arithmetic methods of this class yield boxed results.
+ *
+ *
Example: Given two objects {@code a} and {@code b} of type {@code Number},
+ * use the {@code Arithmetic} class to compute the value of the Lua expression
+ * {@code (a // b)}:
+ *
+ *
+ * // Number a, b
+ * final Number result;
+ * Arithmetic m = Arithmetic.of(a, b);
+ * if (m != null) {
+ * result = m.idiv(a, b);
+ * }
+ * else {
+ * throw new IllegalStateException("a or b is nil");
+ * }
+ *
Invokes arithmetic operations from {@link LuaMathOperators} with all arguments
+ * converted to Lua integers (i.e. {@code long}s).
+ */
+ public static final Arithmetic INTEGER = new IntegerArithmetic();
+ /**
+ * Float arithmetic.
+ *
+ *
Invokes arithmetic operations from {@link LuaMathOperators} with all arguments
+ * converted to Lua floats (i.e. {@code double}s).
+ */
+ public static final Arithmetic FLOAT = new FloatArithmetic();
+
+ private Arithmetic() {
+ // not to be instantiated by the outside world
+ }
+
+ /**
+ * Return an arithmetic based on the concrete types of the arguments {@code a}
+ * and {@code b}.
+ *
+ *
If either of {@code a} or {@code b} is {@code null}, returns {@code null}.
+ * Otherwise, if either of {@code a} or {@code b} a Lua float, returns {@link #FLOAT}.
+ * If {@code a} and {@code b} are both Lua integers, returns {@link #INTEGER}.
+ *
+ * @param a first argument, may be {@code null}
+ * @param b second argument, may be {@code null}
+ * @return {@code null} if {@code a} or {@code b} is {@code null}; {@link #FLOAT} if {@code a} or
+ * {@code b} is a Lua float; {@link #INTEGER} if {@code a} and {@code b} are Lua integers
+ */
+ public static Arithmetic of(Number a, Number b) {
+ if (a == null || b == null) {
+ return null;
+ } else if ((a instanceof Double || a instanceof Float)
+ || (b instanceof Double || b instanceof Float)) {
+ return FLOAT;
+ } else {
+ return INTEGER;
+ }
+ }
+
+ /**
+ * Return an arithmetic based on the concrete type of the argument {@code n}.
+ *
+ * If {@code n} is {@code null}, returns {@code null}; otherwise, returns
+ * {@link #FLOAT} if {@code n} is a Lua float, or {@link #INTEGER} if {@code n}
+ * is a Lua integer.
+ *
+ * @param n the argument, may be {@code null}
+ * @return {@code null} if {@code n} is {@code null}; {@link #FLOAT} if {@code n} is a Lua float;
+ * {@link #INTEGER} if {@code n} is a Lua integer
+ */
+ public static Arithmetic of(Number n) {
+ if (n == null) {
+ return null;
+ } else if (n instanceof Double || n instanceof Float) {
+ return FLOAT;
+ } else {
+ return INTEGER;
+ }
+ }
+
+ /**
+ * Returns the boxed result of the Lua addition of the two numbers
+ * {@code a} and {@code b}.
+ *
+ * @param a first addend, must not be {@code null}
+ * @param b second addend, must not be {@code null}
+ * @return the (boxed) value of the Lua expression {@code (a + b)}
+ * @throws NullPointerException if {@code a} or {@code b} is {@code null}
+ */
+ public abstract Number add(Number a, Number b);
+
+ /**
+ * Returns the boxed result of the Lua subtraction of the two numbers
+ * {@code a} and {@code b}.
+ *
+ * @param a the minuend, must not be {@code null}
+ * @param b the subtrahend, must not be {@code null}
+ * @return the (boxed) value of the Lua expression {@code (a - b)}
+ * @throws NullPointerException if {@code a} or {@code b} is {@code null}
+ */
+ public abstract Number sub(Number a, Number b);
+
+ /**
+ * Returns the boxed result of the Lua multiplication of the two numbers
+ * {@code a} and {@code b}.
+ *
+ * @param a first factor, must not be {@code null}
+ * @param b second factor, must not be {@code null}
+ * @return the (boxed) value of the Lua expression {@code (a * b)}
+ * @throws NullPointerException if {@code a} or {@code b} is {@code null}
+ */
+ public abstract Number mul(Number a, Number b);
+
+ /**
+ * Returns the boxed result of the Lua float division of the two numbers
+ * {@code a} and {@code b}.
+ *
+ * @param a the dividend, must not be {@code null}
+ * @param b the divisor, must not be {@code null}
+ * @return the (boxed) value of the Lua expression {@code (a / b)}
+ * @throws NullPointerException if {@code a} or {@code b} is {@code null}
+ */
+ public abstract Double div(Number a, Number b);
+
+ /**
+ * Returns the boxed result of the Lua modulo of the two numbers
+ * {@code a} and {@code b}.
+ *
+ * @param a the dividend, must not be {@code null}
+ * @param b the divisor, must not be {@code null}
+ * @return the (boxed) value of the Lua expression {@code (a % b)}
+ * @throws NullPointerException if {@code a} or {@code b} is {@code null}
+ */
+ public abstract Number mod(Number a, Number b);
+
+ /**
+ * Returns the boxed result of the Lua floor division of the two numbers
+ * {@code a} and {@code b}.
+ *
+ * @param a the dividend, must not be {@code null}
+ * @param b the divisor, must not be {@code null}
+ * @return the (boxed) value of the Lua expression {@code (a // b)}
+ * @throws NullPointerException if {@code a} or {@code b} is {@code null}
+ */
+ public abstract Number idiv(Number a, Number b);
+
+ /**
+ * Returns the boxed result of the Lua exponentiation of the two numbers
+ * {@code a} and {@code b}.
+ *
+ * @param a the base, must not be {@code null}
+ * @param b the exponent, must not be {@code null}
+ * @return the (boxed) value of the Lua expression {@code (a ^ b)}
+ * @throws NullPointerException if {@code a} or {@code b} is {@code null}
+ */
+ public abstract Double pow(Number a, Number b);
+
+ /**
+ * Returns the boxed result of the Lua arithmetic negation of the number
+ * {@code n}.
+ *
+ * @param n the operand, must not be {@code null}
+ * @return the (boxed) value of the Lua expression {@code (-n)}
+ * @throws NullPointerException if {@code n} is {@code null}
+ */
+ public abstract Number unm(Number n);
+
+ private static final class IntegerArithmetic extends Arithmetic {
+
+ @Override
+ public Long add(Number a, Number b) {
+ return LuaMathOperators.add(a.longValue(), b.longValue());
+ }
+
+ @Override
+ public Long sub(Number a, Number b) {
+ return LuaMathOperators.sub(a.longValue(), b.longValue());
+ }
+
+ @Override
+ public Long mul(Number a, Number b) {
+ return LuaMathOperators.mul(a.longValue(), b.longValue());
+ }
+
+ @Override
+ public Double div(Number a, Number b) {
+ return LuaMathOperators.div(a.longValue(), b.longValue());
+ }
+
+ @Override
+ public Long mod(Number a, Number b) {
+ return LuaMathOperators.mod(a.longValue(), b.longValue());
+ }
+
+ @Override
+ public Long idiv(Number a, Number b) {
+ return LuaMathOperators.idiv(a.longValue(), b.longValue());
+ }
+
+ @Override
+ public Double pow(Number a, Number b) {
+ return LuaMathOperators.pow(a.longValue(), b.longValue());
+ }
+
+ @Override
+ public Long unm(Number n) {
+ return LuaMathOperators.unm(n.longValue());
+ }
+
+ }
+
+ private static final class FloatArithmetic extends Arithmetic {
+
+ @Override
+ public Double add(Number a, Number b) {
+ return LuaMathOperators.add(a.doubleValue(), b.doubleValue());
+ }
+
+ @Override
+ public Double sub(Number a, Number b) {
+ return LuaMathOperators.sub(a.doubleValue(), b.doubleValue());
+ }
+
+ @Override
+ public Double mul(Number a, Number b) {
+ return LuaMathOperators.mul(a.doubleValue(), b.doubleValue());
+ }
+
+ @Override
+ public Double div(Number a, Number b) {
+ return LuaMathOperators.div(a.doubleValue(), b.doubleValue());
+ }
+
+ @Override
+ public Double mod(Number a, Number b) {
+ return LuaMathOperators.mod(a.doubleValue(), b.doubleValue());
+ }
+
+ @Override
+ public Double idiv(Number a, Number b) {
+ return LuaMathOperators.idiv(a.doubleValue(), b.doubleValue());
+ }
+
+ @Override
+ public Double pow(Number a, Number b) {
+ return LuaMathOperators.pow(a.doubleValue(), b.doubleValue());
+ }
+
+ @Override
+ public Double unm(Number n) {
+ return LuaMathOperators.unm(n.doubleValue());
+ }
+
+ }
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/ArrayByteString.java b/luna/src/main/java/org/classdump/luna/ArrayByteString.java
new file mode 100644
index 00000000..a5375720
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/ArrayByteString.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Objects;
+import org.classdump.luna.util.ArrayByteIterator;
+import org.classdump.luna.util.ByteIterator;
+
+/**
+ * A byte string backed by a byte array.
+ */
+class ArrayByteString extends ByteString {
+
+ static final ArrayByteString EMPTY_INSTANCE = new ArrayByteString(new byte[0]);
+
+ private final byte[] bytes;
+ private int hashCode;
+
+ ArrayByteString(byte[] bytes) {
+ this.bytes = Objects.requireNonNull(bytes);
+ }
+
+ private static void checkSubstringBounds(int start, int end, int len) {
+ if (start > end) {
+ throw new IndexOutOfBoundsException("start > end (" + start + " > " + end + ")");
+ } else if (start < 0) {
+ throw new IndexOutOfBoundsException("start < 0 (" + start + " < 0)");
+ } else if (end < 0) {
+ throw new IndexOutOfBoundsException("end < 0 (" + end + " < 0)");
+ } else if (end > len) {
+ throw new IndexOutOfBoundsException("end > length (" + start + " > " + len + ")");
+ }
+ }
+
+ @Override
+ protected boolean equalsByteString(ByteString that) {
+ if (this.length() != that.length()) {
+ return false;
+ }
+
+ int len = this.length();
+ for (int i = 0; i < len; i++) {
+ if (this.byteAt(i) != that.byteAt(i)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int hc = hashCode;
+ if (hc == 0) {
+ if (bytes.length > 0) {
+ for (byte b : bytes) {
+ hc = (hc * 31) + (b & 0xff);
+ }
+ hashCode = hc;
+ }
+ }
+
+ return hc;
+ }
+
+ @Override
+ int maybeHashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public String toString() {
+ return decode();
+ }
+
+ @Override
+ public String toRawString() {
+ char[] chars = new char[bytes.length];
+ for (int i = 0; i < chars.length; i++) {
+ chars[i] = (char) (bytes[i] & 0xff);
+ }
+ return String.valueOf(chars);
+ }
+
+ @Override
+ public int length() {
+ return bytes.length;
+ }
+
+ @Override
+ int maybeLength() {
+ return bytes.length;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return bytes.length == 0;
+ }
+
+ @Override
+ public byte byteAt(int index) {
+ return bytes[index];
+ }
+
+ @Override
+ public ByteIterator byteIterator() {
+ return new ArrayByteIterator(bytes);
+ }
+
+ @Override
+ public InputStream asInputStream() {
+ // no need to go via the iterator
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public ByteString substring(int start, int end) {
+ checkSubstringBounds(start, end, bytes.length);
+ return new ArrayByteString(Arrays.copyOfRange(bytes, start, end));
+ }
+
+ @Override
+ public byte[] getBytes() {
+ return Arrays.copyOf(bytes, bytes.length);
+ }
+
+ @Override
+ public void putTo(ByteBuffer buffer) {
+ buffer.put(bytes);
+ }
+
+ @Override
+ public void writeTo(OutputStream stream) throws IOException {
+ // must make a defensive copy to avoid leaking the contents
+ stream.write(getBytes());
+ }
+
+ @Override
+ public boolean startsWith(byte b) {
+ return bytes.length > 0 && bytes[0] == b;
+ }
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/ByteString.java b/luna/src/main/java/org/classdump/luna/ByteString.java
new file mode 100644
index 00000000..5ed6f7a9
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/ByteString.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Objects;
+import org.classdump.luna.util.ByteIterator;
+
+/**
+ * Byte string, an immutable sequence of bytes.
+ *
+ *
The purpose of this class is to serve as a bridge between Java strings (with their
+ * characters corresponding to 16-bit code units in the Basic Multilingual Plane (BMP))
+ * and Lua strings (raw 8-bit sequences).
+ *
+ *
Byte strings come in two flavours:
+ *
+ *
one is a wrapper of {@link String java.lang.String} with a given {@link Charset}
+ * — constructed using {@link #of(String)};
+ *
the other is a wrapper of a {@code byte} arrays
+ * — constructed using {@link #wrap(byte[])}.
+ *
+ *
+ *
The {@code ByteString} class provides the functionality for treating both cases
+ * as sequences of bytes when they take part in Lua operations, and as Java strings when
+ * used by an outer Java application. However, these perspectives are as lazy
+ * as possible, in order to avoid doing unnecessary work.
+ *
+ *
This class provides a natural lexicographical ordering that is consistent with equals.
+ */
+public abstract class ByteString implements Comparable {
+
+ ByteString() {
+ // no-op: package-private to restrict access
+ }
+
+ /**
+ * Returns a new byte string corresponding to the bytes in {@code s} as encoded
+ * by the specified {@code charset}.
+ *
+ * @param s the string to take the byte view of, must not be {@code null}
+ * @param charset the charset to use for decoding {@code s} into bytes, must not be {@code null}
+ * @return a byte string perspective of {@code s} using {@code charset}
+ * @throws NullPointerException if {@code s} or {@code charset} is {@code null}
+ * @throws IllegalArgumentException if {@code charset} does not provide encoding capability (see
+ * {@link Charset#canEncode()})
+ */
+ public static ByteString of(String s, Charset charset) {
+ return new StringByteString(s, charset);
+ }
+
+ /**
+ * Returns a new byte string corresponding to the bytes in {@code s} as encoded
+ * by the default charset ({@link Charset#defaultCharset()}).
+ *
+ * @param s the string to take the perspective of, must not be {@code null}
+ * @return a byte string perspective of {@code s}
+ * @throws NullPointerException if {@code s} is {@code null}
+ */
+ public static ByteString of(String s) {
+ return of(s, Charset.defaultCharset());
+ }
+
+ /**
+ * Returns a new byte string corresponding to bytes in {@code s} by taking the
+ * least significant byte of each character.
+ *
+ * @param s the string to get bytes from, must not be {@code null}
+ * @return a byte string based on {@code s} by taking the least significant byte of each char
+ * @throws NullPointerException if {@code s} is {@code null}
+ */
+ public static ByteString fromRaw(String s) {
+ byte[] bytes = new byte[s.length()];
+ for (int i = 0; i < bytes.length; i++) {
+ bytes[i] = (byte) ((int) s.charAt(i) & 0xff);
+ }
+ return wrap(bytes);
+ }
+
+ /**
+ * Returns a byte string corresponding to the bytes in {@code s} as encoded by the default
+ * charset in a form suitable for use as a string constant.
+ *
+ *
This method differs from {@link #of(String)} in that it may force the computation
+ * of lazily-evaluated properties of the resulting string at instantiation time and
+ * cache them for use at runtime.
+ *
+ * @param s the string to get bytes from, must not be {@code null}
+ * @return a byte string based on a byte perspective of {@code s}
+ */
+ public static ByteString constOf(String s) {
+ return of(s);
+ }
+
+ static ByteString wrap(byte[] bytes) {
+ return new ArrayByteString(bytes);
+ }
+
+ /**
+ * Returns a byte string containing a copy of the byte array {@code bytes}.
+ *
+ * @param bytes the byte array to use as the byte string, must not be {@code null}
+ * @return a byte string containing a copy of {@code bytes}
+ * @throws NullPointerException if {@code bytes} is {@code null}
+ */
+ public static ByteString copyOf(byte[] bytes) {
+ return copyOf(bytes, 0, bytes.length);
+ }
+
+ /**
+ * Returns a byte string containing a copy of a slice of the byte array {@code bytes}
+ * starting at the offset {@code offset} and consisting of {@code length} bytes.
+ *
+ * @param bytes the byte array to use as the byte string, must not be {@code null}
+ * @param offset offset in {@code bytes} to start reading from
+ * @param length the number of bytes to copy from {@code bytes}
+ * @return a byte string containing a copy of {@code bytes}
+ * @throws NullPointerException if {@code bytes} is {@code null}
+ * @throws IndexOutOfBoundsException if {@code offset} or {@code length} are negative, or if
+ * {@code (offset + length)} is greater than {@code bytes.length}
+ */
+ public static ByteString copyOf(byte[] bytes, int offset, int length) {
+ if (offset < 0 || length < 0 || (offset + length) > bytes.length) {
+ throw new IndexOutOfBoundsException("offset=" + offset + ", length=" + length);
+ }
+
+ return wrap(Arrays.copyOfRange(bytes, offset, length));
+ }
+
+ /**
+ * Returns an empty byte string.
+ *
+ * @return an empty byte string
+ */
+ public static ByteString empty() {
+ return ArrayByteString.EMPTY_INSTANCE;
+ }
+
+ /**
+ * Returns {@code true} if {@code o} is a byte string containing the same bytes as
+ * this byte string.
+ *
+ *
Note: this method uses the strict interpretation of byte strings as byte
+ * sequences. It is therefore not necessarily true that for two byte strings {@code a}
+ * and {@code b}, the result of their comparison is the same as the result of comparing
+ * their images provided by {@link #toString()}:
+ *
+ * @param o object to evaluate for equality with, may be {@code null}
+ * @return {@code true} iff {@code o} is a byte string with equal contents to {@code this}
+ */
+ @Override
+ public final boolean equals(Object o) {
+ return (this == o) || ((o instanceof ByteString) && this.equalsByteString((ByteString) o));
+ }
+
+ /**
+ * Returns the hash code of this byte string. The hash code is computed using the same
+ * function as used by {@link String#hashCode()}, interpreting the byte string's bytes
+ * as unsigned integers.
+ *
+ * @return the hash code of this byte string
+ */
+ @Override
+ public abstract int hashCode();
+
+ /**
+ * Returns an integer i that corresponds to the hash code of this byte string
+ * if i is non-zero. When i is zero, it may or may not be the hash code
+ * of this string.
+ *
+ * @return the hash code of this byte string if non-zero
+ */
+ abstract int maybeHashCode();
+
+ abstract boolean equalsByteString(ByteString that);
+
+ /**
+ * Returns a new byte array containing the bytes of this byte string.
+ *
+ * @return a new byte array
+ */
+ public abstract byte[] getBytes();
+
+ /**
+ * Returns the byte at position {@code index}.
+ *
+ * @param index the position in the string
+ * @return the byte at position {@code index}
+ * @throws IndexOutOfBoundsException if {@code index < 0} or {@code index >= length()}
+ */
+ public abstract byte byteAt(int index);
+
+ /**
+ * Returns an iterator over the bytes in this byte string.
+ *
+ * @return an iterator over the bytes in this byte string
+ */
+ public abstract ByteIterator byteIterator();
+
+ /**
+ * Returns an input stream that reads the contents of this string.
+ *
+ * @return an input stream that reads the contents of this string
+ */
+ public InputStream asInputStream() {
+ return new ByteStringInputStream(byteIterator());
+ }
+
+ /**
+ * Returns the length of this byte string, i.e., the number of bytes it contains.
+ *
+ * @return the length of this byte string
+ */
+ public abstract int length();
+
+ /**
+ * Returns an integer i that is equal to the length of this byte string if
+ * i is non-negative. When i is negative, the length of this byte string
+ * is not yet known.
+ *
+ * @return the length of this byte string if non-negative
+ */
+ abstract int maybeLength();
+
+ /**
+ * Returns {@code true} iff this byte string is empty, i.e., if the number of bytes it
+ * contains is 0.
+ *
+ * @return {@code true} iff this byte string is empty
+ */
+ public abstract boolean isEmpty();
+
+ /**
+ * Returns a substring of this byte string starting at position {@code start} (inclusive),
+ * ending at position {@code end} (exclusive).
+ *
+ *
The indices refer to the byte position in the byte string.
+ *
+ * @param start the first index to include in the new substring (inclusive)
+ * @param end the smallest index immediately following the new substring in this byte string
+ * @return a substring of this byte string ranging from {@code start} (inclusive) to {@code end}
+ * (exclusive)
+ * @throws IndexOutOfBoundsException if {@code start < 0}, {@code end > length()} or {@code start
+ * > end}
+ */
+ public abstract ByteString substring(int start, int end);
+
+ /**
+ * Puts the contents of this byte string to the specified {@code buffer}.
+ *
+ * @param buffer the buffer to use, must not be {@code null}
+ * @throws NullPointerException if {@code buffer} is {@code null}
+ */
+ public abstract void putTo(ByteBuffer buffer);
+
+ /**
+ * Writes the contents of this byte string to the specified {@code stream}.
+ *
+ * @param stream the stream to use, must not be {@code null}
+ * @throws IOException when I/O error happens during the write
+ * @throws NullPointerException if {@code stream} is {@code null}
+ */
+ public abstract void writeTo(OutputStream stream) throws IOException;
+
+ /**
+ * Returns the interpretation of this byte string as a Java string.
+ *
+ * @return the string represented by this byte string
+ */
+ @Override
+ public abstract String toString();
+
+ /**
+ * Returns a string representation of this byte string that uses the specified
+ * charset {@code charset} to decode characters from bytes.
+ *
+ * @param charset the charset to use, must not be {@code null}
+ * @return this byte string decoded into a string using {@code charset}
+ * @throws NullPointerException if {@code charset} is {@code null}
+ */
+ public String decode(Charset charset) {
+ if (isEmpty()) {
+ return "";
+ }
+
+ ByteBuffer byteBuffer = ByteBuffer.allocate(length());
+ putTo(byteBuffer);
+ byteBuffer.flip();
+ return charset.decode(byteBuffer).toString();
+ }
+
+ /**
+ * Returns a string represented by this byte string decoded using the default charset
+ * of the virtual machine.
+ *
+ *
This is effectively equivalent to {@link #decode(Charset)}
+ * called with {@link Charset#defaultCharset()}.
+ *
+ * @return a string decoded from this byte string using the platform's default charset
+ */
+ public String decode() {
+ return decode(Charset.defaultCharset());
+ }
+
+ /**
+ * Returns a string in which all characters are directly mapped to the bytes in this
+ * byte string by treating them as unsigned integers.
+ *
+ *
This method is the complement of {@link #fromRaw(String)}.
+ *
+ * @return a raw string based on this byte string
+ */
+ public abstract String toRawString();
+
+ /**
+ * Compares this byte string lexicographically with {@code that}. Returns a negative
+ * integer, zero, or a positive integer if {@code this} is lesser than, equal to or greater
+ * than {@code that} in this ordering.
+ *
+ *
For the purposes of this ordering, bytes are interpreted as unsigned
+ * integers.
+ *
+ *
Note: this method uses the strict interpretation of byte strings as byte
+ * sequences. It is therefore not necessarily true that for two byte strings {@code a}
+ * and {@code b}, the result of their comparison is the same as the result of comparing
+ * their images provided by {@link #toString()}:
+ *
+ * int byteCmp = a.compareTo(b);
+ * int stringCmp = a.toString().compareTo(b.toString());
+ *
+ * // may fail!
+ * assert(Integer.signum(byteCmp) == Integer.signum(stringCmp));
+ *
+ *
+ *
This is done in order to ensure that the natural ordering provided by this
+ * {@code compareTo()} is consistent with equals.
+ *
+ * @param that byte string to compare to, must not be {@code null}
+ * @return a negative, zero, or positive integer if {@code this} is lexicographically lesser than,
+ * equal to or greater than {@code that}
+ * @throws NullPointerException if {@code that} is {@code null}
+ */
+ @Override
+ public int compareTo(ByteString that) {
+ Objects.requireNonNull(that);
+
+ ByteIterator thisIterator = this.byteIterator();
+ ByteIterator thatIterator = that.byteIterator();
+
+ while (thisIterator.hasNext() && thatIterator.hasNext()) {
+ int thisByte = thisIterator.nextByte() & 0xff;
+ int thatByte = thatIterator.nextByte() & 0xff;
+ int diff = thisByte - thatByte;
+ if (diff != 0) {
+ return diff;
+ }
+ }
+
+ return thisIterator.hasNext()
+ ? 1 // !thatIterator.hasNext() => that is shorter
+ : thatIterator.hasNext()
+ ? -1 // this is shorter
+ : 0; // equal length
+ }
+
+ /**
+ * Returns a byte string formed by a concatenating this byte string with the byte string
+ * {@code other}.
+ *
+ *
Note: this method uses the non-strict interpretation and therefore
+ * may (but might not necessarily) preserve unmappable and malformed characters
+ * occurring in the two strings.
+ *
+ * @param other the byte string to concatenate this byte string with, must not be {@code null}
+ * @return this byte string concatenated with {@code other}
+ * @throws NullPointerException if {@code other} is {@code null}
+ */
+ public ByteString concat(ByteString other) {
+ if (other.isEmpty()) {
+ return this;
+ } else if (this.isEmpty()) {
+ return other;
+ }
+
+ byte[] thisBytes = this.getBytes();
+ byte[] otherBytes = other.getBytes();
+
+ byte[] result = new byte[thisBytes.length + otherBytes.length];
+ System.arraycopy(thisBytes, 0, result, 0, thisBytes.length);
+ System.arraycopy(otherBytes, 0, result, thisBytes.length, otherBytes.length);
+ return ByteString.wrap(result);
+ }
+
+ /**
+ * Returns a byte string formed by concatenating this byte string with the string
+ * {@code other}.
+ *
+ *
This is a convenience method equivalent to
+ *
+ * concat(ByteString.of(other))
+ *
+ *
+ * @param other the string to concatenate with, must not be {@code null}
+ * @return this byte string concatenated with {@code other}
+ * @throws NullPointerException if {@code other} is {@code null}
+ */
+ public ByteString concat(String other) {
+ return this.concat(ByteString.of(other));
+ }
+
+ // TODO: add startsWith(ByteString)
+
+ /**
+ * Returns {@code true} if the first byte of this byte string is {@code b}.
+ *
+ * @param b the byte to compare the first byte of this byte string to
+ * @return {@code true} if this byte string starts with {@code b}
+ */
+ public abstract boolean startsWith(byte b);
+
+ // TODO: add contains(ByteString)
+
+ /**
+ * Returns {@code true} if the byte string contains the byte {@code b}.
+ *
+ * @param b the byte to search for in the byte string
+ * @return {@code true} if this byte string contains {@code b}
+ */
+ public boolean contains(byte b) {
+ ByteIterator it = byteIterator();
+ while (it.hasNext()) {
+ if (b == it.nextByte()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Replaces all occurrences of the byte string {@code target} in this byte string
+ * with the replacement {@code replacement}.
+ *
+ * @param target the substring to replace, must not be {@code null}
+ * @param replacement the replacement, must not be {@code null}
+ * @return this byte string with all occurrences of {@code target} replaced by {@code replacement}
+ * @throws NullPointerException if {@code target} or {@code replacement} is {@code null}
+ */
+ public ByteString replace(ByteString target, ByteString replacement) {
+ // FIXME: don't go via raw strings
+ return ByteString.fromRaw(this.toRawString().replace(
+ target.toRawString(),
+ replacement.toRawString()));
+ }
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/ByteStringBuilder.java b/luna/src/main/java/org/classdump/luna/ByteStringBuilder.java
new file mode 100644
index 00000000..48638d32
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/ByteStringBuilder.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import org.classdump.luna.util.Check;
+
+/**
+ * A builder for byte strings, similar in interface to {@link StringBuilder}.
+ *
+ *
This class is not thread-safe.
+ */
+public class ByteStringBuilder {
+
+ private static final int DEFAULT_CAPACITY = 32;
+ // don't go any smaller than this
+ private static final int MIN_CAPACITY = DEFAULT_CAPACITY;
+ private byte[] buffer;
+ private int length;
+
+ private ByteStringBuilder(byte[] buffer, int length) {
+ this.buffer = buffer;
+ this.length = length;
+ }
+
+ /**
+ * Constructs a new empty {@code ByteStringBuilder} that can hold at least {@code capacity}
+ * bytes.
+ *
+ * @param capacity the initial required capacity, must not be negative
+ * @throws IllegalArgumentException if {@code capacity} is negative
+ */
+ public ByteStringBuilder(int capacity) {
+ this(new byte[idealCapacity(Check.nonNegative(capacity))], 0);
+ }
+
+ /**
+ * Constructs a new empty {@code ByteStringBuilder}.
+ */
+ public ByteStringBuilder() {
+ this(new byte[DEFAULT_CAPACITY], 0);
+ }
+
+ // returns the smallest positive integer i >= x such that i is a power of 2
+ private static int binaryCeil(int x) {
+ if (x < 0) {
+ return 0;
+ }
+ // from "Hacker's Delight" by Henry S. Warren, Jr., section 3-2
+ x -= 1;
+ x |= (x >> 1);
+ x |= (x >> 2);
+ x |= (x >> 4);
+ x |= (x >> 8);
+ x |= (x >> 16);
+ return x + 1;
+ }
+
+ private static int idealCapacity(int desired) {
+ int ceil = binaryCeil(desired);
+ return Math.max(ceil, MIN_CAPACITY);
+ }
+
+ private static byte[] resize(byte[] buf, int newSize) {
+ assert (newSize >= buf.length);
+ byte[] newBuf = new byte[newSize];
+ System.arraycopy(buf, 0, newBuf, 0, buf.length);
+ return newBuf;
+ }
+
+ private void ensureCapacity(int cap) {
+ if (cap > buffer.length) {
+ buffer = resize(buffer, idealCapacity(cap));
+ }
+ }
+
+ /**
+ * Returns the current capacity of the builder.
+ *
+ * @return the current capacity of the builder
+ */
+ public int capacity() {
+ return buffer.length;
+ }
+
+ /**
+ * Returns the number of bytes in this builder.
+ *
+ * @return the number of bytes in this builder
+ */
+ public int length() {
+ return length;
+ }
+
+ /**
+ * Sets the number of bytes in this builder to {@code newLength}.
+ *
+ *
When {@code newLength} is lesser than the current length {@code len},
+ * drops the last {@code (len - newLength)} bytes from the constructed sequence.
+ * If {@code newLength} is greater than {@code len}, appends {@code newLength - len}
+ * zero bytes.
+ *
+ *
No memory is freed when reducing the length of the sequence.
+ *
+ * @param newLength the new length, must not be negative
+ * @throws IndexOutOfBoundsException if {@code newLength} is negative
+ */
+ public void setLength(int newLength) {
+ if (newLength < 0) {
+ throw new IndexOutOfBoundsException(Integer.toString(newLength));
+ }
+
+ if (newLength < length) {
+ length = newLength;
+ } else if (newLength > length) {
+ ensureCapacity(newLength);
+ Arrays.fill(buffer, length, newLength, (byte) 0);
+ }
+ }
+
+ /**
+ * Attempts to reduce the memory consumption of this builder by reducing its capacity
+ * to a smaller, yet still sufficient value.
+ */
+ public void trimToSize() {
+ int cap = idealCapacity(length);
+ if (cap < capacity()) {
+ buffer = resize(buffer, cap);
+ }
+ }
+
+ /**
+ * Sets the byte at position {@code index} to {@code value}.
+ *
+ * @param index the index of the byte to set
+ * @param value the new value of the byte at position {@code index}
+ * @throws IndexOutOfBoundsException if {@code index} is negative or greater than or equal to the
+ * current buffer length
+ */
+ public void setByteAt(int index, byte value) {
+ if (index < 0 || index > length) {
+ throw new IndexOutOfBoundsException(String.valueOf(index));
+ }
+ buffer[index] = value;
+ }
+
+ /**
+ * Appends the byte {@code b}.
+ *
+ * @param b the byte to append
+ * @return this builder
+ */
+ public ByteStringBuilder append(byte b) {
+ ensureCapacity(length + 1);
+ buffer[length] = b;
+ length += 1;
+ return this;
+ }
+
+ /**
+ * Appends the contents of the byte array {@code array}. {@code off}
+ * is the offset in {@code array} to start the write from, and {@code len} is the
+ * number of bytes that should be written.
+ *
+ *
Throws an {@code IndexOutOfBoundsException} if {@code off} or {@code len}
+ * is negative, or if {@code (off + len)} is greater than {@code array.length}.
+ *
+ * @param array the byte array, must not be {@code null}
+ * @param off offset in {@code array} to start from
+ * @param len number of bytes to write
+ * @return this builder
+ * @throws NullPointerException if {@code array} is {@code null}
+ * @throws IndexOutOfBoundsException if {@code off} or {@code len} is negative, or if {@code (off
+ * + len)} is greater than {@code array.length}
+ */
+ public ByteStringBuilder append(byte[] array, int off, int len) {
+ if (off < 0 || len < 0 || (off + len) > array.length) {
+ throw new IndexOutOfBoundsException("off=" + off + ", len=" + len);
+ }
+
+ if (len > 0) {
+ ensureCapacity(length + len);
+ System.arraycopy(array, off, buffer, length, len);
+ length += len;
+ }
+
+ return this;
+ }
+
+ /**
+ * Appends the contents of the byte array {@code array}.
+ *
+ * @param array the byte array to append, must not be {@code null}
+ * @return this builder
+ * @throws NullPointerException if {@code array} is {@code null}
+ */
+ public ByteStringBuilder append(byte[] array) {
+ return append(array, 0, array.length);
+ }
+
+ /**
+ * Appends the contents of the byte string {@code string}.
+ *
+ * @param string the byte string to append, must not be {@code null}
+ * @return this builder
+ * @throws NullPointerException if {@code string} is {@code null}
+ */
+ public ByteStringBuilder append(ByteString string) {
+ return append(string.getBytes());
+ }
+
+ /**
+ * Appends a char sequence {@code charSequence} interpreted as a sequence
+ * of bytes using the specified {@code Charset}.
+ *
+ * @param charSequence the char sequence to append, must not be {@code null}
+ * @param charset the charset to use for encoding, must not be {@code null}
+ * @return this builder
+ * @throws NullPointerException if {@code string} is {@code null}
+ * @throws IllegalArgumentException if {@code charset} cannot does not provide encoding capability
+ * (see {@link Charset#canEncode()})
+ */
+ public ByteStringBuilder append(CharSequence charSequence, Charset charset) {
+ if (!charset.canEncode()) {
+ throw new IllegalArgumentException("Charset cannot encode: " + charset.name());
+ }
+
+ // FIXME: inefficient, could be done more directly
+ append(ByteString.of(charSequence.toString(), charset));
+ return this;
+ }
+
+ /**
+ * Appends the char sequence {@code charSequence} interpreted as a sequence
+ * of bytes using the virtual machine's default charset (see {@link Charset#defaultCharset()}).
+ *
+ * @param charSequence the char sequence to append, must not be {@code null}
+ * @return this builder
+ * @throws NullPointerException if {@code charSequence} is {@code null}
+ */
+ public ByteStringBuilder append(CharSequence charSequence) {
+ return append(charSequence, Charset.defaultCharset());
+ }
+
+ /**
+ * Returns a byte string consisting of the bytes in this builder.
+ *
+ * @return a byte string with this builder's contents
+ */
+ public ByteString toByteString() {
+ return ByteString.copyOf(buffer, 0, length);
+ }
+
+ /**
+ * Returns the interpretation of this builder's bytes as a {@code java.lang.String}.
+ *
+ * @return a {@code java.lang.String} interpretation of the bytes in this builder
+ */
+ @Override
+ public String toString() {
+ return toByteString().toString();
+ }
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/ByteStringInputStream.java b/luna/src/main/java/org/classdump/luna/ByteStringInputStream.java
new file mode 100644
index 00000000..9a2d237e
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/ByteStringInputStream.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+import java.io.IOException;
+import java.io.InputStream;
+import org.classdump.luna.util.ByteIterator;
+
+class ByteStringInputStream extends InputStream {
+
+ private final ByteIterator iterator;
+
+ public ByteStringInputStream(ByteIterator iterator) {
+ this.iterator = iterator;
+ }
+
+ @Override
+ public int read() throws IOException {
+ return !iterator.hasNext() ? -1 : iterator.nextByte() & 0xff;
+ }
+
+ // TODO implement more efficient version
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ return super.read(b, off, len);
+ }
+}
diff --git a/luna/src/main/java/org/classdump/luna/ConversionException.java b/luna/src/main/java/org/classdump/luna/ConversionException.java
new file mode 100644
index 00000000..5a042cfa
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/ConversionException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+/**
+ * An exception thrown to indicate an unsuccessful value conversion.
+ */
+public class ConversionException extends LuaRuntimeException {
+
+ /**
+ * Constructs a new instance with the given error message.
+ *
+ * @param message error message
+ */
+ public ConversionException(String message) {
+ super(message);
+ }
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/Conversions.java b/luna/src/main/java/org/classdump/luna/Conversions.java
new file mode 100644
index 00000000..71980d3a
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/Conversions.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+import java.util.Arrays;
+
+/**
+ * Static methods implementing Lua value conversions.
+ */
+public final class Conversions {
+
+ private static final ByteString NULL_ERROR_MESSAGE = ByteString.constOf("(null)");
+
+ private Conversions() {
+ // not to be instantiated
+ }
+
+ /**
+ * Returns the numerical value of the string {@code s}, or {@code null} if
+ * {@code s} does not have a numerical value.
+ *
+ *
If {@code s} is a valid Lua integer literal with optional sign, the numerical
+ * value is the corresponding integer; if {@code s} is a valid Lua float literal with
+ * optional sign, the numerical value is the corresponding float. Otherwise, the {@code s}
+ * does not have a numerical value.
+ *
+ *
Leading and trailing whitespace in {@code s} is ignored by this method.
+ *
+ *
Numbers returned by this method are in the canonical representation.
+ *
+ * @param s string to convert to numerical value, may be {@code null}
+ * @return a number representing the numerical value of {@code s} (in the canonical
+ * representation), or {@code null} if {@code s} does not have a numerical value
+ */
+ public static Number numericalValueOf(ByteString s) {
+ String trimmed = s.toString().trim();
+ try {
+ return Long.valueOf(LuaFormat.parseInteger(trimmed));
+ } catch (NumberFormatException ei) {
+ try {
+ return Double.valueOf(LuaFormat.parseFloat(trimmed));
+ } catch (NumberFormatException ef) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Returns the numerical value of the object {@code o}, or {@code null} if {@code o}
+ * does not have a numerical value.
+ *
+ *
If {@code o} is already a number, returns {@code o} cast to number. If {@code o}
+ * is a string, returns its numerical value (see {@link #numericalValueOf(ByteString)}).
+ * Otherwise, returns {@code null}.
+ *
+ *
This method differs from {@link #arithmeticValueOf(Object)} in that it
+ * preserves the numerical value representation of coerced strings. For use in arithmetic
+ * operations following Lua's argument conversion rules, use that method instead.
+ *
+ *
Numbers returned by this method are not necessarily in the canonical representation.
+ *
+ * @param o object to convert to numerical value, may be {@code null}
+ * @return number representing the numerical value of {@code o} (not necessarily in the canonical
+ * representation), of {@code null} if {@code o} does not have a numerical value
+ * @see #arithmeticValueOf(Object)
+ */
+ public static Number numericalValueOf(Object o) {
+ if (o instanceof Number) {
+ return (Number) o;
+ } else if (o instanceof ByteString) {
+ return numericalValueOf((ByteString) o);
+ } else if (o instanceof String) {
+ return numericalValueOf(ByteString.of((String) o));
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the numerical value of {@code o}, throwing a {@link ConversionException}
+ * if {@code o} does not have a numerical value.
+ *
+ *
The conversion rules are those of {@link #numericalValueOf(Object)}; the only difference
+ * is that this method throws an exception rather than returning {@code null} to signal
+ * errors.
+ *
+ *
Numbers returned by this method are not necessarily in the canonical representation.
+ *
+ * @param o object to convert to numerical value, may be {@code null}
+ * @param name value name for error reporting, may be {@code null}
+ * @return number representing the numerical value of {@code o} (not necessarily in the canonical
+ * representation), guaranteed to be non-{@code null}
+ * @throws ConversionException if {@code o} is not a number or string convertible to number.
+ * @see #numericalValueOf(Object)
+ */
+ public static Number toNumericalValue(Object o, String name) {
+ Number n = numericalValueOf(o);
+ if (n == null) {
+ throw new ConversionException((name != null ? name : "value") + " must be a number");
+ } else {
+ return n;
+ }
+ }
+
+ /**
+ * Returns the number {@code n} in its canonical representation,
+ * i.e. a {@link java.lang.Long} if {@code n} is a Lua integer, or a {@link java.lang.Double}
+ * if {@code n} is a Lua float.
+ *
+ * @param n number to convert to canonical representation, must not be {@code null}
+ * @return an instance of {@code Long} if {@code n} is an integer, or an instance of {@code
+ * Double} if {@code n} is a float
+ * @throws NullPointerException if {@code n} is {@code null}
+ */
+ public static Number toCanonicalNumber(Number n) {
+ if (n instanceof Long || n instanceof Double) {
+ // already in canonical representation
+ return n;
+ } else if (n instanceof Float) {
+ // re-box
+ return Double.valueOf(n.doubleValue());
+ } else {
+ // re-box
+ return Long.valueOf(n.longValue());
+ }
+ }
+
+ /**
+ * Returns the value {@code o} to its canonical representation.
+ *
+ *
For numbers, this method is equivalent to {@link #toCanonicalNumber(Number)}.
+ * If {@code o} is a {@link String java.lang.String}, it is wrapped into a byte
+ * string by {@link ByteString#of(String)}. Otherwise, {@code o} is in canonical
+ * representation.
+ *
+ *
This method is intended for use at the Java → Lua boundary, and whenever
+ * it is not certain that {@code o} is in a canonical representation when a canonical
+ * representation is required.
+ *
+ * @param o value to convert to canonical representation, may be {@code null}
+ * @return {@code o} converted to canonical representation
+ */
+ public static Object canonicalRepresentationOf(Object o) {
+ if (o instanceof Number) {
+ return toCanonicalNumber((Number) o);
+ } else if (o instanceof String) {
+ return ByteString.of((String) o);
+ } else {
+ return o;
+ }
+ }
+
+ /**
+ * Returns the value {@code o} in its Java representation.
+ *
+ *
If {@code o} is a {@link ByteString}, returns {@code o} as a {@code java.lang.String}
+ * (using {@link ByteString#toString()}. Otherwise, returns {@code o}.
+ *
+ *
This method is intended for use at the Lua → Java boundary for interoperating
+ * with Java code unaware of (or not concerned with) the interpretation of Lua
+ * strings as sequences of bytes.
+ *
+ * @param o value to convert to Java representation, may be {@code null}
+ * @return {@code o} converted to a {@code java.lang.String} if {@code o} is a byte string, {@code
+ * o} otherwise
+ */
+ public static Object javaRepresentationOf(Object o) {
+ if (o instanceof ByteString) {
+ return o.toString();
+ } else {
+ return o;
+ }
+ }
+
+ /**
+ * Modifies the contents of the array {@code values} by converting all values to
+ * their canonical representations.
+ *
+ * @param values values to convert to their canonical representations, must not be {@code null}
+ * @throws NullPointerException if {@code values} is {@code null}
+ * @see #canonicalRepresentationOf(Object)
+ */
+ public static void toCanonicalValues(Object[] values) {
+ for (int i = 0; i < values.length; i++) {
+ Object v = values[i];
+ values[i] = canonicalRepresentationOf(v);
+ }
+ }
+
+ /**
+ * Modifies the contents of the array {@code values} by converting all values to
+ * their Java representations.
+ *
+ *
This method is intended for use at the Lua → Java boundary for interoperating
+ * with Java code unaware of (or not concerned with) the interpretation of Lua
+ * strings as sequences of bytes.
+ *
+ * @param values values to convert to their Java representations, must not be {@code null}
+ * @throws NullPointerException if {@code values} is {@code null}
+ * @see #javaRepresentationOf(Object)
+ */
+ public static void toJavaValues(Object[] values) {
+ for (int i = 0; i < values.length; i++) {
+ Object v = values[i];
+ values[i] = javaRepresentationOf(v);
+ }
+ }
+
+ /**
+ * Returns a copy of the array {@code values} with all values converted to their
+ * canonical representation.
+ *
+ * @param values values to convert to their canonical representation, must not be {@code null}
+ * @return a copy of {@code values} with all elements converted to canonical representation
+ * @see #canonicalRepresentationOf(Object)
+ */
+ public static Object[] copyAsCanonicalValues(Object[] values) {
+ values = Arrays.copyOf(values, values.length);
+ toCanonicalValues(values);
+ return values;
+ }
+
+ /**
+ * Returns a copy of the array {@code values} with all values converted to their
+ * Java representation.
+ *
+ *
This method is intended for use at the Lua → Java boundary for interoperating
+ * with Java code unaware of (or not concerned with) the interpretation of Lua
+ * strings as sequences of bytes.
+ *
+ * @param values values to convert to their Java representation, must not be {@code null}
+ * @return a copy of {@code values} with all elements converted to their Java representation
+ * @see #javaRepresentationOf(Object)
+ */
+ public static Object[] copyAsJavaValues(Object[] values) {
+ values = Arrays.copyOf(values, values.length);
+ toJavaValues(values);
+ return values;
+ }
+
+ /**
+ * Normalises the number {@code n} so that it may be used as a key in a Lua
+ * table.
+ *
+ *
If {@code n} has an integer value i, returns the canonical representation
+ * of i; otherwise, returns the canonical representation of {@code n}
+ * (see {@link #toCanonicalNumber(Number)}).
+ *
+ * @param n number to normalise, must not be {@code null}
+ * @return an canonical integer if {@code n} has an integer value, the canonical representation of
+ * {@code n} otherwise
+ * @throws NullPointerException if {@code n} is {@code null}
+ */
+ public static Number normaliseKey(Number n) {
+ Long i = integerValueOf(n);
+ return i != null ? i : toCanonicalNumber(n);
+ }
+
+ /**
+ * Normalises the argument {@code o} so that it may be used safely as a key
+ * in a Lua table.
+ *
+ *
If {@code o} is a number, returns the number normalised (see {@link #normaliseKey(Number)}.
+ * If {@code o} is a {@code java.lang.String}, returns {@code o} as a byte string using
+ * {@link ByteString#of(String)}. Otherwise, returns {@code o}.
+ *
+ * @param o object to normalise, may be {@code null}
+ * @return normalised number if {@code o} is a number, {@code o} as byte string if {@code o} is a
+ * {@code java.lang.String}, {@code o} otherwise
+ */
+ public static Object normaliseKey(Object o) {
+ if (o instanceof Number) {
+ return normaliseKey((Number) o);
+ } else if (o instanceof String) {
+ return ByteString.of((String) o);
+ } else {
+ return o;
+ }
+ }
+
+ /**
+ * Returns the arithmetic value of the object {@code o}, or {@code null} if {@code o}
+ * does not have an arithmetic value.
+ *
+ *
If {@code o} is a number, then that number is its arithmetic value. If {@code o}
+ * is a string that has a numerical value (see {@link #numericalValueOf(ByteString)}),
+ * its arithmetic value is the numerical value converted to a float. Otherwise,
+ * {@code o} does not have an arithmetic value.
+ *
+ *
Note that this method differs from {@link #numericalValueOf(Object)} in that it
+ * coerces strings convertible to numbers into into floats rather than preserving
+ * their numerical value representation, and also note that this conversion happens
+ * after the numerical value has been determined. Most significantly,
+ *
+ *
+ * Conversions.arithmeticValueOf("-0")
+ *
+ *
+ *
yields {@code 0.0} rather than {@code 0} (as would be the case with
+ * {@code numericalValueOf("-0")}), or {@code -0.0} (it would in the case if the string
+ * was parsed directly as a float).
+ *
+ *
Numbers returned by this method are not necessarily in the canonical representation.
+ *
+ * @param o object to convert to arithmetic value, may be {@code null}
+ * @return number representing the arithmetic value of {@code o} (not necessarily in the canonical
+ * representation), or {@code null} if {@code o} does not have an arithmetic value
+ * @see #numericalValueOf(Object)
+ */
+ public static Number arithmeticValueOf(Object o) {
+ if (o instanceof Number) {
+ return (Number) o;
+ } else {
+ Number n = numericalValueOf(o);
+ return n != null ? floatValueOf(n) : null;
+ }
+ }
+
+ /**
+ * Returns the integer value of the number {@code n}, or {@code null} if {@code n}
+ * does not have an integer value.
+ *
+ *
{@code n} has an integer value if and only if the number it denotes can be represented
+ * as a signed 64-bit integer. That integer is then the integer value of {@code n}.
+ * In other words, if {@code n} is a float, it has an integer value if and only if
+ * it can be converted to a {@code long} without loss of precision.
+ *
+ * @param n number to convert to integer, must not be {@code null}
+ * @return a {@code Long} representing the integer value of {@code n}, or {@code null} if {@code
+ * n} does not have an integer value
+ * @throws NullPointerException if {@code n} is {@code null}
+ * @see LuaMathOperators#hasExactIntegerRepresentation(double)
+ */
+ public static Long integerValueOf(Number n) {
+ if (n instanceof Double || n instanceof Float) {
+ double d = n.doubleValue();
+ return LuaMathOperators.hasExactIntegerRepresentation(d) ? Long.valueOf((long) d) : null;
+ } else if (n instanceof Long) {
+ return (Long) n;
+ } else {
+ return Long.valueOf(n.longValue());
+ }
+ }
+
+ /**
+ * Returns the integer value of the number {@code n}, throwing
+ * a {@link NoIntegerRepresentationException} if {@code n} does not have an integer value.
+ *
+ *
This is a variant of {@link #integerValueOf(Number)}; the difference is that
+ * this method throws an exception rather than returning {@code null} to signal that
+ * {@code n} does not have an integer value, and that this method returns the unboxed
+ * integer value of {@code n} (as a {@code long}).
+ *
+ * @param n object to be converted to integer, must not be {@code null}
+ * @return integer value of {@code n}
+ * @throws NoIntegerRepresentationException if {@code n} does not have an integer value
+ * @throws NullPointerException if {@code n} is {@code null}
+ */
+ public static long toIntegerValue(Number n) {
+ Long l = integerValueOf(n);
+ if (l != null) {
+ return l.longValue();
+ } else {
+ throw new NoIntegerRepresentationException();
+ }
+ }
+
+ /**
+ * Returns the integer value of the object {@code o}, or {@code null} if {@code o}
+ * does not have an integer value.
+ *
+ *
The integer value of {@code o} is the integer value of its numerical value
+ * (see {@link #numericalValueOf(Object)}), when it exists.
+ *
+ * @param o object to be converted to integer, may be {@code null}
+ * @return a {@code Long} representing the integer value of {@code o}, or {@code null} if {@code
+ * o} does not have a integer value
+ * @see #integerValueOf(Number)
+ */
+ public static Long integerValueOf(Object o) {
+ Number n = numericalValueOf(o);
+ return n != null ? integerValueOf(n) : null;
+ }
+
+ /**
+ * Returns the integer value of the object {@code o}, throwing
+ * a {@link NoIntegerRepresentationException} if {@code o} does not have an integer value.
+ *
+ *
This is a variant of {@link #integerValueOf(Object)}; the difference is that
+ * this method throws an exception rather than returning {@code null} to signal that
+ * {@code o} does not have an integer value, and that this method returns the unboxed
+ * integer value of {@code o} (as a {@code long}).
+ *
+ * @param o object to be converted to integer, may be {@code null}
+ * @return integer value of {@code n}
+ * @throws NoIntegerRepresentationException if {@code o} does not have an integer value
+ */
+ public static long toIntegerValue(Object o) {
+ Long l = integerValueOf(o);
+ if (l != null) {
+ return l.longValue();
+ } else {
+ throw new NoIntegerRepresentationException();
+ }
+ }
+
+ /**
+ * Returns the float value of the number {@code n}.
+ *
+ *
The float value of {@code n} is its numerical value converted to a Lua float.
+ *
+ * @param n the number to convert to float, must not be {@code null}
+ * @return the float value of {@code n}, guaranteed to be non-{@code null}
+ * @throws NullPointerException if {@code n} is {@code null}
+ */
+ public static Double floatValueOf(Number n) {
+ return n instanceof Double
+ ? (Double) n
+ : Double.valueOf(n.doubleValue());
+ }
+
+ /**
+ * Returns the float value of the object {@code o}, or {@code null} if {@code o}
+ * does not have a float value.
+ *
+ *
The float value of {@code o} is the {@linkplain #floatValueOf(Number) float value}
+ * of its numerical value (see {@link #numericalValueOf(Object)}), when {@code o}
+ * has a numerical value.
+ *
+ * @param o the object to be converted to float, may be {@code null}
+ * @return a {@code Double} representing the float value of {@code o}, or {@code null} if {@code
+ * o} does not have a float value
+ */
+ public static Double floatValueOf(Object o) {
+ Number n = numericalValueOf(o);
+ return n != null ? floatValueOf(n) : null;
+ }
+
+ /**
+ * Returns the boolean value of the object {@code o}.
+ *
+ *
The boolean value of {@code o} is {@code false} if and only if {@code o} is nil
+ * (i.e., {@code null}) or false (i.e., a {@link Boolean} {@code b} such
+ * that {@code b.booleanValue() == false}).
+ *
+ * @param o object to convert to boolean, may be {@code null}
+ * @return {@code false} if {@code o} is nil or false, {@code true} otherwise
+ */
+ public static boolean booleanValueOf(Object o) {
+ return !(o == null || (o instanceof Boolean && !((Boolean) o).booleanValue()));
+ }
+
+ /**
+ * Returns the string value of the number {@code n}.
+ *
+ *
The string value of integers is the result of {@link LuaFormat#toByteString(long)}
+ * on their numerical value; similarly the string value of floats is the result
+ * of {@link LuaFormat#toByteString(double)} on their numerical value.
+ *
+ * @param n number to be converted to string, must not be {@code null}
+ * @return string value of {@code n}, guaranteed to be non-{@code null}
+ * @throws NullPointerException if {@code n} is {@code null}
+ */
+ public static ByteString stringValueOf(Number n) {
+ if (n instanceof Double || n instanceof Float) {
+ return LuaFormat.toByteString(n.doubleValue());
+ } else {
+ return LuaFormat.toByteString(n.longValue());
+ }
+ }
+
+ /**
+ * Returns the string value of the object {@code o}, or {@code null} if {@code o} does
+ * not have a string value.
+ *
+ *
If {@code o} is a string, that is the string value. If {@code o} is a number,
+ * returns the string value of that number (see {@link #stringValueOf(Number)}).
+ * Otherwise, {@code o} does not have a string value.
+ *
+ * @param o object to be converted to string, may be {@code null}
+ * @return string value of {@code o}, or {@code null} if {@code o} does not have a string value
+ */
+ public static ByteString stringValueOf(Object o) {
+ if (o instanceof ByteString) {
+ return (ByteString) o;
+ } else if (o instanceof Number) {
+ return stringValueOf((Number) o);
+ } else if (o instanceof String) {
+ return ByteString.of((String) o);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Converts the object {@code o} to a human-readable string format.
+ *
+ *
The conversion rules are the following:
+ *
+ *
+ *
If {@code o} is a {@code string} or {@code number}, returns the string value
+ * of {@code o};
+ *
if {@code o} is nil (i.e., {@code null}), returns {@code "nil"};
+ *
if {@code o} is a {@code boolean}, returns {@code "true"} if {@code o} is true
+ * or {@code "false"} if {@code o} is false;
+ *
otherwise, returns the string of the form {@code "TYPE: 0xHASH"}, where
+ * TYPE is the Lua type of {@code o}, and HASH
+ * is the {@link Object#hashCode()} of {@code o}
+ * in hexadecimal format.
+ *
+ *
+ *
+ *
Note that this method ignores the object's {@code toString()} method
+ * and its {@code __tostring} metamethod.
+ *
+ * @param o the object to be converted to string, may be {@code null}
+ * @return human-readable string representation of {@code o}
+ * @see #stringValueOf(Object)
+ */
+ public static ByteString toHumanReadableString(Object o) {
+ if (o == null) {
+ return LuaFormat.NIL;
+ } else if (o instanceof ByteString) {
+ return (ByteString) o;
+ } else if (o instanceof Number) {
+ return stringValueOf((Number) o);
+ } else if (o instanceof Boolean) {
+ return LuaFormat.toByteString(((Boolean) o).booleanValue());
+ } else if (o instanceof String) {
+ return ByteString.of((String) o);
+ } else {
+ return ByteString.of(String.format("%s: %#010x",
+ PlainValueTypeNamer.INSTANCE.typeNameOf(o),
+ o.hashCode()));
+ }
+ }
+
+ /**
+ * Converts a {@code Throwable} {@code t} to an error object.
+ *
+ *
If {@code t} is a {@link LuaRuntimeException}, the result of this operation
+ * is the result of its {@link LuaRuntimeException#getErrorObject()}. Otherwise,
+ * the result is {@link Throwable#getMessage()}.
+ *
+ * @param t throwable to convert to error object, must not be {@code null}
+ * @return error object represented by {@code t}
+ * @throws NullPointerException if {@code t} is {@code null}
+ */
+ public static Object toErrorObject(Throwable t) {
+ if (t instanceof LuaRuntimeException) {
+ return ((LuaRuntimeException) t).getErrorObject();
+ } else {
+ return t.getMessage();
+ }
+ }
+
+ /**
+ * Converts a {@code Throwable} {@code t} to an error message (a byte string).
+ *
+ *
This is equivalent to converting the error object retrieved from {@code t} by
+ * {@link #toErrorObject(Throwable)} to a byte string using
+ * {@link #stringValueOf(Object)}.
+ *
+ *
If the error object does not have a string value, returns {@code "(null)"}.
+ *
+ * @param t throwable to convert to error message, must not be {@code null}
+ * @return error message of {@code t}, or {@code "(null)"} if {@code t} does not have a string
+ * error message
+ * @throws NullPointerException if {@code t} is {@code null}
+ */
+ public static ByteString toErrorMessage(Throwable t) {
+ ByteString m = Conversions.stringValueOf(toErrorObject(t));
+ return m != null ? m : NULL_ERROR_MESSAGE;
+ }
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/LuaFormat.java b/luna/src/main/java/org/classdump/luna/LuaFormat.java
new file mode 100644
index 00000000..a0baaefa
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/LuaFormat.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Static methods for parsing and generating lexical strings following the Lua lexical
+ * conventions.
+ */
+public final class LuaFormat {
+
+ /**
+ * The byte string representation of nil.
+ */
+ public static final ByteString NIL = ByteString.constOf("nil");
+ /**
+ * The byte string representation of true.
+ */
+ public static final ByteString TRUE = ByteString.constOf("true");
+ /**
+ * The byte string representation of false.
+ */
+ public static final ByteString FALSE = ByteString.constOf("false");
+ /**
+ * The byte string representation of infinity.
+ */
+ public static final ByteString INF = ByteString.constOf("inf");
+ /**
+ * The byte string representation of NaN.
+ */
+ public static final ByteString NAN = ByteString.constOf("nan");
+ /**
+ * Byte string representation of the Lua {@code nil} type.
+ */
+ public static final ByteString TYPENAME_NIL = NIL;
+ /**
+ * Byte string representation of the Lua {@code boolean} type.
+ */
+ public static final ByteString TYPENAME_BOOLEAN = ByteString.constOf("boolean");
+ /**
+ * Byte string representation of the Lua {@code number} type.
+ */
+ public static final ByteString TYPENAME_NUMBER = ByteString.constOf("number");
+ /**
+ * Byte string representation of the Lua {@code string} type.
+ */
+ public static final ByteString TYPENAME_STRING = ByteString.constOf("string");
+ /**
+ * Byte string representation of the Lua {@code table} type.
+ */
+ public static final ByteString TYPENAME_TABLE = ByteString.constOf("table");
+ /**
+ * Byte string representation of the Lua {@code function} type.
+ */
+ public static final ByteString TYPENAME_FUNCTION = ByteString.constOf("function");
+ /**
+ * Byte string representation of the Lua {@code userdata} type.
+ */
+ public static final ByteString TYPENAME_USERDATA = ByteString.constOf("userdata");
+ /**
+ * Byte string representation of the Lua {@code thread} type.
+ */
+ public static final ByteString TYPENAME_THREAD = ByteString.constOf("thread");
+ /**
+ * Byte string representation of the Lua {@code lua object} type.
+ */
+ public static final ByteString TYPENAME_LUAOBJECT = ByteString.constOf("luaobject");
+ /**
+ * The '\a' character.
+ */
+ public static final char CHAR_BELL = 0x07;
+ /**
+ * The '\v' character.
+ */
+ public static final char CHAR_VERTICAL_TAB = 0x0b;
+ private static final ByteString NEG_INF = ByteString.constOf("-" + INF);
+ private static final Set keywords;
+
+ static {
+ Set ks = new HashSet<>();
+ Collections.addAll(ks,
+ "and", "break", "do", "else", "elseif", "end", "false", "for",
+ "function", "goto", "if", "in", "local", "nil", "not", "or",
+ "repeat", "return", "then", "true", "until", "while");
+ keywords = Collections.unmodifiableSet(ks);
+ }
+
+ private LuaFormat() {
+ // not to be instantiated
+ }
+
+ /**
+ * Returns the Lua format string representation of the boolean value {@code b}.
+ *
+ *
Note: this method returns a {@code java.lang.String}. In order to
+ * obtain a byte string, use {@link #toByteString(boolean)} rather than
+ * wrapping the result of this method using {@link ByteString#of(String)}.
+ *
+ * @param b the boolean value
+ * @return string representation of {@code b}
+ */
+ public static String toString(boolean b) {
+ return toByteString(b).toString();
+ }
+
+ /**
+ * Returns the Lua format byte string representation of the boolean value {@code b}
+ * as a byte string.
+ *
+ * @param b the boolean value
+ * @return byte string representation of {@code b}
+ */
+ public static ByteString toByteString(boolean b) {
+ return b ? TRUE : FALSE;
+ }
+
+ /**
+ * Returns the Lua format string representation of the integer value {@code l}.
+ *
+ *
Note: this method returns a {@code java.lang.String}. In order to
+ * obtain a byte string, use {@link #toByteString(long)} rather than
+ * wrapping the result of this method using {@link ByteString#of(String)}.
+ *
+ * @param l the integer value
+ * @return string representation of {@code l}
+ */
+ public static String toString(long l) {
+ return Long.toString(l);
+ }
+
+ /**
+ * Returns the Lua format byte string representation of the integer value {@code l}.
+ *
+ * @param l the integer value
+ * @return byte string representation of {@code l}
+ */
+ public static ByteString toByteString(long l) {
+ return ByteString.of(toString(l));
+ }
+
+ /**
+ * Returns the Lua format string representation of the float value {@code f}.
+ *
+ *
Note: this method returns a {@code java.lang.String}. In order to
+ * obtain a byte string, use {@link #toByteString(long)} rather than
+ * wrapping the result of this method using {@link ByteString#of(String)}.
+ *
+ * @param f the float value
+ * @return string representation of {@code f}
+ */
+ public static String toString(double f) {
+ return toByteString(f).toString();
+ }
+
+ private static ByteString finiteDoubleToByteString(double f) {
+ // f assumed not to be NaN or infinite
+ // TODO: check precision used in Lua
+ // TODO: don't go via java.lang.String
+ String s = Double.toString(f).toLowerCase();
+ return ByteString.of(s);
+ }
+
+ /**
+ * Returns the Lua format byte string representation of the float value {@code f}.
+ *
+ * @param f the float value
+ * @return byte string representation of {@code f}
+ */
+ public static ByteString toByteString(double f) {
+ if (Double.isNaN(f)) {
+ return NAN;
+ } else if (Double.isInfinite(f)) {
+ return f > 0 ? INF : NEG_INF;
+ } else {
+ return finiteDoubleToByteString(f);
+ }
+ }
+
+ private static int hexValue(int c) {
+ if (c >= '0' && c <= '9') {
+ return c - (int) '0';
+ } else if (c >= 'a' && c <= 'f') {
+ return 10 + c - (int) 'a';
+ } else if (c >= 'A' && c <= 'F') {
+ return 10 + c - (int) 'A';
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Parses the string {@code s} as an integer according to the Lua lexer rules.
+ * When {@code s} is not an integer numeral, throws a {@link NumberFormatException}.
+ *
+ *
This method ignores leading and trailing whitespace in {@code s}.
+ *
+ * @param s string to be parsed, must not be {@code null}
+ * @return the integer value represented by {@code s}
+ * @throws NullPointerException if {@code s} is {@code null}
+ * @throws NumberFormatException if {@code s} is not a valid Lua format string representing an
+ * integer value
+ */
+ public static long parseInteger(String s) throws NumberFormatException {
+ s = s.trim();
+ if (s.startsWith("0x") || s.startsWith("0X")) {
+ long l = 0;
+ int from = Math.max(2, s.length() - 16);
+
+ for (int idx = 2; idx < from; idx++) {
+ if (hexValue(s.charAt(idx)) < 0) {
+ throw new NumberFormatException("Illegal character #" + idx + " in \"" + s + "\"");
+ }
+ }
+
+ // only take the last 16 characters of the string for the value
+ for (int idx = Math.max(2, s.length() - 16); idx < s.length(); idx++) {
+ int hex = hexValue(s.charAt(idx));
+ if (hex < 0) {
+ throw new NumberFormatException("Illegal character #" + idx + " in \"" + s + "\"");
+ }
+ l = l << 4 | hex;
+ }
+
+ return l;
+ } else {
+ return Long.parseLong(s);
+ }
+ }
+
+ /**
+ * Parses the string {@code s} as a float according to the Lua lexer rules.
+ * When {@code s} is not a float numeral, throws a {@link NumberFormatException}.
+ *
+ *
This method ignores leading and trailing whitespace in {@code s}.
+ *
+ * @param s the string to be parsed, must not be {@code null}
+ * @return the float value represented by {@code s}
+ * @throws NullPointerException if {@code s} is {@code null}
+ * @throws NumberFormatException if {@code s} is not a valid Lua format string representing a
+ * float value
+ */
+ public static double parseFloat(String s) throws NumberFormatException {
+ try {
+ return Double.parseDouble(s);
+ } catch (NumberFormatException e0) {
+ // might be missing the trailing exponent for hex floating point constants
+ try {
+ return Double.parseDouble(s.trim() + "p0");
+ } catch (NumberFormatException e1) {
+ throw new NumberFormatException("Not a number: " + s);
+ }
+ }
+ }
+
+ /**
+ * Parses {@code s} as an integer following the Lua lexer rules. When {@code s} is
+ * an integer numeral, returns its value boxed as a {@link Long}. Otherwise, returns
+ * {@code null}.
+ *
+ *
This is a variant of {@link #parseInteger(String)} that signals invalid input
+ * by returning {@code null} rather than throwing a {@code NumberFormatException}.
+ *
+ * @param s the string to be parsed, must not be {@code null}
+ * @return the (boxed) integer value represented by {@code s} if {@code s} is an integer numeral;
+ * {@code null} otherwise
+ * @throws NullPointerException if {@code s} is {@code null}
+ */
+ public static Long tryParseInteger(String s) {
+ try {
+ return parseInteger(s);
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Parses {@code s} as a float following the Lua lexer rules. When {@code s} is
+ * a float numeral, returns its value boxed as a {@link Double}. Otherwise, returns
+ * {@code null}.
+ *
+ *
This is a variant of {@link #parseFloat(String)} that signals invalid input
+ * by returning {@code null} rather than throwing a {@code NumberFormatException}.
+ *
+ * @param s the string to be parsed, must not be {@code null}
+ * @return the (boxed) float value represented by {@code s} if {@code s} is an float numeral;
+ * {@code null} otherwise
+ * @throws NullPointerException if {@code s} is {@code null}
+ */
+ public static Double tryParseFloat(String s) {
+ try {
+ return parseFloat(s);
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Parses {@code s} as a number following the Lua lexer rules. When {@code s} is
+ * a numeral, returns its value boxed either as a {@link Long} (for integer numerals)
+ * or a {@link Double} (for float numerals). Otherwise, returns {@code null}.
+ *
+ *
Note an integer numeral is also a float numeral, but not all float numerals are
+ * integer numerals. This method returns the "most canonical" representation of the numeric
+ * value represented by {@code s}: it first tries to parse {@code s} as an integer,
+ * attempting to parse {@code s} as a float only when {@code s} is not an integer numeral.
+ *
+ * @param s the string to be parsed, must not be {@code null}
+ * @return the numeric value represented by {@code s}, or {@code null} if {@code s} is not a
+ * numeral
+ */
+ public static Number tryParseNumeral(String s) {
+ Long l = tryParseInteger(s);
+ return l != null ? l : (Number) tryParseFloat(s);
+ }
+
+ private static boolean isASCIIPrintable(char c) {
+ // ASCII printable character range
+ return c >= 32 && c < 127;
+ }
+
+ private static int shortEscape(char c) {
+ switch (c) {
+ case CHAR_BELL:
+ return 'a';
+ case '\b':
+ return 'b';
+ case '\f':
+ return 'f';
+ case '\n':
+ return 'n';
+ case '\r':
+ return 'r';
+ case '\t':
+ return 't';
+ case CHAR_VERTICAL_TAB:
+ return 'v';
+ case '"':
+ return '"';
+ default:
+ return -1;
+ }
+ }
+
+ private static char toHex(int i) {
+ // i must be between 0x0 and 0xf
+ return i < 0xa ? (char) ((int) '0' + i) : (char) ((int) 'a' + i - 0xa);
+ }
+
+ /**
+ * Returns a string {@code esc} formed from the character sequence {@code s} such that
+ * when {@code esc} is read by a Lua lexer as a string literal, it evaluates to a string equal
+ * to {@code s}. The resulting string is enclosed in double quotes ({@code "}).
+ *
+ * @param s the character sequence to escape, must not be {@code null}
+ * @return a Lua string literal representing {@code s}
+ * @throws NullPointerException if {@code s} is {@code null}
+ */
+ public static String escape(CharSequence s) {
+ Objects.requireNonNull(s);
+
+ StringBuilder bld = new StringBuilder();
+ bld.append('"');
+
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+
+ if (c != '\\' && c != '"' && isASCIIPrintable(c)) {
+ bld.append(c);
+ } else {
+ // escaping
+ bld.append('\\');
+
+ int esc = shortEscape(c);
+
+ if (esc != -1) {
+ bld.append((char) esc);
+ } else {
+ if ((int) c <= 0xff) {
+ bld.append('x');
+ bld.append(toHex(((int) c >>> 8) & 0xf));
+ bld.append(toHex((int) c & 0xf));
+ } else {
+ bld.append(Integer.toString((int) c));
+ }
+ }
+ }
+ }
+
+ bld.append('"');
+ return bld.toString();
+ }
+
+ /**
+ * Returns a string {@code esc} formed from the byte string {@code s} such that
+ * when {@code esc} is read by a Lua lexer as a string literal, it evaluates to
+ * a byte string equal to {@code s}. The resulting string is enclosed in double quotes
+ * ({@code "}).
+ *
+ * @param byteString the byte sequence sequence to escape, must not be {@code null}
+ * @return a Lua string literal representing {@code s}
+ * @throws NullPointerException if {@code s} is {@code null}
+ */
+ public static String escape(ByteString byteString) {
+ return escape(byteString.toRawString());
+ }
+
+ /**
+ * Returns {@code true} iff the string {@code s} is a keyword in Lua.
+ *
+ *
A keyword in Lua is one of the following strings:
+ * {@code "and"}, {@code "break"}, {@code "do"}, {@code "else"}, {@code "elseif"},
+ * {@code "end"}, {@code "false"}, {@code "for"}, {@code "function"}, {@code "goto"},
+ * {@code "if"}, {@code "in"}, {@code "local"}, {@code "nil"}, {@code "not"},
+ * {@code "or"}, {@code "repeat"}, {@code "return"}, {@code "then"}, {@code "true"},
+ * {@code "until"}, {@code "while"}.
+ *
+ * @param s the string to be examined, may be {@code null}
+ * @return {@code true} if {@code s} is a Lua keyword; {@code false} otherwise
+ */
+ public static boolean isKeyword(String s) {
+ return s != null && keywords.contains(s);
+ }
+
+ /**
+ * Returns {@code true} iff the string {@code s} is a valid Lua name.
+ *
+ *
According to §3.1 of the Lua Reference Manual,
+ *
+ *
+ * Names (also called identifiers) in Lua can be any string of letters, digits,
+ * and underscores, not beginning with a digit and not being a reserved word.
+ *
+ *
+ *
This implementation treats letters as characters in the ranges
+ * {@code 'a'}...{@code 'z'} and {@code 'A'}...{@code 'Z'}, and numbers as characters in
+ * the range {@code '0'}...{@code '9'}.
+ *
+ * @param s the string to be checked for being a valid name, may be {@code null}
+ * @return {@code true} if {@code s} is a valid name in Lua; {@code false} otherwise
+ */
+ public static boolean isValidName(String s) {
+ if (s == null || s.isEmpty() || isKeyword(s)) {
+ return false;
+ }
+
+ char c = s.charAt(0);
+
+ if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c != '_')) {
+ return false;
+ }
+
+ for (int i = 1; i < s.length(); i++) {
+ c = s.charAt(i);
+ if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c != '_') && (c < '0' || c > '9')) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/LuaMathOperators.java b/luna/src/main/java/org/classdump/luna/LuaMathOperators.java
new file mode 100644
index 00000000..99d47ecb
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/LuaMathOperators.java
@@ -0,0 +1,600 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+/**
+ * A collection of static methods for performing the equivalents of Lua's arithmetic,
+ * bitwise and numerical comparison operations.
+ *
+ *
This class and its methods exploits the isomorphism between Lua integers and
+ * Java {@code long} on the one hand, and between Lua floats and Java {@code double}
+ * on the other.
+ *
+ *
For each operation, there are as many variants in the form of a static method
+ * as there are valid type combinations. While all arithmetic operations
+ * are defined in two variants (for two {@code long}s and two {@code double}s, e.g.
+ * {@link #idiv(long, long)} and {@link #idiv(double, double)}), bitwise operations
+ * have single variants (taking two {@code long}s, e.g. {@link #band(long, long)}),
+ * and numerical comparison operations have four variants (for argument type combination).
+ *
+ *
It is the task of a Lua implementation to select the appropriate method and
+ * supply it with the required values; however, note that the method selection is well-behaved
+ * under the type conversion rules of the Java programming language.
+ */
+public final class LuaMathOperators {
+
+ private static final double MAX_LONG_AS_DOUBLE = (double) Long.MAX_VALUE;
+
+ // Arithmetic operators
+ private static final double MIN_LONG_AS_DOUBLE = (double) Long.MIN_VALUE;
+
+ private LuaMathOperators() {
+ // not to be instantiated or extended
+ }
+
+ /**
+ * Returns the result of the addition of two {@code long}s, equivalent to the addition
+ * of two integers in Lua.
+ *
+ * @param a first addend
+ * @param b second addend
+ * @return the value of the Lua expression {@code (a + b)}, where {@code a} and {@code b} are Lua
+ * integers
+ */
+ public static long add(long a, long b) {
+ return a + b;
+ }
+
+ /**
+ * Returns the result of the addition of two {@code double}s, equivalent to the addition
+ * of two floats in Lua.
+ *
+ * @param a first addend
+ * @param b second addend
+ * @return the value of the Lua expression {@code (a + b)}, where {@code a} and {@code b} are Lua
+ * floats
+ */
+ public static double add(double a, double b) {
+ return a + b;
+ }
+
+ /**
+ * Returns the result of the subtraction of two {@code long}s, equivalent to the subtraction
+ * of two integers in Lua.
+ *
+ * @param a the minuend
+ * @param b the subtrahend
+ * @return the value of the Lua expression {@code (a - b)}, where {@code a} and {@code b} are Lua
+ * integers
+ */
+ public static long sub(long a, long b) {
+ return a - b;
+ }
+
+ /**
+ * Returns the result of the subtraction of two {@code double}s, equivalent to the subtraction
+ * of two floats in Lua.
+ *
+ * @param a the minuend
+ * @param b the subtrahend
+ * @return the value of the Lua expression {@code (a - b)}, where {@code a} and {@code b} are Lua
+ * floats
+ */
+ public static double sub(double a, double b) {
+ return a - b;
+ }
+
+ /**
+ * Returns the result of the multiplication of two {@code long}s, equivalent to
+ * the multiplication of two integers in Lua.
+ *
+ * @param a first factor
+ * @param b second factor
+ * @return the value of the Lua expression {@code (a * b)}, where {@code a} and {@code b} are Lua
+ * integers
+ */
+ public static long mul(long a, long b) {
+ return a * b;
+ }
+
+ /**
+ * Returns the result of the multiplication of two {@code double}s, equivalent to
+ * the multiplication of two floats in Lua.
+ *
+ * @param a first factor
+ * @param b second factor
+ * @return the value of the Lua expression {@code (a * b)}, where {@code a} and {@code b} are Lua
+ * floats
+ */
+ public static double mul(double a, double b) {
+ return a * b;
+ }
+
+ /**
+ * Returns the result of the division of two {@code long}s, equivalent to the float
+ * division of two integers in Lua. The result is always a {@code double}; when {@code b}
+ * is zero, the result is NaN.
+ *
+ *
Note that this behaviour differs from the standard Java integer division.
+ *
+ * @param a the dividend
+ * @param b the divisor
+ * @return the value of the Lua expression {@code (a / b)}, where {@code a} and {@code b} are Lua
+ * integers
+ */
+ public static double div(long a, long b) {
+ return ((double) a) / ((double) b);
+ }
+
+ /**
+ * Returns the result of the division of two {@code double}s, equivalent to the float
+ * division of two floats in Lua.
+ *
+ * @param a the dividend
+ * @param b the divisor
+ * @return the value of the Lua expression {@code (a / b)}, where {@code a} and {@code b} are Lua
+ * floats
+ */
+ public static double div(double a, double b) {
+ return a / b;
+ }
+
+ /**
+ * Returns the floor modulus of two {@code long}s, equivalent to the modulus
+ * of two integers in Lua.
+ *
+ *
Note that in Lua,
+ *
+ *
+ * Modulo is defined as the remainder of a division that rounds the quotient
+ * towards minus infinity (floor division).
+ *
+ *
+ *
This definition is not equivalent to the standard Java definition of modulo
+ * (which is the remainder of a division rounding toward zero).
+ *
+ * @param a the dividend
+ * @param b the divisor
+ * @return the value of the Lua expression {@code (a % b)}, where {@code a} and {@code b} are Lua
+ * integers
+ * @throws ArithmeticException if {@code b} is zero
+ */
+ public static long mod(long a, long b) {
+ // Note: in JDK 8+, Math.floorMod could be used
+ if (b == 0) {
+ throw new ArithmeticException("attempt to perform 'n%0'");
+ } else {
+ return a - b * (long) Math.floor((double) a / (double) b);
+ }
+ }
+
+ /**
+ * Returns the floor modulus of two {@code double}s, equivalent to the modulus
+ * of two floats in Lua.
+ *
+ *
Note that in Lua,
+ *
+ *
+ * Modulo is defined as the remainder of a division that rounds the quotient
+ * towards minus infinity (floor division).
+ *
+ *
+ *
This definition is not equivalent to the standard Java definition of modulo
+ * (which is the remainder of a division rounding toward zero).
+ *
+ * @param a the dividend
+ * @param b the divisor
+ * @return the value of the Lua expression {@code (a % b)}, where {@code a} and {@code b} are Lua
+ * floats
+ */
+ public static double mod(double a, double b) {
+ return b != 0 ? a - b * Math.floor(a / b) : Double.NaN;
+ }
+
+ /**
+ * Returns the result of floor division of two {@code long}s, equivalent to the Lua
+ * floor division of two integers.
+ *
+ *
In Lua,
+ *
+ *
+ * Floor division (//) is a division that rounds the quotient towards minus infinity,
+ * that is, the floor of the division of its operands.
+ *
+ *
+ * @param a the dividend
+ * @param b the divisor
+ * @return the value of the Lua expression {@code (a // b)} where {@code a} and {@code b} are Lua
+ * integers
+ * @throws ArithmeticException if {@code b} is zero
+ */
+ public static long idiv(long a, long b) {
+ if (b == 0) {
+ throw new ArithmeticException("attempt to divide by zero");
+ } else {
+ long q = a / b;
+ return q * b == a || (a ^ b) >= 0 ? q : q - 1;
+ }
+ }
+
+ /**
+ * Returns the result of floor division of two {@code double}s, equivalent to the Lua
+ * floor division of two floats:
+ *
+ *
In Lua,
+ *
+ *
+ * Floor division (//) is a division that rounds the quotient towards minus infinity,
+ * that is, the floor of the division of its operands.
+ *
+ *
+ * @param a the dividend
+ * @param b the divisor
+ * @return the value of the Lua expression {@code (a // b)} where {@code a} and {@code b} are Lua
+ * floats
+ */
+ public static double idiv(double a, double b) {
+ return Math.floor(a / b);
+ }
+
+ /**
+ * Returns the result of the exponentiation of two {@code long}s, equivalent to the Lua
+ * exponentiation of two integers. Note that the resulting value is a {@code double}.
+ *
+ * @param a the base
+ * @param b the exponent
+ * @return the value of the Lua expression {@code (a ^ b)}, where {@code a} and {@code b} are Lua
+ * integers
+ */
+ public static double pow(long a, long b) {
+ return Math.pow((double) a, (double) b);
+ }
+
+ /**
+ * Returns the result of the exponentiation of two {@code double}s, equivalent to Lua
+ * exponentiation of two floats.
+ *
+ * @param a the base
+ * @param b the exponent
+ * @return the value of the Lua expression {@code (a ^ b)}, where {@code a} and {@code b} are Lua
+ * floats
+ */
+ public static double pow(double a, double b) {
+ return Math.pow(a, b);
+ }
+
+ // Bitwise operators
+
+ /**
+ * Returns the result of the (arithmetic) negation of a {@code long}, equivalent to
+ * the Lua unary minus on an integer.
+ *
+ * @param n the operand
+ * @return the value of the Lua expression {@code (-n)}, where {@code n} is a Lua integer
+ */
+ public static long unm(long n) {
+ return -n;
+ }
+
+ /**
+ * Returns the result of the (arithmetic) negation of a {@code long}, equivalent to
+ * the Lua unary minus on a float.
+ *
+ * @param n the operand
+ * @return the value of the Lua expression {@code (-n)}, where {@code n} is a Lua float
+ */
+ public static double unm(double n) {
+ return -n;
+ }
+
+ /**
+ * Returns the result of the bitwise AND of two {@code long}s, equivalent to the Lua
+ * bitwise AND of two integers.
+ *
+ * @param a the first operand
+ * @param b the second operand
+ * @return the value of the Lua expression {@code (a & b)}, where {@code a} and {@code b} are Lua
+ * integers
+ */
+ public static long band(long a, long b) {
+ return a & b;
+ }
+
+ /**
+ * Returns the result of the bitwise OR of two {@code long}s, equivalent to the Lua
+ * bitwise OR of two integers.
+ *
+ * @param a the first operand
+ * @param b the second operand
+ * @return the value of the Lua expression {@code (a | b)}, where {@code a} and {@code b} are Lua
+ * integers
+ */
+ public static long bor(long a, long b) {
+ return a | b;
+ }
+
+ /**
+ * Returns the result of the bitwise exclusive OR of two {@code long}s,
+ * equivalent to the Lua bitwise exclusive OR of two integers.
+ *
+ * @param a the first operand
+ * @param b the second operand
+ * @return the value of the Lua expression {@code (a ~ b)}, where {@code a} and {@code b} are Lua
+ * integers
+ */
+ public static long bxor(long a, long b) {
+ return a ^ b;
+ }
+
+ /**
+ * Returns the result of the (bitwise) logical left shift, equivalent to the Lua bitwise
+ * left shift on two integers. Vacant bits are filled with zeros. When {@code b} is negative,
+ * the result is equal to the result of a right shift by {@code -b}.
+ *
+ *
Note that Lua's behaviour differs from Java's {@code <<} operator in that if
+ * {@code b} is greater than 64, the result is zero, as all bits have been shifted out.
+ *
+ * @param a the left-hand side operand
+ * @param b the right-hand side operand (shift distance)
+ * @return the value of the Lua expression {@code (a << b)}, where {@code a} and {@code b} are Lua
+ * integers
+ */
+ public static long shl(long a, long b) {
+ return b < 0 ? shr(a, -b) : (b < 64 ? a << b : 0);
+ }
+
+ // Numerical comparison operators
+
+ /**
+ * Returns the result of the (bitwise) logical right shift, equivalent to the Lua bitwise
+ * right shift on two integers. Vacant bits are filled with zeros. When {@code b} is negative,
+ * the result is equal to the result of a left shift by {@code -b}.
+ *
+ *
Note that Lua's behaviour differs from Java's {@code >>>} operator in that if
+ * {@code b} is greater than 64, the result is zero, as all bits have been shifted out.
+ *
+ * @param a the left-hand side operand
+ * @param b the right-hand side operand (shift distance)
+ * @return the value of the Lua expression {@code (a << b)}, where {@code a} and {@code b} are Lua
+ * integers
+ */
+ public static long shr(long a, long b) {
+ return b < 0 ? shl(a, -b) : (b < 64 ? a >>> b : 0);
+ }
+
+ /**
+ * Returns the result of the bitwise unary NOT of a {@code long}, equivalent to the Lua
+ * bitwise unary NOT on an integer.
+ *
+ * @param n the operand
+ * @return the value of the Lua expression {@code (~b)}, where {@code n} is a Lua integer
+ */
+ public static long bnot(long n) {
+ return ~n;
+ }
+
+ /**
+ * Returns {@code true} iff the {@code double} {@code d} can be represented by
+ * a {@code long} without the loss of precision, i.e. if {@code ((long) d)}
+ * and {@code d} denote the same mathematical value.
+ *
+ * @param d the {@code double} in question
+ * @return {@code true} iff {@code d} can be represented by a {@code long} without the loss of
+ * precision
+ */
+ public static boolean hasExactIntegerRepresentation(double d) {
+ long l = (long) d;
+ return (double) l == d && l != Long.MAX_VALUE;
+ }
+
+ /**
+ * Returns {@code true} iff the {@code long} {@code l} can be represented by
+ * a {@code double} without the loss of precision, i.e. if {@code ((double) l)}
+ * and {@code l} denote the same mathematical value.
+ *
+ * @param l the {@code long} in question
+ * @return {@code true} iff {@code l} can be represented by a {@code double} without the loss of
+ * precision
+ */
+ public static boolean hasExactFloatRepresentation(long l) {
+ double d = (double) l;
+ return (long) d == l && l != Long.MAX_VALUE;
+ }
+
+ /**
+ * Returns {@code true} iff the {@code long}s {@code a} and {@code b} denote
+ * the same mathematical value. This is equivalent to the Lua numerical equality
+ * comparison of two integers.
+ *
+ * @param a a {@code long}
+ * @param b a {@code long} to be compared with {@code a} for mathematical equality
+ * @return {@code true} iff the Lua expression {@code (a == b)}, where {@code a} and {@code b} are
+ * Lua integers, would evaluate to (Lua) true
+ */
+ public static boolean eq(long a, long b) {
+ return a == b;
+ }
+
+ /**
+ * Returns {@code true} iff the {@code long} {@code a} denotes the same mathematical
+ * value as the {@code double} {@code b}. This is equivalent to the Lua numerical
+ * equality comparison of an integer and a float.
+ *
+ * @param a a {@code long}
+ * @param b a {@code double} to be compared with {@code a} for mathematical equality
+ * @return {@code true} iff the Lua expression {@code (a == b)}, where {@code a} is a Lua integer
+ * and {@code b} is a Lua float, would evaluate to (Lua) true
+ */
+ public static boolean eq(long a, double b) {
+ return hasExactFloatRepresentation(a) && (double) a == b;
+ }
+
+ /**
+ * Returns {@code true} iff the {@code double} {@code a} denotes the same mathematical
+ * value as the {@code long} {@code b}. This is equivalent to the Lua numerical equality
+ * comparison of a float and an integer.
+ *
+ * @param a a {@code double}
+ * @param b a {@code long} to be compared with {@code a} for mathematical equality
+ * @return {@code true} iff the Lua expression {@code (a == b)}, where {@code a} is a Lua float
+ * and {@code b} is a Lua integer, would evaluate to (Lua) true
+ */
+ public static boolean eq(double a, long b) {
+ return hasExactFloatRepresentation(b) && a == (double) b;
+ }
+
+ /**
+ * Returns {@code true} iff the {@code double}s {@code a} and {@code b} denote
+ * the same mathematical value. This is equivalent to the Lua numerical equality
+ * comparison of two doubles.
+ *
+ * @param a a {@code double}
+ * @param b a {@code double} to be compared with {@code a} for mathematical equality
+ * @return {@code true} iff the Lua expression {@code (a == b)}, where {@code a} and {@code b} are
+ * Lua floats, would evaluate to (Lua) true
+ */
+ public static boolean eq(double a, double b) {
+ return a == b;
+ }
+
+ /**
+ * Returns {@code true} iff the mathematical value of the {@code long} {@code a}
+ * is lesser than the mathematical value of the {@code long} {@code b}. This is equivalent
+ * to the Lua numerical lesser-than comparison of two integers.
+ *
+ * @param a a {@code long}
+ * @param b a {@code long} to be compared with {@code a}
+ * @return {@code true} iff the Lua expression {@code (a < b)}, where {@code a} and {@code b} are
+ * Lua integers, would evaluate to (Lua) true
+ */
+ public static boolean lt(long a, long b) {
+ return a < b;
+ }
+
+ /**
+ * Returns {@code true} iff the mathematical value of the {@code long} {@code a}
+ * is lesser than the mathematical value of the {@code double} {@code b}. This
+ * is equivalent to the Lua numerical lesser-than comparison of an integer and a float.
+ *
+ * @param a a {@code long}
+ * @param b a {@code double} to be compared with {@code a}
+ * @return {@code true} iff the Lua expression {@code (a < b)}, where {@code a} is a Lua integer
+ * and {@code b} is a Lua float, would evaluate to (Lua) true
+ */
+ public static boolean lt(long a, double b) {
+ if (hasExactFloatRepresentation(a)) {
+ return (double) a < b;
+ } else {
+ return !Double.isNaN(b) && b > MIN_LONG_AS_DOUBLE && (b >= MAX_LONG_AS_DOUBLE
+ || a < (long) b);
+ }
+ }
+
+ /**
+ * Returns {@code true} iff the mathematical value of the {@code double} {@code a}
+ * is lesser than the mathematical value of the {@code long} {@code b}. This
+ * is equivalent to the Lua numerical lesser-than comparison of a float and an integer.
+ *
+ * @param a a {@code double}
+ * @param b a {@code long} to be compared with {@code a}
+ * @return {@code true} iff the Lua expression {@code (a < b)}, where {@code a} is a Lua float and
+ * {@code b} is a Lua integer, would evaluate to (Lua) true
+ */
+ public static boolean lt(double a, long b) {
+ return !Double.isNaN(a) && !le(b, a);
+ }
+
+ /**
+ * Returns {@code true} iff the mathematical value of the {@code double} {@code a}
+ * is lesser than the mathematical value of the {@code double} {@code b}. This
+ * is equivalent to the Lua numerical lesser-than comparison of two floats.
+ *
+ * @param a a {@code double}
+ * @param b a {@code double} to be compared with {@code a}
+ * @return {@code true} iff the Lua expression {@code (a < b)}, where {@code a} and {@code b} are
+ * Lua floats, would evaluate to (Lua) true
+ */
+ public static boolean lt(double a, double b) {
+ return a < b;
+ }
+
+ /**
+ * Returns {@code true} iff the mathematical value of the {@code long} {@code a}
+ * is lesser than or equal to the mathematical value of the {@code long} {@code b}.
+ * This is equivalent to the Lua numerical lesser-than-or-equal comparison of
+ * two integers.
+ *
+ * @param a a {@code long}
+ * @param b a {@code long} to be compared with {@code a}
+ * @return {@code true} iff the Lua expression {@code (a <= b)}, where {@code a} and {@code b} are
+ * Lua integers, would evaluate to (Lua) true
+ */
+ public static boolean le(long a, long b) {
+ return a <= b;
+ }
+
+ /**
+ * Returns {@code true} iff the mathematical value of the {@code long} {@code a}
+ * is lesser than or equal to the mathematical value of the {@code double} {@code b}.
+ * This is equivalent to the Lua numerical lesser-than-or-equal comparison of
+ * an integer and a float.
+ *
+ * @param a a {@code long}
+ * @param b a {@code double} to be compared with {@code a}
+ * @return {@code true} iff the Lua expression {@code (a <= b)}, where {@code a} is a Lua integer
+ * and {@code b} is a Lua float, would evaluate to (Lua) true
+ */
+ public static boolean le(long a, double b) {
+ if (hasExactFloatRepresentation(a)) {
+ return (double) a <= b;
+ } else {
+ return !Double.isNaN(b) && b > MIN_LONG_AS_DOUBLE && (b >= MAX_LONG_AS_DOUBLE
+ || a <= (long) b);
+ }
+ }
+
+ /**
+ * Returns {@code true} iff the mathematical value of the {@code double} {@code a}
+ * is lesser than or equal to the mathematical value of the {@code long} {@code b}.
+ * This is equivalent to the Lua numerical lesser-than-or-equal comparison of
+ * a float and an integer.
+ *
+ * @param a a {@code double}
+ * @param b a {@code long} to be compared with {@code a}
+ * @return {@code true} iff the Lua expression {@code (a <= b)}, where {@code a} is a Lua float
+ * and {@code b} is a Lua integer, would evaluate to (Lua) true
+ */
+ public static boolean le(double a, long b) {
+ return !Double.isNaN(a) && !lt(b, a);
+ }
+
+ /**
+ * Returns {@code true} iff the mathematical value of the {@code double} {@code a}
+ * is lesser than or equal to the mathematical value of the {@code double} {@code b}.
+ * This is equivalent to the Lua numerical lesser-than-or-equal comparison of
+ * two floats.
+ *
+ * @param a a {@code double}
+ * @param b a {@code double} to be compared with {@code a}
+ * @return {@code true} iff the Lua expression {@code (a <= b)}, where {@code a} and {@code b} are
+ * Lua floats, would evaluate to (Lua) true
+ */
+ public static boolean le(double a, double b) {
+ return a <= b;
+ }
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/LuaObject.java b/luna/src/main/java/org/classdump/luna/LuaObject.java
new file mode 100644
index 00000000..ef3ba596
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/LuaObject.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+/**
+ * Base class of objects that have a metatable attached to them on a per-instance basis.
+ */
+public abstract class LuaObject {
+
+ /**
+ * Returns the metatable of this object, or {@code null} if this object does not have
+ * a metatable.
+ *
+ * @return this object's metatable, or {@code null} if this object does not have a metatable
+ */
+ public abstract Table getMetatable();
+
+ /**
+ * Sets the metatable of this object to {@code mt}. {@code mt} may be {@code null}:
+ * in that case, removes the metatable from this object.
+ *
+ *
Returns the metatable previously associated with this object (i.e., the metatable
+ * before the call of this method; possibly {@code null}).
+ *
+ * @param mt new metatable to attach to this object, may be {@code null}
+ * @return previous metatable associated with this object
+ */
+ public abstract Table setMetatable(Table mt);
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/LuaRuntimeException.java b/luna/src/main/java/org/classdump/luna/LuaRuntimeException.java
new file mode 100644
index 00000000..51dc158b
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/LuaRuntimeException.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+/**
+ * Base class for runtime exceptions that carry arbitrary error objects
+ * attached to them.
+ *
+ *
To retrieve the error object, use {@link #getErrorObject()}.
+ */
+public class LuaRuntimeException extends RuntimeException {
+
+ private final Object errorObject;
+
+ private LuaRuntimeException(Throwable cause, Object errorObject) {
+ super(cause);
+ this.errorObject = errorObject;
+ }
+
+ /**
+ * Constructs a new {@code LuaRuntimeException} with {@code errorObject} as its
+ * error object. {@code errorObject} may be {@code null}.
+ *
+ * @param errorObject the error object, may be {@code null}
+ */
+ public LuaRuntimeException(Object errorObject) {
+ this(null, errorObject);
+ }
+
+ /**
+ * Constructs a new {@code LuaRuntimeException} with {@code cause} as its cause.
+ *
+ *
When queried for the error object, invokes {@link Conversions#toErrorObject(Throwable)}
+ * on {@code cause}; when {@code cause} is {@code null}, then the error object
+ * is {@code null}.
+ *
+ * @param cause the cause of this error, may be {@code null}
+ */
+ public LuaRuntimeException(Throwable cause) {
+ this(cause, null);
+ }
+
+ /**
+ * Returns the error object attached to this exception converted to a string.
+ *
+ * @return error object converted to a string
+ */
+ @Override
+ public String getMessage() {
+ return getErrorLocation() + Conversions.toHumanReadableString(getErrorObject()).toString();
+ }
+
+ /**
+ * Returns the error object attached to this exception. The error object may be {@code null}.
+ *
+ * @return the error object attached to this exception (possibly {@code null})
+ */
+ public Object getErrorObject() {
+ Throwable cause = getCause();
+ if (cause != null) {
+ return Conversions.toErrorObject(cause);
+ } else {
+ return errorObject;
+ }
+ }
+
+ /**
+ * Returns the closest location in the Lua code when this exception was triggered.
+ *
+ * @return the location of this error in the Lua code, @{code file:line} or @{code unknown:-1} if
+ * it could not be determined
+ */
+ public String getErrorLocation() {
+ for (StackTraceElement stackTraceElement : getStackTrace()) {
+ if (stackTraceElement.getClassName().startsWith("luna_dynamic")) {
+ return stackTraceElement.getFileName() + ":" + stackTraceElement.getLineNumber() + ": ";
+ }
+ }
+ return "";
+ }
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/LuaType.java b/luna/src/main/java/org/classdump/luna/LuaType.java
new file mode 100644
index 00000000..2b0c2fcd
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/LuaType.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+import org.classdump.luna.runtime.Coroutine;
+import org.classdump.luna.runtime.LuaFunction;
+
+/**
+ * An enum representing a Lua type.
+ *
+ *
There are eight types in Lua ({@code nil}, {@code boolean}, {@code number}, {@code string},
+ * {@code function}, {@code userdata}, {@code thread} and {@code table}). In Luna,
+ * all Java object references are mapped to a Lua type according to the following list:
+ *
+ *
+ *
{@link #NIL} ... no explicit {@code nil} type; any (Java) {@code null} value is
+ * considered nil
+ *
{@link #BOOLEAN} ... instances of {@link Boolean java.lang.Boolean}
+ *
{@link #NUMBER} ... instances of {@link Number java.lang.Number}:
+ *
integer ... any other subclass of {@code java.lang.Number},
+ * with {@link Long java.lang.Long} being the canonical
+ * representation
+ *
+ *
+ *
{@link #STRING} ... instances of {@link String java.lang.String}
+ *
{@link #FUNCTION} ... {@link LuaFunction}
+ *
{@link #USERDATA}:
+ *
+ *
full userdata ... {@link Userdata}
+ *
light userdata ... instances of any class other than those mentioned
+ * in this list
+ *
+ *
+ *
{@link #THREAD} ... {@link Coroutine}
+ *
{@link #TABLE} ... {@link Table}
+ *
+ *
+ *
For numeric values, the canonical representation is the default, full-precision
+ * representation of the number as either a float or integer. To convert a number to its
+ * canonical value, use {@link Conversions#toCanonicalNumber(Number)}.
+ *
+ *
To retrieve the name of the type of a Lua value, use the {@link ValueTypeNamer}
+ * interface (for names based on the type only, without taking into account the {@code __name}
+ * metamethod, use {@link PlainValueTypeNamer}).
+ */
+public enum LuaType {
+
+ /**
+ * The Lua {@code nil} type, corresponding to {@code null} references.
+ */
+ NIL,
+
+ /**
+ * The Lua {@code boolean} type, corresponding to instances
+ * of {@link Boolean java.lang.Boolean}.
+ */
+ BOOLEAN,
+
+ /**
+ * The Lua {@code number} type, corresponding to instances of {@link Number java.lang.Number}.
+ *
+ *
Instances of {@link Double java.lang.Double} and {@link Float java.lang.Float}
+ * are mapped to Lua floats ({@code Double}s being the canonical representation). All other
+ * subclasses of {@code Number} are mapped to Lua integers, with {@link Long java.lang.Long}
+ * being the canonical representation.
+ */
+ NUMBER,
+
+ /**
+ * The Lua {@code string} type, corresponding to instances of {@link ByteString}
+ * and {@link String java.lang.String}.
+ */
+ STRING,
+
+ /**
+ * The Lua {@code function} type, corresponding to instances of {@link LuaFunction}.
+ */
+ FUNCTION,
+
+ /**
+ * The Lua {@code userdata} type, corresponding to instances of {@link Userdata} (for full
+ * userdata), or any other subclasses of {@link java.lang.Object} not mapped to a Lua
+ * type (for light userdata).
+ */
+ USERDATA,
+
+ /**
+ * The Lua {@code thread} type, corresponding to instances of the {@link Coroutine} class.
+ */
+ THREAD,
+
+ /**
+ * The Lua {@code table} type, corresponding to instances of {@link Table}.
+ */
+ TABLE;
+
+ /**
+ * Returns the Lua type of the object {@code o}.
+ *
+ * @param o the object to determine the type of, may be {@code null}
+ * @return the Lua type of {@code o}
+ */
+ public static LuaType typeOf(Object o) {
+ if (o == null) {
+ return LuaType.NIL;
+ } else if (o instanceof Boolean) {
+ return LuaType.BOOLEAN;
+ } else if (o instanceof Number) {
+ return LuaType.NUMBER;
+ } else if (o instanceof ByteString || o instanceof String) {
+ return LuaType.STRING;
+ } else if (o instanceof Table) {
+ return LuaType.TABLE;
+ } else if (o instanceof LuaFunction) {
+ return LuaType.FUNCTION;
+ } else if (o instanceof Coroutine) {
+ return LuaType.THREAD;
+ } else {
+ return LuaType.USERDATA;
+ }
+ }
+
+ /**
+ * Returns {@code true} iff {@code o} is nil.
+ *
+ *
{@code o} is nil if and only if {@code o} is {@code null}.
+ *
+ * @param o the object to test for being nil, may be {@code null}
+ * @return {@code true} iff {@code o} is nil
+ */
+ public static boolean isNil(Object o) {
+ return o == null;
+ }
+
+ /**
+ * Returns {@code true} iff {@code o} is a Lua boolean.
+ *
+ *
{@code o} is a Lua boolean if and only if {@code o} is an instance of
+ * {@link Boolean java.lang.Boolean}.
+ *
+ * @param o the object to test for being a boolean, may be {@code null}
+ * @return {@code true} iff {@code o} is a Lua boolean
+ */
+ public static boolean isBoolean(Object o) {
+ return o instanceof Boolean;
+ }
+
+ /**
+ * Returns {@code true} iff {@code o} is a Lua number.
+ *
+ *
{@code o} is a Lua number if and only if {@code o} is an instance of
+ * {@link Number java.lang.Number}.
+ *
+ * @param o the object to test for being a number, may be {@code null}
+ * @return {@code true} iff {@code o} is a Lua number
+ */
+ public static boolean isNumber(Object o) {
+ return o instanceof Number;
+ }
+
+ /**
+ * Returns {@code true} iff {@code o} is a Lua float.
+ *
+ *
{@code o} is a Lua float if and only if {@code o} is an instance of
+ * {@link Double java.lang.Double} or {@link Float java.lang.Float}.
+ *
+ * @param o the object to test for being a float, may be {@code null}
+ * @return {@code true} iff {@code o} is a Lua float
+ */
+ public static boolean isFloat(Object o) {
+ return o instanceof Double || o instanceof Float;
+ }
+
+ /**
+ * Returns {@code true} iff {@code o} is a Lua integer.
+ *
+ *
{@code o} is a Lua number if and only if {@code o} is a Lua number and is not
+ * a Lua float.
+ *
+ * @param o the object to test for being an integer, may be {@code null}
+ * @return {@code true} iff {@code o} is a Lua integer
+ */
+ public static boolean isInteger(Object o) {
+ return isNumber(o) && !isFloat(o);
+ }
+
+ /**
+ * Returns {@code true} iff {@code o} is a Lua string.
+ *
+ *
{@code o} is a Lua string if and only if {@code o} is an instance of
+ * {@link ByteString} or {@link String java.lang.String}.
+ *
+ * @param o the object to test for being a string, may be {@code null}
+ * @return {@code true} iff {@code o} is a Lua string
+ */
+ public static boolean isString(Object o) {
+ return o instanceof ByteString || o instanceof String;
+ }
+
+ /**
+ * Returns {@code true} iff {@code o} is a Lua function.
+ *
+ *
{@code o} is a Lua function if and only if {@code o} is an instance of
+ * {@link LuaFunction}.
+ *
+ * @param o the object to test for being a function, may be {@code null}
+ * @return {@code true} iff {@code o} is a Lua function
+ */
+ public static boolean isFunction(Object o) {
+ return o instanceof LuaFunction;
+ }
+
+ /**
+ * Returns {@code true} iff {@code o} is a Lua userdata.
+ *
+ *
{@code o} is a Lua userdata if it is not {@code nil}, {@code boolean}, {@code number},
+ * {@code string}, {@code function}, {@code thread} or {@code table}.
+ *
+ * @param o the object to test for being a userdata, may be {@code null}
+ * @return {@code true} iff {@code o} is a Lua userdata
+ */
+ public static boolean isUserdata(Object o) {
+ return typeOf(o) == USERDATA;
+ }
+
+ /**
+ * Returns {@code true} iff {@code o} is full userdata.
+ *
+ *
{@code o} is full userdata if and only if {@code o} is an instance of
+ * {@link Userdata}.
+ *
+ * @param o the object to test for being full userdata, may be {@code null}
+ * @return {@code true} iff {@code o} is full userdata
+ */
+ public static boolean isFullUserdata(Object o) {
+ return o instanceof Userdata;
+ }
+
+ /**
+ * Returns {@code true} iff the object {@code o} is light userdata.
+ *
+ *
An object is light userdata when its Lua type is {@link #USERDATA} and it
+ * is not an instance of the {@link Userdata} class. In other words, it is not an
+ * instance of any class mapped to a Lua type.
+ *
+ * @param o the object to test for being light userdata, may be {@code null}
+ * @return {@code true} iff {@code o} is light userdata
+ */
+ public static boolean isLightUserdata(Object o) {
+ return !isFullUserdata(o) && isUserdata(o);
+ }
+
+ /**
+ * Returns {@code true} iff the object {@code o} is a Lua thread.
+ *
+ *
{@code o} is a Lua thread if and only if {@code o} is an instance of {@link Coroutine}.
+ *
+ * @param o the object to test for being a Lua thread, may be {@code null}
+ * @return {@code true} iff {@code o} is a Lua thread
+ */
+ public static boolean isThread(Object o) {
+ return o instanceof Coroutine;
+ }
+
+ /**
+ * Returns {@code true} iff the object {@code o} is a Lua table.
+ *
+ *
{@code o} is a Lua table if and only if {@code o} is an instance of {@link Table}.
+ *
+ * @param o the object to test for being a Lua table, may be {@code null}
+ * @return {@code true} iff {@code o} is a Lua table
+ */
+ public static boolean isTable(Object o) {
+ return o instanceof Table;
+ }
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/MetatableAccessor.java b/luna/src/main/java/org/classdump/luna/MetatableAccessor.java
new file mode 100644
index 00000000..63015d2b
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/MetatableAccessor.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+/**
+ * A metatable accessor, an interface for getting and setting object metatables.
+ *
+ *
In Lua, only tables and (full) userdata carry their own metatables; for all other
+ * types of values T, all values of type T share a metatable. This interface
+ * provides a uniform setter for metatables of all types.
+ */
+public interface MetatableAccessor extends MetatableProvider {
+
+ /**
+ * Sets the metatable for nil (i.e., the {@code nil} type) to {@code table}.
+ * {@code table} may be {@code null}: in that case, clears the metatable. Returns
+ * the previous metatable.
+ *
+ * @param table new metatable for the {@code nil} type, may be {@code null}
+ * @return the previous metatable for the {@code nil} type
+ */
+ Table setNilMetatable(Table table);
+
+ /**
+ * Sets the metatable for the {@code boolean} type.
+ * {@code table} may be {@code null}: in that case, clears the metatable. Returns
+ * the previous metatable.
+ *
+ * @param table new metatable for the {@code boolean} type, may be {@code null}
+ * @return the previous metatable for the {@code boolean} type
+ */
+ Table setBooleanMetatable(Table table);
+
+ /**
+ * Sets the metatable for the {@code number} type.
+ * {@code table} may be {@code null}: in that case, clears the metatable. Returns
+ * the previous metatable.
+ *
+ * @param table new metatable for the {@code number} type, may be {@code null}
+ * @return the previous metatable for the {@code number} type
+ */
+ Table setNumberMetatable(Table table);
+
+ /**
+ * Sets the metatable for the {@code string} type.
+ * {@code table} may be {@code null}: in that case, clears the metatable. Returns
+ * the previous metatable.
+ *
+ * @param table new metatable for the {@code string} type, may be {@code null}
+ * @return the previous metatable for the {@code string} type
+ */
+ Table setStringMetatable(Table table);
+
+ /**
+ * Sets the metatable for the {@code function} type.
+ * {@code table} may be {@code null}: in that case, clears the metatable. Returns
+ * the previous metatable.
+ *
+ * @param table new metatable for the {@code function} type, may be {@code null}
+ * @return the previous metatable for the {@code function} type
+ */
+ Table setFunctionMetatable(Table table);
+
+ /**
+ * Sets the metatable for the {@code thread} type.
+ * {@code table} may be {@code null}: in that case, clears the metatable. Returns
+ * the previous metatable.
+ *
+ * @param table new metatable for the {@code thread} type, may be {@code null}
+ * @return the previous metatable for the {@code thread} type
+ */
+ Table setThreadMetatable(Table table);
+
+ /**
+ * Sets the metatable for light userdata.
+ * {@code table} may be {@code null}: in that case, clears the metatable. Returns
+ * the previous metatable.
+ *
+ * @param table new metatable for light userdata, may be {@code null}
+ * @return the previous metatable for light userdata
+ */
+ Table setLightUserdataMetatable(Table table);
+
+ /**
+ * Sets the metatable of the object {@code instance} to {@code table}.
+ * {@code table} may be {@code null}: in that case, clears {@code instance}'s metatable.
+ * Returns the previous metatable.
+ *
+ *
Note that {@code instance} may share the metatable with other instances of the same
+ * (Lua) type. This method provides a uniform interface for setting the metatables
+ * of all types.
+ *
+ * @param instance object to set the metatable of, may be {@code null}
+ * @param table new metatable of {@code instance}, may be {@code null}
+ * @return the previous metatable of {@code instance}
+ */
+ Table setMetatable(Object instance, Table table);
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/MetatableProvider.java b/luna/src/main/java/org/classdump/luna/MetatableProvider.java
new file mode 100644
index 00000000..c44d2177
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/MetatableProvider.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+/**
+ * An interface for obtaining value metatables.
+ *
+ *
In Lua, only tables and (full) userdata carry their own metatables; for all other
+ * types of values T, all values of type T share a metatable. This interface
+ * provides uniform access to metatables of all types.
+ */
+public interface MetatableProvider {
+
+ /**
+ * Returns the metatable for nil (the {@code nil} type), or {@code null} if this
+ * provider does not assign a metatable to the {@code nil} type.
+ *
+ * @return the metatable for the {@code nil} type
+ */
+ Table getNilMetatable();
+
+ /**
+ * Returns the metatable for {@code boolean} values, or {@code null} if this provider does
+ * not assign a metatable to the {@code boolean} type.
+ *
+ * @return the metatable for the {@code boolean} type
+ */
+ Table getBooleanMetatable();
+
+ /**
+ * Returns the metatable for {@code number} values, or {@code null} if this provider does
+ * not assign a metatable to the {@code number} type.
+ *
+ * @return the metatable for the {@code number} type
+ */
+ Table getNumberMetatable();
+
+ /**
+ * Returns the metatable for {@code string} values, or {@code null} if this provider does
+ * not assign a metatable to the {@code string} type.
+ *
+ * @return the metatable for the {@code string} type
+ */
+ Table getStringMetatable();
+
+ /**
+ * Returns the metatable for {@code function} values, or {@code null} if this provider does
+ * not assign a metatable to the {@code function} type.
+ *
+ * @return the metatable for the {@code function} type
+ */
+ Table getFunctionMetatable();
+
+ /**
+ * Returns the metatable for {@code thread} values, or {@code null} if this provider does
+ * not assign a metatable to the {@code thread} type.
+ *
+ * @return the metatable for the {@code thread} type
+ */
+ Table getThreadMetatable();
+
+ /**
+ * Returns the metatable for light userdata, or {@code null} if this provider does
+ * not assign a metatable to light userdata..
+ *
+ * @return the metatable for light userdata
+ */
+ Table getLightUserdataMetatable();
+
+ /**
+ * Returns the metatable for the object {@code instance}, or {@code null} if this
+ * metatable provider does not assign any metatable to {@code instance}.
+ *
+ * @param instance the object to obtain a metatable for, may be {@code null}
+ * @return the metatable of {@code instance}, or {@code null} if there is no metatable assigned to
+ * {@code instance} in this provider
+ */
+ Table getMetatable(Object instance);
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/Metatables.java b/luna/src/main/java/org/classdump/luna/Metatables.java
new file mode 100644
index 00000000..a267164b
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/Metatables.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+import java.util.Objects;
+
+/**
+ * Metatable keys and utilities.
+ */
+public final class Metatables {
+
+ /**
+ * The metatable key {@code "__add"}. When defined, customises the behaviour of
+ * the Lua addition operator ({@code +}).
+ */
+ public static final ByteString MT_ADD = ByteString.constOf("__add");
+ /**
+ * The metatable key {@code "__sub"}. When defined, customises the behaviour of
+ * the Lua subtraction operator (binary {@code -}).
+ */
+ public static final ByteString MT_SUB = ByteString.constOf("__sub");
+ /**
+ * The metatable key {@code "__mul"}. When defined, customises the behaviour of
+ * the Lua multiplication operator ({@code *}).
+ */
+ public static final ByteString MT_MUL = ByteString.constOf("__mul");
+ /**
+ * The metatable key {@code "__div"}. When defined, customises the behaviour of
+ * the Lua division operator ({@code /}).
+ */
+ public static final ByteString MT_DIV = ByteString.constOf("__div");
+ /**
+ * The metatable key {@code "__mod"}. When defined, customises the behaviour of
+ * the Lua modulo operator ({@code %}).
+ */
+ public static final ByteString MT_MOD = ByteString.constOf("__mod");
+ /**
+ * The metatable key {@code "__pow"}. When defined, customises the behaviour of
+ * the Lua exponentiation operator ({@code ^}).
+ */
+ public static final ByteString MT_POW = ByteString.constOf("__pow");
+ /**
+ * The metatable key {@code "__unm"}. When defined, customises the behaviour of
+ * the Lua unary minus operator (unary {@code -}).
+ */
+ public static final ByteString MT_UNM = ByteString.constOf("__unm");
+ /**
+ * The metatable key {@code "__idiv"}. When defined, customises the behaviour of
+ * the Lua floor division ({@code //}).
+ */
+ public static final ByteString MT_IDIV = ByteString.constOf("__idiv");
+ /**
+ * The metatable key {@code "__band"}. When defined, customises the behaviour of
+ * the Lua bitwise AND operator ({@code &}).
+ */
+ public static final ByteString MT_BAND = ByteString.constOf("__band");
+ /**
+ * The metatable key {@code "__bor"}. When defined, customises the behaviour of
+ * the Lua bitwise OR operator ({@code |}).
+ */
+ public static final ByteString MT_BOR = ByteString.constOf("__bor");
+ /**
+ * The metatable key {@code "__bxor"}. When defined, customises the behaviour of
+ * the Lua bitwise XOR operator (binary {@code ~}).
+ */
+ public static final ByteString MT_BXOR = ByteString.constOf("__bxor");
+ /**
+ * The metatable key {@code "__bnot"}. When defined, customises the behaviour of
+ * the Lua bitwise NOT operator (unary {@code ~}).
+ */
+ public static final ByteString MT_BNOT = ByteString.constOf("__bnot");
+ /**
+ * The metatable key {@code "__shl"}. When defined, customises the behaviour of
+ * the Lua bitwise left shift operator ({@code <<}).
+ */
+ public static final ByteString MT_SHL = ByteString.constOf("__shl");
+ /**
+ * The metatable key {@code "__shr"}. When defined, customises the behaviour of
+ * the Lua bitwise right shift operator ({@code >>}).
+ */
+ public static final ByteString MT_SHR = ByteString.constOf("__shr");
+ /**
+ * The metatable key {@code "__concat"}. When defined, customises the behaviour of
+ * the Lua concatenation operator ({@code ..}).
+ */
+ public static final ByteString MT_CONCAT = ByteString.constOf("__concat");
+ /**
+ * The metatable key {@code "__len"}. When defined, customises the behaviour of
+ * the Lua length operator ({@code #}).
+ */
+ public static final ByteString MT_LEN = ByteString.constOf("__len");
+ /**
+ * The metatable key {@code "__eq"}. When defined, customises the behaviour of
+ * the Lua equality operator ({@code ==}).
+ */
+ public static final ByteString MT_EQ = ByteString.constOf("__eq");
+ /**
+ * The metatable key {@code "__lt"}. When defined, customises the behaviour of
+ * the Lua lesser-than operator ({@code <}).
+ */
+ public static final ByteString MT_LT = ByteString.constOf("__lt");
+ /**
+ * The metatable key {@code "__le"}. When defined, customises the behaviour of
+ * the Lua lesser-than-or-equal-to operator ({@code <=}).
+ */
+ public static final ByteString MT_LE = ByteString.constOf("__le");
+ /**
+ * The metatable key {@code "__index"}. When defined, customises the behaviour of
+ * the (non-assignment) Lua table access operator ({@code t[k]}).
+ */
+ public static final ByteString MT_INDEX = ByteString.constOf("__index");
+ /**
+ * The metatable key {@code "__newindex"}. When defined, customises the behaviour of
+ * Lua table assignment ({@code t[k] = v}).
+ */
+ public static final ByteString MT_NEWINDEX = ByteString.constOf("__newindex");
+ /**
+ * The metatable key {@code "__call"}. When defined, customises the behaviour of
+ * the Lua call operator ({@code f(args)}).
+ */
+ public static final ByteString MT_CALL = ByteString.constOf("__call");
+ /**
+ * The metatable key {@code "__mode"}. Used to control the weakness of table keys
+ * and values.
+ */
+ public static final ByteString MT_MODE = ByteString.constOf("__mode");
+
+ private Metatables() {
+ // not to be instantiated
+ }
+
+ /**
+ * Returns the entry with the key {@code event} of the metatable of the {@link LuaObject}
+ * {@code o}. If {@code o} does not have a metatable or {@code event} does not exist in it as
+ * a key, returns {@code null}.
+ *
+ *
The access of the metatable is raw (i.e. uses {@link Table#rawget(Object)}).
+ *
+ *
This method differs from {@link #getMetamethod(MetatableProvider, ByteString, Object)}
+ * in that it does not require a metatable provider as the object in question is known
+ * to have metatables attached on a per-instance basis.
+ *
+ * @param event the key to look up in the metatable, must not be {@code null}
+ * @param o the object in question, must not be {@code null}
+ * @return a non-{@code null} value if {@code event} is a key in {@code o}'s metatable; {@code
+ * null} otherwise
+ * @throws NullPointerException if {@code o} or {@code event} is {@code null}
+ */
+ public static Object getMetamethod(ByteString event, LuaObject o) {
+ Objects.requireNonNull(event);
+ Objects.requireNonNull(o);
+
+ Table mt = o.getMetatable();
+ if (mt != null) {
+ return mt.rawget(event);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the entry with the key {@code event} of the metatable of the object {@code o}.
+ * If {@code o} does not have a metatable or {@code event} does not exist in it as
+ * a key, returns {@code null}.
+ *
+ *
The access of the metatable is raw (i.e. uses {@link Table#rawget(Object)}).
+ *
+ * @param metatableProvider the metatable provider, must not be {@code null}
+ * @param event the key to look up in the metatable, must not be {@code null}
+ * @param o the object in question, may be {@code null}
+ * @return a non-{@code null} value if {@code event} is a key in {@code o}'s metatable; {@code
+ * null} otherwise
+ * @throws NullPointerException if {@code metatableProvider} or {@code event} is {@code null}
+ */
+ public static Object getMetamethod(MetatableProvider metatableProvider, ByteString event,
+ Object o) {
+ Objects.requireNonNull(event);
+ // o can be null
+
+ Table mt = metatableProvider.getMetatable(o);
+ if (mt != null) {
+ return mt.rawget(event);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the metatable entry {@code event} for {@code a} or in {@code b}, or {@code null}
+ * if neither {@code a} nor {@code b} has such an entry in their metatable.
+ *
+ *
This method is similar to {@link #getMetamethod(MetatableProvider, ByteString, Object)},
+ * but first looks up the entry {@code event} in {@code a}, and if this fails (by
+ * returning {@code null}), tries to look {@code event} up in {@code b}.
+ *
+ * @param metatableProvider the metatable provider, must not be {@code null}
+ * @param event the key to look up in the metatable, must not be {@code null}
+ * @param a the first object to try, may be {@code null}
+ * @param b the second object to try, may be {@code null}
+ * @return a non-{@code null} value if {@code event} is a key in {@code a}'s or {@code b}'s
+ * metatable (in this order); {@code null} otherwise
+ * @throws NullPointerException if {@code metatableProvider} or {@code event} is {@code null}
+ */
+ public static Object binaryHandlerFor(MetatableProvider metatableProvider, ByteString event,
+ Object a, Object b) {
+ Objects.requireNonNull(metatableProvider);
+ Objects.requireNonNull(event);
+ Object ma = Metatables.getMetamethod(metatableProvider, event, a);
+ return ma != null ? ma : Metatables.getMetamethod(metatableProvider, event, b);
+ }
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/NoIntegerRepresentationException.java b/luna/src/main/java/org/classdump/luna/NoIntegerRepresentationException.java
new file mode 100644
index 00000000..fb1a661f
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/NoIntegerRepresentationException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+/**
+ * An exception thrown to indicate an unsuccessful conversion of a number to an integer,
+ * i.e., when a number has no integer representation.
+ */
+public class NoIntegerRepresentationException extends ConversionException {
+
+ /**
+ * Constructs a new instance of {@code NoIntegerRepresentationException}.
+ */
+ public NoIntegerRepresentationException() {
+ super("number has no integer representation");
+ }
+
+}
diff --git a/luna/src/main/java/org/classdump/luna/Ordering.java b/luna/src/main/java/org/classdump/luna/Ordering.java
new file mode 100644
index 00000000..398fb990
--- /dev/null
+++ b/luna/src/main/java/org/classdump/luna/Ordering.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2016 Miroslav Janíček
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.classdump.luna;
+
+import java.util.Comparator;
+
+/**
+ * A representation of an ordering on values, allowing the comparison of values
+ * of the same type (in the type parameter {@code T}).
+ *
+ *
In Lua, only strings and numbers have such an ordering. This class serves the
+ * purpose of a bridge between the concrete representation of Lua numbers
+ * (as {@link java.lang.Number}) and the raw comparison operations provided by
+ * {@link LuaMathOperators}, and the concrete representation of Lua strings
+ * (as {@link java.lang.String}) and the comparison operations defined on them.
+ *
+ *
Consequently, there are two concrete implementations of this class:
+ * {@link #NUMERIC} for the numeric ordering and {@link #STRING} for the string
+ * ordering. These instances may be used directly for comparing objects of known,
+ * conforming types; for unknown objects, the method {@link #of(Object, Object)}
+ * returns an ordering that accepts {@link java.lang.Object}, and uses one of
+ * the two ordering instances, or {@code null} if the arguments
+ * are not directly comparable in Lua.
+ *
+ *
The comparison methods of this class return unboxed booleans.
+ *
+ *
This class implements the {@link Comparator} interface by imposing a total
+ * order on the accepted values. For numbers, this total ordering is different
+ * from the one imposed by this class. See the documentation of {@link NumericOrdering}
+ * for more details.
+ *
+ *
Example: Given two objects {@code a}, and {@code b}, attempt to
+ * evaluate the Lua expression {@code (a <= b)}:
+ *
+ *
+ * // Object a, b
+ * final boolean result;
+ * Ordering<Object> cmp = Ordering.of(a, b);
+ * if (cmp != null) {
+ * // a and b are comparable in cmp
+ * result = cmp.le(a, b);
+ * }
+ * else {
+ * throw new RuntimeException("a and b not comparable");
+ * }
+ *
+ *
+ * @param the type of values comparable in the ordering
+ */
+public abstract class Ordering implements Comparator {
+
+ /**
+ * A static instance of the numeric ordering.
+ */
+ public static final NumericOrdering NUMERIC = new NumericOrdering();
+ /**
+ * A static instance of the string ordering.
+ */
+ public static final StringOrdering STRING = new StringOrdering();
+ private static final NumericObjectOrdering NUMERIC_OBJECT = new NumericObjectOrdering();
+ private static final StringObjectOrdering STRING_OBJECT = new StringObjectOrdering();
+
+ private Ordering() {
+ // not to be instantiated by the outside world
+ }
+
+ /**
+ * Returns {@code true} iff the object {@code a} is raw-equal to {@code b} following
+ * the Lua equality rules.
+ *
+ *
Excerpt from the Lua Reference Manual (§3.4.4):
+ *
+ *
+ *
Equality (==) first compares the type of its operands. If the types are different,
+ * then the result is false. Otherwise, the values of the operands are compared.
+ * Strings are compared in the obvious way. Numbers are equal if they denote the
+ * same mathematical value.
+ *
+ *
Tables, userdata, and threads are compared by reference: two objects are considered
+ * equal only if they are the same object. Every time you create a new object (a table,
+ * userdata, or thread), this new object is different from any previously existing
+ * object. Closures with the same reference are always equal. Closures with any
+ * detectable difference (different behavior, different definition) are always
+ * different.
+ *
+ *
+ *
Note: Luna uses {@link Object#equals(Object)} to compare all non-nil,
+ * non-string, and non-numeric values for equality, effectively shifting the
+ * responsibility of adhering to the rules of Lua raw-equality for tables, userdata
+ * and threads to their implementations.
+ *
+ * @param a an object, may be {@code null}
+ * @param b another object, may be {@code null}
+ * @return {@code true} iff {@code a} is raw-equal to {@code b}
+ */
+ public static boolean isRawEqual(Object a, Object b) {
+ if (a == null && b == null) {
+ // two nils
+ return true;
+ } else if (a == null) {
+ // b is definitely not nil; also ensures that neither a nor b is null in the tests below
+ return false;
+ } else if (a instanceof Number && b instanceof Number) {
+ return Ordering.NUMERIC.eq((Number) a, (Number) b);
+ } else if (LuaType.isString(a) && LuaType.isString(b)) {
+ return Ordering.STRING.eq(toByteString(a), toByteString(b));
+ } else {
+ return a.equals(b);
+ }
+ }
+
+ private static ByteString toByteString(Object o) throws ClassCastException {
+ if (o instanceof ByteString) {
+ return (ByteString) o;
+ } else {
+ return ByteString.of((String) o); // may throw a ClassCastException
+ }
+ }
+
+ /**
+ * Based on the actual types of the arguments {@code a} and {@code b}, returns
+ * the ordering in which {@code a} and {@code b} can be compared, or {@code null}
+ * if they are not comparable.
+ *
+ *
More specifically, if {@code a} and {@code b} are both numbers, returns
+ * an ordering that uses (but is distinct from) {@link #NUMERIC}; if {@code a} and
+ * {@code b} are both strings, returns an ordering that uses (but is distinct from)
+ * {@link #STRING}; otherwise, returns {@code null}.
+ *
+ *
Note that when the result is non-{@code null}, it is guaranteed that
+ * 1) neither {@code a} nor {@code b} is {@code null}; and 2)
+ * both {@code a} and {@code b} are of types accepted by the underlying ordering.
+ * Caution must be observed when using the ordering with another object {@code c}
+ * (i.e., other than {@code a} or {@code b}): the returned ordering will throw
+ * a {@link ClassCastException} if {@code c} is of an incompatible type, or
+ * a {@link NullPointerException} if {@code c} is {@code null}.
+ *
+ * @param a an object, may be {@code null}
+ * @param b another object, may be {@code null}
+ * @return an ordering based on {@link #NUMERIC} if both {@code a} and {@code b} are numbers; an
+ * ordering based on {@link #STRING} if both {@code a} and {@code b} are strings; {@code null}
+ * otherwise
+ */
+ public static Ordering