diff --git a/src/addon/java/lua/addon/luacompat/LuaCompat.java b/src/addon/java/lua/addon/luacompat/LuaCompat.java index e3899c1d..4c8a4465 100644 --- a/src/addon/java/lua/addon/luacompat/LuaCompat.java +++ b/src/addon/java/lua/addon/luacompat/LuaCompat.java @@ -103,10 +103,7 @@ public class LuaCompat extends LFunction { LValue k = vm.getArg(1); LValue result = LNil.NIL; if ( t instanceof LTable ) { - LValue v = (LValue) ( (LTable) t ).rawGet( k ); - if ( v != null ) { - result = v; - } + result = ( (LTable) t ).get( k ); } vm.setResult( result ); } break; diff --git a/src/main/java/lua/value/LDouble.java b/src/main/java/lua/value/LDouble.java index ea84598a..4172b808 100644 --- a/src/main/java/lua/value/LDouble.java +++ b/src/main/java/lua/value/LDouble.java @@ -18,6 +18,12 @@ public class LDouble extends LNumber { return String.valueOf(m_value); } + public boolean isInteger() { + // Cast to int and then back to double and see if the value + // survives the round trip. + return ( (double) ( (int) m_value ) ) == m_value; + } + // binary operations on integers, first dispatch public LValue luaBinOpUnknown(int opcode, LValue lhs) { return lhs.luaBinOpDouble( opcode, this.m_value ); diff --git a/src/main/java/lua/value/LInteger.java b/src/main/java/lua/value/LInteger.java index b46c91d3..b66f44c4 100644 --- a/src/main/java/lua/value/LInteger.java +++ b/src/main/java/lua/value/LInteger.java @@ -9,11 +9,15 @@ public class LInteger extends LNumber { public LInteger(int value) { this.m_value = value; } - - public int hashCode() { - return m_value; + + public final int hashCode() { + return hashCodeOf( m_value ); } - + + public static int hashCodeOf( int v ) { + return v; + } + public int luaAsInt() { return m_value; } @@ -22,6 +26,10 @@ public class LInteger extends LNumber { return String.valueOf(m_value); } + public boolean isInteger() { + return true; + } + // binary operations on integers, first dispatch public LValue luaBinOpUnknown(int opcode, LValue lhs) { return lhs.luaBinOpInteger( opcode, this.m_value ); diff --git a/src/main/java/lua/value/LNumber.java b/src/main/java/lua/value/LNumber.java index 72d8f4ae..78427f37 100644 --- a/src/main/java/lua/value/LNumber.java +++ b/src/main/java/lua/value/LNumber.java @@ -19,4 +19,9 @@ public class LNumber extends LValue { return TYPE_NAME; } + /** + * Returns false by default for non-LNumbers, but subclasses of LNumber must + * override. + */ + public abstract boolean isInteger(); } diff --git a/src/main/java/lua/value/LTable.java b/src/main/java/lua/value/LTable.java index 5af96843..0df1de0e 100644 --- a/src/main/java/lua/value/LTable.java +++ b/src/main/java/lua/value/LTable.java @@ -1,11 +1,21 @@ package lua.value; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.Vector; - +import lua.Lua; import lua.VM; +/** + * Simple implementation of table structure for Lua VM. Maintains both an array + * part and a hash part. Does not attempt to acheive the same performance as the + * C version. + * + * Java code can put values in the table or get values out (bypassing the + * metatable, if there is one) using put() and get(). There are specializations + * of put() and get() for integers and Strings to avoid allocating wrapper + * objects when possible. + * + * remove() methods are private: setting a key's value to nil is the correct way + * to remove an entry from the table. + */ public class LTable extends LValue { public static final LString TYPE_NAME = new LString("table"); @@ -15,192 +25,244 @@ public class LTable extends LValue { /** Metatable tag for intercepting table sets */ private static final LString TM_NEWINDEX = new LString("__newindex"); - - public Hashtable m_hash = new Hashtable(); - private Vector m_vector; // if non-null then size() > 0 + /** + * Zero-length array to use instead of null, so that we don't need to + * check for null everywhere. + */ + private static final LValue[] EMPTY_ARRAY = new LValue[0]; - // stride and offset are needed only for the - // implementation where m_vector stores keys - // {offset, stride + offset, ..., (size-1)*stride + offset} - // where size = m_vector.size() - // - // private int stride = 1; // always non-0; if m_vector.size() == 1 then stride == 1 - // private int offset; // if m_vector.size() == 1 then offset is the single integer key + /** + * Minimum legal capacity for the hash portion. Note that the hash portion + * must never be filled to capacity or findSlot() will run forever. + */ + private static final int MIN_HASH_CAPACITY = 2; + + /** + * Array of keys in the hash part. When there is no hash part this is null. + * Elements of m_hashKeys are never LNil.NIL - they are null to indicate + * the hash slot is empty and some non-null, non-nil value otherwise. + */ + private LValue[] m_hashKeys; + + /** + * Values in the hash part. Must be null when m_hashKeys is null and equal + * in size otherwise. + */ + private LValue[] m_hashValues; + + /** + * m_hashEntries is the number of slots that are used. Must always be less + * than m_hashKeys.length. + */ + private int m_hashEntries; + + /** + * Array of values to store the "array part" of the table, that is the + * entries with positive integer keys. Elements must never be null: "empty" + * slots are set to LNil.NIL. + */ + private LValue[] m_vector; + + /** + * Number of values in m_vector that non-nil. + */ + private int m_arrayEntries; private LTable m_metatable; + + + /** Construct an empty LTable with no initial capacity. */ public LTable() { - } - - public LTable(int narray, int nhash) { + m_vector = EMPTY_ARRAY; } - /** Utility method for putting a string-keyed value directly, typically for initializing a table */ - public void put(String key, LValue value) { - m_hash.put( new LString(key), value ); + /** + * Construct an empty LTable that is expected to contain entries with keys + * in the range 1 .. narray and nhash non-integer keys. + */ + public LTable( int narray, int nhash ) { + if ( nhash > 0 ) { + // Allocate arrays 25% bigger than nhash to account for load factor. + final int capacity = Math.max( nhash + ( nhash >> 2 ), nhash + 1 ); + m_hashKeys = new LValue[capacity]; + m_hashValues = new LValue[capacity]; + } + m_vector = new LValue[narray]; + for ( int i = 0; i < narray; ++i ) { + m_vector[i] = LNil.NIL; + } } - public void rawSet(LValue key, LValue val) { - - if (key instanceof LInteger) { - int iKey = ((LInteger) key).luaAsInt() - 1; - - // implementation where m_vector stores keys - // {1, ..., size} - // where size = m_vector.size() - // - if (m_vector == null) { - if (iKey == 0) { - m_vector = new Vector(); - m_vector.addElement(val); - return; - } - } else if (iKey >= 0) { - int size = m_vector.size(); - if (iKey < size) { - m_vector.setElementAt(val, iKey); - return; - } else if (iKey == size) { - m_vector.addElement(val); - return; - } - } - - /* - // implementation where m_vector stores keys - // {offset, stride + offset, ..., (size-1)*stride + offset} - // where size = m_vector.size() - // - if (m_vector == null) { - offset = iKey; - m_vector = new Vector(); - m_vector.add(val); + /** + * Return total number of keys mapped to non-nil values. Not to be confused + * with luaLength, which returns some number n such that the value at n+1 is + * nil. + */ + public int size() { + return m_hashEntries + m_arrayEntries; + } + + /** + * Generic put method for all types of keys, but does not use the metatable. + */ + public void put( LValue key, LValue val ) { + if ( key.isInteger() ) { + // call the integer-specific put method + put( key.luaAsInt(), val ); + } else if ( val == null || val == LNil.NIL ) { + // Remove the key if the value is nil. This comes after the check + // for an integer key so that values are properly removed from + // the array part. + remove( key ); + } else { + if ( checkLoadFactor() ) + rehash(); + int slot = findSlot( key ); + if ( fillHashSlot( slot, val ) ) return; - } else { - int size = m_vector.size(); - int multiple = iKey - offset; - if (multiple >= 0) { - int i = multiple / stride; - if ((i < size) && (i * stride == multiple)) { - m_vector.set(i, val); - return; - } - } else if (size == 1) { - stride = iKey - offset; - m_vector.add(val); - return; - } else if (iKey == stride * size + offset) { - m_vector.add(val); - return; - } - } - */ + m_hashKeys[slot] = key; } - - m_hash.put( key, val ); - - /* TODO: this is old incorrect code, kept here until metatables are fixed - if ( m_metatable != null ) { - if ( ! m_hash.containsKey(key) ) { - LValue event = (LValue) m_metatable.m_hash.get( TM_NEWINDEX ); - if ( event != null && event != LNil.NIL ) { - event.luaSetTable( vm, table, key, val ); - return; - } - } - } - */ } - public boolean containsKey(LValue key) { - if (m_vector != null) { - if (key instanceof LInteger) { - int iKey = ((LInteger) key).luaAsInt(); - if ((iKey >= 1) && (iKey <= m_vector.size())) { - return m_vector.elementAt(iKey-1) != LNil.NIL; - } - } + /** + * Utility method for putting a string-keyed value directly, typically for + * initializing a table. Bypasses the metatable, if any. + */ + public void put( String key, LValue value ) { + if (value == null || value == LNil.NIL) { + remove( key ); + return; } - return m_hash.containsKey( key ); + if (checkLoadFactor()) + rehash(); + int slot = findSlot( key ); + if (fillHashSlot( slot, value )) + return; + m_hashKeys[slot] = new LString( key ); } - /** Utility method to directly get the value in a table, without metatable calls */ - public LValue rawGet(LValue key) { - - if (m_vector != null) { - if (key instanceof LInteger) { - int iKey = ((LInteger) key).luaAsInt() - 1; - - // implementation where m_vector stores keys - // {0, ..., size-1} - // where size = m_vector.size() - // - if ((iKey >= 0) && (iKey < m_vector.size())) { - return (LValue) m_vector.elementAt(iKey); - } - - /* - // implementation where m_vector stores keys - // {offset, stride + offset, ..., (size-1)*stride + offset} - // where size = m_vector.size() - // - int multiple = iKey - offset; - if (multiple >= 0) { - int i = multiple / stride; - if ((i < m_vector.size()) && (i * stride == multiple)) { - vm.push((LValue) m_vector.get(i)); - return; + /** + * Method for putting an integer-keyed value. Bypasses the metatable, if + * any. + */ + public void put( int key, LValue value ) { + if (value == null || value == LNil.NIL) { + remove( key ); + return; + } + if ( key > 0 ) { + final int index = key - 1; + for ( ;; ) { + if ( index < m_vector.length ) { + if ( m_vector[index] == LNil.NIL ) { + ++m_arrayEntries; } + m_vector[index] = value; + return; + } else if ( index < ( m_arrayEntries + 1 ) * 2 ) { + resize( ( m_arrayEntries + 1 ) * 2 ); + } else { + break; } - */ } } - LValue v = (LValue) m_hash.get(key); - return ( v != null ) ? v : LNil.NIL; + // No room in array part, use hash part instead. + if ( checkLoadFactor() ) + rehash(); + int slot = findSlot( key ); + if ( fillHashSlot( slot, value ) ) + return; + m_hashKeys[ slot ] = new LInteger( key ); + } + + + /** + * Utility method to directly get the value in a table, without metatable + * calls. Must never return null, use LNil.NIL instead. + */ + public LValue get( LValue key ) { + if ( m_vector.length > 0 && key.isInteger() ) { + final int index = key.luaAsInt() - 1; + if ( index >= 0 && index < m_vector.length ) { + return m_vector[index]; + } + } + + if ( m_hashKeys == null ) + return LNil.NIL; + + int slot = findSlot( key ); + return ( m_hashKeys[slot] != null ) ? m_hashValues[slot] : LNil.NIL; + } + + /** Utility method for retrieving an integer-keyed value */ + public LValue get( int key ) { + if ( key > 0 && key <= m_vector.length ) { + return m_vector[key - 1]; + } + + int slot = findSlot( key ); + return ( m_hashKeys[slot] != null ) ? m_hashValues[slot] : LNil.NIL; + } + + + /** + * Return true if the table contains an entry with the given key, false if + * not. Ignores the metatable. + */ + public boolean containsKey( LValue key ) { + if ( m_vector.length > 0 && key.isInteger() ) { + final int index = key.luaAsInt() - 1; + if ( index >= 0 && index < m_vector.length ) { + final LValue v = m_vector[index]; + return v != LNil.NIL; + } + } + if ( m_hashKeys == null ) + return false; + final int slot = findSlot( key ); + return m_hashKeys[ slot ] != null; } public void luaGetTable(VM vm, LValue table, LValue key) { - LValue v = rawGet(key); + LValue v = get(key); if ( v == LNil.NIL && m_metatable != null ) { - LValue event = m_metatable.rawGet( TM_INDEX ); + LValue event = m_metatable.get( TM_INDEX ); if ( event != null && event != LNil.NIL ) { event.luaGetTable( vm, table, key ); return; } } - // TODO: table is unused -- is this correct? - // stack.stack[base] = val; - vm.push(v!=null? v: LNil.NIL); + vm.push(v); } public void luaSetTable(VM vm, LValue table, LValue key, LValue val) { if ( !containsKey( key ) && m_metatable != null ) { - LValue event = m_metatable.rawGet( TM_NEWINDEX ); + LValue event = m_metatable.get( TM_NEWINDEX ); if ( event != null && event != LNil.NIL ) { event.luaSetTable( vm, table, key, val ); return; } } - rawSet(key, val); + put(key, val); } - /* TODO: why was this overridden in the first place? - public String toString() { - return m_hash.toString(); - } - */ - - public String luaAsString() { - return "table: "+id(); - } - - /** Built-in opcode LEN, for Strings and Tables */ + /** + * Return the "length" of this table. This will not return the same result + * as the C version in all cases, but that's ok because the length operation + * on a table where the integer keys are sparse is vaguely defined. + */ public LValue luaLength() { - int hashSize = m_hash.size(); - return new LInteger( - m_vector == null ? hashSize : hashSize + m_vector.size()); + for ( int i = Math.max( 0, m_arrayEntries-1 ); i < m_vector.length; ++i ) { + if ( m_vector[i] != LNil.NIL && + ( i+1 == m_vector.length || m_vector[i+1] == LNil.NIL ) ) { + return new LInteger( i+1 ); + } + } + return new LInteger( 0 ); } /** Valid for tables */ @@ -212,6 +274,14 @@ public class LTable extends LValue { public void luaSetMetatable(LValue metatable) { this.m_metatable = (LTable) metatable; } + + public String luaAsString() { + return "table: "+id(); + } + + public LString luaGetType() { + return TYPE_NAME; + } /** Valid for tables */ public LValue luaPairs() { @@ -219,35 +289,262 @@ public class LTable extends LValue { } /** Iterator for tables */ - private static final class LTableIterator extends LFunction { + static final class LTableIterator extends LFunction { private final LTable t; - private final Enumeration e; - private int i; - + private int arrayIndex; + private int hashIndex; + private LTableIterator(LTable t) { this.t = t; - this.e = t.m_hash.keys(); - this.i = (t.m_vector == null) ? -1 : 0; + this.arrayIndex = 0; + this.hashIndex = 0; } - + // perform a lua call public boolean luaStackCall(VM vm) { vm.setResult(); - if ((i >= 0) && (i < t.m_vector.size())) { - vm.push(new LInteger(i+1)); - vm.push((LValue) t.m_vector.elementAt(i)); - ++i; - } else if ( e.hasMoreElements() ) { - LValue key = (LValue) e.nextElement(); - vm.push( key ); - vm.push((LValue) t.m_hash.get(key)); + int i; + while ( ( i = arrayIndex++ ) < t.m_vector.length ) { + if ( t.m_vector[i] != LNil.NIL ) { + vm.push( new LInteger( arrayIndex ) ); + vm.push( t.m_vector[ i ] ); + return false; + } + } + if ( t.m_hashKeys != null ) { + while ( ( i = hashIndex++ ) < t.m_hashKeys.length ) { + if ( t.m_hashKeys[i] != null ) { + vm.push( t.m_hashKeys[i] ); + vm.push( t.m_hashValues[i] ); + return false; + } + } } return false; } } - public LString luaGetType() { - return TYPE_NAME; + /** Remove the value in the table with the given integer key. */ + private void remove( int key ) { + if ( key > 0 ) { + final int index = key - 1; + if ( index < m_vector.length ) { + if ( m_vector[ index ] != LNil.NIL ) { + --m_arrayEntries; + } + return; + } + } + + if ( m_hashKeys != null ) { + int slot = findSlot( key ); + clearSlot( slot ); + } + } + + private void remove( String key ) { + if ( m_hashKeys != null ) { + int slot = findSlot( key ); + clearSlot( slot ); + } + } + + private void remove( LValue key ) { + if ( m_hashKeys != null ) { + int slot = findSlot( key ); + clearSlot( slot ); + } + } + + private void clearSlot( int i ) { + if ( m_hashKeys[ i ] != null ) { + + int j = i; + while ( m_hashKeys[ j = ( ( j + 1 ) % m_hashKeys.length ) ] != null ) { + final int k = hashToIndex( m_hashKeys[ j ].hashCode() ); + if ( ( j > i && ( k <= i || k > j ) ) || + ( j < i && ( k <= i && k > j ) ) ) { + m_hashKeys[ i ] = m_hashKeys[ j ]; + m_hashValues[ i ] = m_hashValues[ j ]; + i = j; + } + } + + --m_hashEntries; + m_hashKeys[ i ] = null; + m_hashValues[ i ] = null; + + if ( m_hashEntries == 0 ) { + m_hashKeys = null; + m_hashValues = null; + } + } + } + + private int findSlot( LValue key ) { + int i = hashToIndex( key.hashCode() ); + + // This loop is guaranteed to terminate as long as we never allow the + // table to get 100% full. + LValue k; + while ( ( k = m_hashKeys[i] ) != null && + !key.luaBinCmpUnknown( Lua.OP_EQ, k ) ) { + i = ( i + 1 ) % m_hashKeys.length; + } + return i; } + private int findSlot( String key ) { + // NOTE: currently LString uses the String's hashCode. + int i = hashToIndex( key.hashCode() ); + + // This loop is guaranteed to terminate as long as we never allow the + // table to get 100% full. + LValue k; + while ( ( k = m_hashKeys[i] ) != null && + !k.luaBinCmpString( Lua.OP_EQ, key ) ) { + i = ( i + 1 ) % m_hashKeys.length; + } + return i; + } + + private int findSlot( int key ) { + int i = hashToIndex( LInteger.hashCodeOf( key ) ); + + // This loop is guaranteed to terminate as long as we never allow the + // table to get 100% full. + LValue k; + while ( ( k = m_hashKeys[i] ) != null && + !k.luaBinCmpInteger( Lua.OP_EQ, key ) ) { + i = ( i + 1 ) % m_hashKeys.length; + } + return i; + } + + /** + * @return true if the given slot was already occupied, false otherwise. + */ + private boolean fillHashSlot( int slot, LValue value ) { + m_hashValues[ slot ] = value; + if ( m_hashKeys[ slot ] != null ) { + return true; + } else { + ++m_hashEntries; + return false; + } + } + + private int hashToIndex( int hash ) { + return ( hash & 0x7FFFFFFF ) % m_hashKeys.length; + } + + /** + * Should be called before inserting a value into the hash. + * + * @return true if the hash portion of the LTable is at its capacity. + */ + private boolean checkLoadFactor() { + if ( m_hashKeys == null ) + return true; + // Using a load factor of 2/3 because that is easy to compute without + // overflow or division. + final int hashCapacity = m_hashKeys.length; + return ( hashCapacity >> 1 ) >= ( hashCapacity - m_hashEntries ); + } + + private void rehash() { + final int oldCapacity = ( m_hashKeys != null ) ? m_hashKeys.length : 0; + final int newCapacity = ( oldCapacity > 0 ) ? 2 * oldCapacity : MIN_HASH_CAPACITY; + + final LValue[] oldKeys = m_hashKeys; + final LValue[] oldValues = m_hashValues; + + m_hashKeys = new LValue[ newCapacity ]; + m_hashValues = new LValue[ newCapacity ]; + + for ( int i = 0; i < oldCapacity; ++i ) { + final LValue k = oldKeys[i]; + if ( k != null ) { + final LValue v = oldValues[i]; + final int slot = findSlot( k ); + m_hashKeys[slot] = k; + m_hashValues[slot] = v; + } + } + } + + private void resize( int newCapacity ) { + final int oldCapacity = m_vector.length; + LValue[] newVector = new LValue[ newCapacity ]; + System.arraycopy( m_vector, 0, newVector, 0, Math.min( oldCapacity, newCapacity ) ); + + // We need to move keys from hash part to array part if array part is + // getting bigger, and from array part to hash part if array is getting + // smaller. + if ( newCapacity > oldCapacity ) { + if ( m_hashKeys != null ) { + for ( int i = oldCapacity; i < newCapacity; ++i ) { + int slot = findSlot( i+1 ); + if ( m_hashKeys[ slot ] != null ) { + newVector[ i ] = m_hashValues[ slot ]; + m_hashKeys[ i ] = null; + --m_hashEntries; + } else { + // Make sure all array-part values are initialized to nil + // so that we can just do one compare instead of two + // whenever we need to check if a slot is full or not. + newVector[ i ] = LNil.NIL; + } + } + } else { + for ( int i = oldCapacity; i < newCapacity; ++i ) { + newVector[ i ] = LNil.NIL; + } + } + } else { + for ( int i = newCapacity; i < oldCapacity; ++i ) { + LValue v = m_vector[i]; + if ( v != LNil.NIL ) { + if (checkLoadFactor()) + rehash(); + final int slot = findSlot( i+1 ); + m_hashKeys[ slot ] = new LInteger( i+1 ); + m_hashValues[ slot ] = v; + ++m_hashEntries; + } + } + } + + m_vector = newVector; + } + + // hooks for junit + + int getHashCapacity() { + return ( m_hashKeys != null ) ? m_hashKeys.length : 0; + } + + int getArrayCapacity() { + return m_vector.length; + } + + LValue[] getKeys() { + LValue[] keys = new LValue[ m_arrayEntries + m_hashEntries ]; + int out = 0; + + for ( int i = 0; i < m_vector.length; ++i ) { + if ( m_vector[ i ] != LNil.NIL ) { + keys[ out++ ] = new LInteger( i + 1 ); + } + } + + if ( m_hashKeys != null ) { + for ( int i = 0; i < m_hashKeys.length; ++i ) { + if ( m_hashKeys[ i ] != null ) + keys[ out++ ] = m_hashKeys[i]; + } + } + + return keys; + } } diff --git a/src/main/java/lua/value/LValue.java b/src/main/java/lua/value/LValue.java index a9178caf..465caa0f 100644 --- a/src/main/java/lua/value/LValue.java +++ b/src/main/java/lua/value/LValue.java @@ -18,7 +18,12 @@ public class LValue { public boolean luaAsBoolean() { return true; } - + + /** Return true if this value can be represented as an "int" */ + public boolean isInteger() { + return false; + } + // perform a lua call, return true if the call is to a lua function, false // if it ran to completion. public boolean luaStackCall(VM vm) { diff --git a/src/test/java/lua/StandardTest.java b/src/test/java/lua/StandardTest.java index 206de5c3..1a069f10 100644 --- a/src/test/java/lua/StandardTest.java +++ b/src/test/java/lua/StandardTest.java @@ -78,7 +78,7 @@ public class StandardTest extends TestCase { // relies on weak tables having their elements removed by // the garbage collector. Until we implement that, remove the // built-in collectgarbage function. - GlobalState.getGlobalsTable().rawSet( new LString("collectgarbage"), LNil.NIL ); + GlobalState.getGlobalsTable().put( "collectgarbage", LNil.NIL ); StackState state = new StackState(); Closure c = new Closure( state, code ); diff --git a/src/test/java/lua/value/LTableTest.java b/src/test/java/lua/value/LTableTest.java new file mode 100644 index 00000000..214daff1 --- /dev/null +++ b/src/test/java/lua/value/LTableTest.java @@ -0,0 +1,137 @@ +package lua.value; + +import junit.framework.TestCase; + +public class LTableTest extends TestCase { + + public void testInOrderIntegerKeyInsertion() { + LTable t = new LTable(); + + for ( int i = 1; i <= 32; ++i ) { + t.put( i, new LString( "Test Value! "+i ) ); + } + + // Ensure capacities make sense + assertEquals( 0, t.getHashCapacity() ); + + assertTrue( t.getArrayCapacity() >= 32 ); + assertTrue( t.getArrayCapacity() <= 64 ); + + // Ensure all keys are still there. + for ( int i = 1; i <= 32; ++i ) { + assertEquals( "Test Value! " + i, t.get( i ).luaAsString() ); + } + } + + public void testOutOfOrderIntegerKeyInsertion() { + LTable t = new LTable(); + + for ( int i = 32; i > 0; --i ) { + t.put( i, new LString( "Test Value! "+i ) ); + } + + // Ensure capacities make sense + assertEquals( 0, t.getHashCapacity() ); + + assertTrue( t.getArrayCapacity() >= 32 ); + assertTrue( t.getArrayCapacity() <= 64 ); + + // Ensure all keys are still there. + for ( int i = 1; i <= 32; ++i ) { + assertEquals( "Test Value! " + i, t.get( i ).luaAsString() ); + } + } + + public void testStringAndIntegerKeys() { + LTable t = new LTable(); + + for ( int i = 0; i < 10; ++i ) { + LString str = new LString( String.valueOf( i ) ); + t.put( i, str ); + t.put( str, new LInteger( i ) ); + } + + assertTrue( t.getArrayCapacity() >= 9 ); // 1, 2, ..., 9 + assertTrue( t.getArrayCapacity() <= 18 ); + assertTrue( t.getHashCapacity() >= 11 ); // 0, "0", "1", ..., "9" + assertTrue( t.getHashCapacity() <= 33 ); + + LValue[] keys = t.getKeys(); + + int intKeys = 0; + int stringKeys = 0; + + assertEquals( 20, keys.length ); + for ( int i = 0; i < keys.length; ++i ) { + LValue k = keys[i]; + + if ( k instanceof LInteger ) { + final int ik = k.luaAsInt(); + assertTrue( ik >= 0 && ik < 10 ); + final int mask = 1 << ik; + assertTrue( ( intKeys & mask ) == 0 ); + intKeys |= mask; + } else if ( k instanceof LString ) { + final int ik = Integer.parseInt( k.luaAsString() ); + assertEquals( String.valueOf( ik ), k.luaAsString() ); + assertTrue( ik >= 0 && ik < 10 ); + final int mask = 1 << ik; + assertTrue( "Key \""+ik+"\" found more than once", ( stringKeys & mask ) == 0 ); + stringKeys |= mask; + } else { + fail( "Unexpected type of key found" ); + } + } + + assertEquals( 0x03FF, intKeys ); + assertEquals( 0x03FF, stringKeys ); + } + + public void testBadInitialCapacity() { + LTable t = new LTable(0, 1); + + t.put( "test", new LString("foo") ); + t.put( "explode", new LString("explode") ); + assertEquals( 2, t.size() ); + } + + public void testRemove1() { + LTable t = new LTable(0, 1); + + t.put( "test", new LString("foo") ); + t.put( "explode", LNil.NIL ); + t.put( 42, LNil.NIL ); + t.put( new LTable(), LNil.NIL ); + t.put( "test", LNil.NIL ); + assertEquals( 0, t.size() ); + + t.put( 10, new LInteger( 5 ) ); + t.put( 10, LNil.NIL ); + assertEquals( 0, t.size() ); + } + + public void testRemove2() { + LTable t = new LTable(0, 1); + + t.put( "test", new LString("foo") ); + t.put( "string", new LInteger( 10 ) ); + assertEquals( 2, t.size() ); + + t.put( "string", LNil.NIL ); + t.put( "three", new LDouble( 3.14 ) ); + assertEquals( 2, t.size() ); + + t.put( "test", LNil.NIL ); + assertEquals( 1, t.size() ); + + t.put( 10, new LInteger( 5 ) ); + assertEquals( 2, t.size() ); + + t.put( 10, LNil.NIL ); + assertEquals( 1, t.size() ); + + t.put( "three", LNil.NIL ); + assertEquals( 0, t.size() ); + } + +}