New implementation for Lua tables. Does not use Vector or Hashtable, instead
uses plain Java arrays directly to keep heap allocation to a minimum. Includes some unit tests that seem to indicate the basic operations are correct. However, the following things are not implemented: * Shrinking the capacity when elements are removed * Optimal storage of each element in array vs. hash when entries are added out of order. A junit test case is there for this.
This commit is contained in:
@@ -103,10 +103,7 @@ public class LuaCompat extends LFunction {
|
|||||||
LValue k = vm.getArg(1);
|
LValue k = vm.getArg(1);
|
||||||
LValue result = LNil.NIL;
|
LValue result = LNil.NIL;
|
||||||
if ( t instanceof LTable ) {
|
if ( t instanceof LTable ) {
|
||||||
LValue v = (LValue) ( (LTable) t ).rawGet( k );
|
result = ( (LTable) t ).get( k );
|
||||||
if ( v != null ) {
|
|
||||||
result = v;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
vm.setResult( result );
|
vm.setResult( result );
|
||||||
} break;
|
} break;
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ public class LDouble extends LNumber {
|
|||||||
return String.valueOf(m_value);
|
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
|
// binary operations on integers, first dispatch
|
||||||
public LValue luaBinOpUnknown(int opcode, LValue lhs) {
|
public LValue luaBinOpUnknown(int opcode, LValue lhs) {
|
||||||
return lhs.luaBinOpDouble( opcode, this.m_value );
|
return lhs.luaBinOpDouble( opcode, this.m_value );
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ public class LInteger extends LNumber {
|
|||||||
this.m_value = value;
|
this.m_value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int hashCode() {
|
public final int hashCode() {
|
||||||
return m_value;
|
return hashCodeOf( m_value );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int hashCodeOf( int v ) {
|
||||||
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int luaAsInt() {
|
public int luaAsInt() {
|
||||||
@@ -22,6 +26,10 @@ public class LInteger extends LNumber {
|
|||||||
return String.valueOf(m_value);
|
return String.valueOf(m_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isInteger() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// binary operations on integers, first dispatch
|
// binary operations on integers, first dispatch
|
||||||
public LValue luaBinOpUnknown(int opcode, LValue lhs) {
|
public LValue luaBinOpUnknown(int opcode, LValue lhs) {
|
||||||
return lhs.luaBinOpInteger( opcode, this.m_value );
|
return lhs.luaBinOpInteger( opcode, this.m_value );
|
||||||
|
|||||||
@@ -19,4 +19,9 @@ public class LNumber extends LValue {
|
|||||||
return TYPE_NAME;
|
return TYPE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns false by default for non-LNumbers, but subclasses of LNumber must
|
||||||
|
* override.
|
||||||
|
*/
|
||||||
|
public abstract boolean isInteger();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
package lua.value;
|
package lua.value;
|
||||||
|
|
||||||
import java.util.Enumeration;
|
import lua.Lua;
|
||||||
import java.util.Hashtable;
|
|
||||||
import java.util.Vector;
|
|
||||||
|
|
||||||
import lua.VM;
|
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 class LTable extends LValue {
|
||||||
|
|
||||||
public static final LString TYPE_NAME = new LString("table");
|
public static final LString TYPE_NAME = new LString("table");
|
||||||
@@ -16,191 +26,243 @@ public class LTable extends LValue {
|
|||||||
/** Metatable tag for intercepting table sets */
|
/** Metatable tag for intercepting table sets */
|
||||||
private static final LString TM_NEWINDEX = new LString("__newindex");
|
private static final LString TM_NEWINDEX = new LString("__newindex");
|
||||||
|
|
||||||
public Hashtable m_hash = new Hashtable();
|
/**
|
||||||
|
* 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];
|
||||||
|
|
||||||
private Vector m_vector; // if non-null then size() > 0
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
// stride and offset are needed only for the
|
/**
|
||||||
// implementation where m_vector stores keys
|
* Array of keys in the hash part. When there is no hash part this is null.
|
||||||
// {offset, stride + offset, ..., (size-1)*stride + offset}
|
* Elements of m_hashKeys are never LNil.NIL - they are null to indicate
|
||||||
// where size = m_vector.size()
|
* the hash slot is empty and some non-null, non-nil value otherwise.
|
||||||
//
|
*/
|
||||||
// private int stride = 1; // always non-0; if m_vector.size() == 1 then stride == 1
|
private LValue[] m_hashKeys;
|
||||||
// private int offset; // if m_vector.size() == 1 then offset is the single integer key
|
|
||||||
|
/**
|
||||||
|
* 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;
|
private LTable m_metatable;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Construct an empty LTable with no initial capacity. */
|
||||||
public LTable() {
|
public LTable() {
|
||||||
|
m_vector = EMPTY_ARRAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LTable(int narray, int nhash) {
|
/**
|
||||||
|
* 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];
|
||||||
/** Utility method for putting a string-keyed value directly, typically for initializing a table */
|
for ( int i = 0; i < narray; ++i ) {
|
||||||
public void put(String key, LValue value) {
|
m_vector[i] = LNil.NIL;
|
||||||
m_hash.put( new LString(key), value );
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
* Return total number of keys mapped to non-nil values. Not to be confused
|
||||||
// {offset, stride + offset, ..., (size-1)*stride + offset}
|
* with luaLength, which returns some number n such that the value at n+1 is
|
||||||
// where size = m_vector.size()
|
* nil.
|
||||||
//
|
*/
|
||||||
if (m_vector == null) {
|
public int size() {
|
||||||
offset = iKey;
|
return m_hashEntries + m_arrayEntries;
|
||||||
m_vector = new Vector();
|
}
|
||||||
m_vector.add(val);
|
|
||||||
return;
|
/**
|
||||||
|
* 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 {
|
} else {
|
||||||
int size = m_vector.size();
|
if ( checkLoadFactor() )
|
||||||
int multiple = iKey - offset;
|
rehash();
|
||||||
if (multiple >= 0) {
|
int slot = findSlot( key );
|
||||||
int i = multiple / stride;
|
if ( fillHashSlot( slot, val ) )
|
||||||
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;
|
return;
|
||||||
|
m_hashKeys[slot] = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
m_hash.put( key, val );
|
remove( key );
|
||||||
|
|
||||||
/* 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;
|
return;
|
||||||
}
|
}
|
||||||
|
if (checkLoadFactor())
|
||||||
|
rehash();
|
||||||
|
int slot = findSlot( key );
|
||||||
|
if (fillHashSlot( slot, value ))
|
||||||
|
return;
|
||||||
|
m_hashKeys[slot] = new LString( key );
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
public boolean containsKey(LValue key) {
|
remove( 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m_hash.containsKey( 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;
|
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);
|
// No room in array part, use hash part instead.
|
||||||
return ( v != null ) ? v : LNil.NIL;
|
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) {
|
public void luaGetTable(VM vm, LValue table, LValue key) {
|
||||||
LValue v = rawGet(key);
|
LValue v = get(key);
|
||||||
if ( v == LNil.NIL && m_metatable != null ) {
|
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 ) {
|
if ( event != null && event != LNil.NIL ) {
|
||||||
event.luaGetTable( vm, table, key );
|
event.luaGetTable( vm, table, key );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: table is unused -- is this correct?
|
vm.push(v);
|
||||||
// stack.stack[base] = val;
|
|
||||||
vm.push(v!=null? v: LNil.NIL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void luaSetTable(VM vm, LValue table, LValue key, LValue val) {
|
public void luaSetTable(VM vm, LValue table, LValue key, LValue val) {
|
||||||
if ( !containsKey( key ) && m_metatable != null ) {
|
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 ) {
|
if ( event != null && event != LNil.NIL ) {
|
||||||
event.luaSetTable( vm, table, key, val );
|
event.luaSetTable( vm, table, key, val );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rawSet(key, val);
|
put(key, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: why was this overridden in the first place?
|
/**
|
||||||
public String toString() {
|
* Return the "length" of this table. This will not return the same result
|
||||||
return m_hash.toString();
|
* 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 String luaAsString() {
|
|
||||||
return "table: "+id();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Built-in opcode LEN, for Strings and Tables */
|
|
||||||
public LValue luaLength() {
|
public LValue luaLength() {
|
||||||
int hashSize = m_hash.size();
|
for ( int i = Math.max( 0, m_arrayEntries-1 ); i < m_vector.length; ++i ) {
|
||||||
return new LInteger(
|
if ( m_vector[i] != LNil.NIL &&
|
||||||
m_vector == null ? hashSize : hashSize + m_vector.size());
|
( i+1 == m_vector.length || m_vector[i+1] == LNil.NIL ) ) {
|
||||||
|
return new LInteger( i+1 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new LInteger( 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Valid for tables */
|
/** Valid for tables */
|
||||||
@@ -213,41 +275,276 @@ public class LTable extends LValue {
|
|||||||
this.m_metatable = (LTable) metatable;
|
this.m_metatable = (LTable) metatable;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Valid for tables */
|
public String luaAsString() {
|
||||||
public LValue luaPairs() {
|
return "table: "+id();
|
||||||
return new LTableIterator(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Iterator for tables */
|
|
||||||
private static final class LTableIterator extends LFunction {
|
|
||||||
private final LTable t;
|
|
||||||
private final Enumeration e;
|
|
||||||
private int i;
|
|
||||||
|
|
||||||
private LTableIterator(LTable t) {
|
|
||||||
this.t = t;
|
|
||||||
this.e = t.m_hash.keys();
|
|
||||||
this.i = (t.m_vector == null) ? -1 : 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));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LString luaGetType() {
|
public LString luaGetType() {
|
||||||
return TYPE_NAME;
|
return TYPE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Valid for tables */
|
||||||
|
public LValue luaPairs() {
|
||||||
|
return new LTableIterator(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Iterator for tables */
|
||||||
|
static final class LTableIterator extends LFunction {
|
||||||
|
private final LTable t;
|
||||||
|
private int arrayIndex;
|
||||||
|
private int hashIndex;
|
||||||
|
|
||||||
|
private LTableIterator(LTable t) {
|
||||||
|
this.t = t;
|
||||||
|
this.arrayIndex = 0;
|
||||||
|
this.hashIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// perform a lua call
|
||||||
|
public boolean luaStackCall(VM vm) {
|
||||||
|
vm.setResult();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ public class LValue {
|
|||||||
return true;
|
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
|
// perform a lua call, return true if the call is to a lua function, false
|
||||||
// if it ran to completion.
|
// if it ran to completion.
|
||||||
public boolean luaStackCall(VM vm) {
|
public boolean luaStackCall(VM vm) {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ public class StandardTest extends TestCase {
|
|||||||
// relies on weak tables having their elements removed by
|
// relies on weak tables having their elements removed by
|
||||||
// the garbage collector. Until we implement that, remove the
|
// the garbage collector. Until we implement that, remove the
|
||||||
// built-in collectgarbage function.
|
// built-in collectgarbage function.
|
||||||
GlobalState.getGlobalsTable().rawSet( new LString("collectgarbage"), LNil.NIL );
|
GlobalState.getGlobalsTable().put( "collectgarbage", LNil.NIL );
|
||||||
StackState state = new StackState();
|
StackState state = new StackState();
|
||||||
Closure c = new Closure( state, code );
|
Closure c = new Closure( state, code );
|
||||||
|
|
||||||
|
|||||||
137
src/test/java/lua/value/LTableTest.java
Normal file
137
src/test/java/lua/value/LTableTest.java
Normal file
@@ -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() );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user