From abe80e6d4b9fedcf75e730bb7a0ba26fba73d8fa Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Sat, 24 Jan 2015 22:27:35 -0800 Subject: [PATCH] Initial map implementation. Still lots of methods missing and clean up and tests to do. Also still no literal syntax. But the core hash table code is there and working. The supported key types are all, uh, supported. --- builtin/core.wren | 5 + src/wren_core.c | 40 ++++- src/wren_utils.h | 1 + src/wren_value.c | 316 ++++++++++++++++++++++++++++++++++++---- src/wren_value.h | 93 ++++++++---- src/wren_vm.h | 1 + test/map/count.wren | 12 ++ test/map/grow.wren | 73 ++++++++++ test/map/key_types.wren | 43 ++++++ test/map/new.wren | 4 + test/map/type.wren | 7 + 11 files changed, 541 insertions(+), 54 deletions(-) create mode 100644 test/map/count.wren create mode 100644 test/map/grow.wren create mode 100644 test/map/key_types.wren create mode 100644 test/map/new.wren create mode 100644 test/map/type.wren diff --git a/builtin/core.wren b/builtin/core.wren index 54b48f3b..598f6168 100644 --- a/builtin/core.wren +++ b/builtin/core.wren @@ -88,4 +88,9 @@ class List is Sequence { } } +class Map { + // TODO: Implement this. + toString { "{}" } +} + class Range is Sequence {} diff --git a/src/wren_core.c b/src/wren_core.c index d8db5cf5..29dd20af 100644 --- a/src/wren_core.c +++ b/src/wren_core.c @@ -131,6 +131,11 @@ static const char* libSource = " }\n" "}\n" "\n" +"class Map {\n" +" // TODO: Implement this.\n" +" toString { \"{}\" }\n" +"}\n" +"\n" "class Range is Sequence {}\n"; // Validates that the given argument in [args] is a function. Returns true if @@ -676,6 +681,29 @@ DEF_NATIVE(list_subscriptSetter) RETURN_VAL(args[2]); } +DEF_NATIVE(map_instantiate) +{ + RETURN_OBJ(wrenNewMap(vm)); +} + +DEF_NATIVE(map_subscript) +{ + // TODO: Validate key type. + RETURN_VAL(wrenMapGet(AS_MAP(args[0]), args[1])); +} + +DEF_NATIVE(map_subscriptSetter) +{ + // TODO: Validate key type. + wrenMapSet(vm, AS_MAP(args[0]), args[1], args[2]); + RETURN_VAL(args[2]); +} + +DEF_NATIVE(map_count) +{ + RETURN_NUM(AS_MAP(args[0])->count); +} + DEF_NATIVE(null_not) { RETURN_VAL(TRUE_VAL); @@ -876,12 +904,12 @@ DEF_NATIVE(object_not) DEF_NATIVE(object_eqeq) { - RETURN_BOOL(wrenValuesEqual(args[0], args[1])); + RETURN_BOOL(wrenValuesSame(args[0], args[1])); } DEF_NATIVE(object_bangeq) { - RETURN_BOOL(!wrenValuesEqual(args[0], args[1])); + RETURN_BOOL(!wrenValuesSame(args[0], args[1])); } DEF_NATIVE(object_new) @@ -1338,7 +1366,15 @@ void wrenInitializeCore(WrenVM* vm) NATIVE(vm->listClass, "iteratorValue ", list_iteratorValue); NATIVE(vm->listClass, "removeAt ", list_removeAt); + vm->mapClass = AS_CLASS(findGlobal(vm, "Map")); + NATIVE(vm->mapClass->obj.classObj, " instantiate", map_instantiate); + NATIVE(vm->mapClass, "[ ]", map_subscript); + NATIVE(vm->mapClass, "[ ]=", map_subscriptSetter); + NATIVE(vm->mapClass, "count", map_count); + // TODO: More map methods. + vm->rangeClass = AS_CLASS(findGlobal(vm, "Range")); + // TODO: == operator. NATIVE(vm->rangeClass, "from", range_from); NATIVE(vm->rangeClass, "to", range_to); NATIVE(vm->rangeClass, "min", range_min); diff --git a/src/wren_utils.h b/src/wren_utils.h index ba6c668c..e5ba483d 100644 --- a/src/wren_utils.h +++ b/src/wren_utils.h @@ -52,6 +52,7 @@ DECLARE_BUFFER(Byte, uint8_t); DECLARE_BUFFER(Int, int); DECLARE_BUFFER(String, char*); +// TODO: Change this to use a map. typedef StringBuffer SymbolTable; // Initializes the symbol table. diff --git a/src/wren_value.c b/src/wren_value.c index 8595e534..060d3d9a 100644 --- a/src/wren_value.c +++ b/src/wren_value.c @@ -6,14 +6,26 @@ #include "wren_vm.h" // TODO: Tune these. -// The initial (and minimum) capacity of a non-empty list object. -#define LIST_MIN_CAPACITY (16) +// The initial (and minimum) capacity of a non-empty list or map object. +#define MIN_CAPACITY 16 -// The rate at which a list's capacity grows when the size exceeds the current -// capacity. The new capacity will be determined by *multiplying* the old -// capacity by this. Growing geometrically is necessary to ensure that adding -// to a list has O(1) amortized complexity. -#define LIST_GROW_FACTOR (2) +// The rate at which a collection's capacity grows when the size exceeds the +// current capacity. The new capacity will be determined by *multiplying* the +// old capacity by this. Growing geometrically is necessary to ensure that +// adding to a collection has O(1) amortized complexity. +#define GROW_FACTOR 2 + +// The maximum percentage of map entries that can be filled before the map is +// grown. A lower load takes more memory but reduces collisions which makes +// lookup faster. +#define MAP_LOAD_PERCENT 75 + +// Hash codes for singleton values. +// TODO: Tune these. +#define HASH_FALSE 1 +#define HASH_NAN 2 +#define HASH_NULL 3 +#define HASH_TRUE 4 DEFINE_BUFFER(Value, Value); DEFINE_BUFFER(Method, Method); @@ -240,15 +252,15 @@ ObjList* wrenNewList(WrenVM* vm, int numElements) return list; } -// Grows [list] if needed to ensure it can hold [count] elements. -static void ensureListCapacity(WrenVM* vm, ObjList* list, int count) +// Grows [list] if needed to ensure it can hold one more element. +static void ensureListCapacity(WrenVM* vm, ObjList* list) { - if (list->capacity >= count) return; + if (list->capacity >= list->count + 1) return; - int capacity = list->capacity * LIST_GROW_FACTOR; - if (capacity < LIST_MIN_CAPACITY) capacity = LIST_MIN_CAPACITY; + int capacity = list->capacity * GROW_FACTOR; + if (capacity < MIN_CAPACITY) capacity = MIN_CAPACITY; - list->capacity *= 2; + list->capacity = capacity; list->elements = (Value*)wrenReallocate(vm, list->elements, list->capacity * sizeof(Value), capacity * sizeof(Value)); // TODO: Handle allocation failure. @@ -259,7 +271,7 @@ void wrenListAdd(WrenVM* vm, ObjList* list, Value value) { if (IS_OBJ(value)) wrenPushRoot(vm, AS_OBJ(value)); - ensureListCapacity(vm, list, list->count + 1); + ensureListCapacity(vm, list); if (IS_OBJ(value)) wrenPopRoot(vm); @@ -270,7 +282,7 @@ void wrenListInsert(WrenVM* vm, ObjList* list, Value value, int index) { if (IS_OBJ(value)) wrenPushRoot(vm, AS_OBJ(value)); - ensureListCapacity(vm, list, list->count + 1); + ensureListCapacity(vm, list); if (IS_OBJ(value)) wrenPopRoot(vm); @@ -297,12 +309,12 @@ Value wrenListRemoveAt(WrenVM* vm, ObjList* list, int index) } // If we have too much excess capacity, shrink it. - if (list->capacity / LIST_GROW_FACTOR >= list->count) + if (list->capacity / GROW_FACTOR >= list->count) { list->elements = (Value*)wrenReallocate(vm, list->elements, sizeof(Value) * list->capacity, - sizeof(Value) * (list->capacity / LIST_GROW_FACTOR)); - list->capacity /= LIST_GROW_FACTOR; + sizeof(Value) * (list->capacity / GROW_FACTOR)); + list->capacity /= GROW_FACTOR; } if (IS_OBJ(removed)) wrenPopRoot(vm); @@ -311,6 +323,205 @@ Value wrenListRemoveAt(WrenVM* vm, ObjList* list, int index) return removed; } +ObjMap* wrenNewMap(WrenVM* vm) +{ + ObjMap* map = ALLOCATE(vm, ObjMap); + initObj(vm, &map->obj, OBJ_MAP, vm->mapClass); + map->capacity = 0; + map->count = 0; + map->entries = NULL; + return map; +} + +// Generates a hash code for [num]. +static uint32_t hashNumber(double num) +{ + // Hash the raw bits of the value. + DoubleBits data; + data.num = num; + return data.bits32[0] ^ data.bits32[1]; +} + +// Generates a hash code for [object]. +static uint32_t hashObject(Obj* object) +{ + switch (object->type) + { + case OBJ_CLASS: + // Classes just use their name. + return hashObject((Obj*)((ObjClass*)object)->name); + + case OBJ_RANGE: + { + ObjRange* range = (ObjRange*)object; + return hashNumber(range->from) ^ hashNumber(range->to); + } + + case OBJ_STRING: + { + ObjString* string = (ObjString*)object; + + // FNV-1a hash. See: http://www.isthe.com/chongo/tech/comp/fnv/ + uint32_t hash = 2166136261; + + // We want the contents of the string to affect the hash, but we also + // want to ensure it runs in constant time. We also don't want to bias + // towards the prefix or suffix of the string. So sample up to eight + // characters spread throughout the string. + // TODO: Tune this. + uint32_t step = 1 + 7 / string->length; + for (uint32_t i = 0; i < string->length; i += step) + { + hash ^= string->value[i]; + hash *= 16777619; + } + + return hash; + } + + default: + ASSERT(false, "Only immutable objects can be hashed."); + return 0; + } +} + +// Generates a hash code for [value], which must be one of the built-in +// immutable types: null, bool, class, num, range, or string. +static uint32_t hashValue(Value value) +{ + // TODO: We'll probably want to randomize this at some point. + +#if WREN_NAN_TAGGING + if (IS_NUM(value)) return hashNumber(AS_NUM(value)); + if (IS_OBJ(value)) return hashObject(AS_OBJ(value)); + + switch (GET_TAG(value)) + { + case TAG_FALSE: return HASH_FALSE; + case TAG_NAN: return HASH_NAN; + case TAG_NULL: return HASH_NULL; + case TAG_TRUE: return HASH_TRUE; + } +#else + switch (value.type) + { + case VAL_FALSE: return HASH_FALSE; + case VAL_NULL: return HASH_NULL; + case VAL_NUM: return hashNumber(AS_NUM(value)); + case VAL_TRUE: return HASH_TRUE; + case VAL_OBJ: return hashObject(AS_OBJ(value)); + } +#endif + UNREACHABLE(); + return 0; +} + +// Inserts [key] and [value] in the array of [entries] with the given +// [capacity]. +// +// Returns `true` if this is the first time [key] was added to the map. +static bool addEntry(MapEntry* entries, uint32_t capacity, + Value key, Value value) +{ + // Figure out where to insert it in the table. Use open addressing and + // basic linear probing. + uint32_t index = hashValue(key) % capacity; + + // We don't worry about an infinite loop here because ensureMapCapacity() + // ensures there are open spaces in the table. + while (true) + { + MapEntry* entry = &entries[index]; + + // If we found an empty slot, the key is not in the table. + if (IS_UNDEFINED(entry->key)) + { + entry->key = key; + entry->value = value; + return true; + } + + // If the key already exists, just replace the value. + if (wrenValuesEqual(entry->key, key)) + { + entry->value = value; + return false; + } + + // Try the next slot. + index = (index + 1) % capacity; + } +} + +// Ensures there is enough capacity in the entry array to store at least one +// more entry. +static void ensureMapCapacity(WrenVM* vm, ObjMap* map) +{ + // Only resize if we're too full. + if (map->count + 1 <= map->capacity * MAP_LOAD_PERCENT / 100) return; + + // Figure out the new hash table size. + uint32_t newCapacity = map->capacity * GROW_FACTOR; + if (newCapacity < MIN_CAPACITY) newCapacity = MIN_CAPACITY; + + // Create the new empty hash table. + MapEntry* newEntries = ALLOCATE_ARRAY(vm, MapEntry, newCapacity); + for (uint32_t i = 0; i < newCapacity; i++) + { + newEntries[i].key = UNDEFINED_VAL; + } + + // Re-add the existing entries. + if (map->capacity > 0) + { + for (uint32_t i = 0; i < map->capacity; i++) + { + MapEntry* entry = &map->entries[i]; + if (IS_UNDEFINED(entry->key)) continue; + + addEntry(newEntries, newCapacity, entry->key, entry->value); + } + } + + // Replace the array. + wrenReallocate(vm, map->entries, 0, 0); + map->entries = newEntries; + map->capacity = newCapacity; +} + +Value wrenMapGet(ObjMap* map, Value key) +{ + // Figure out where to insert it in the table. Use open addressing and + // basic linear probing. + uint32_t index = hashValue(key) % map->capacity; + + // We don't worry about an infinite loop here because ensureMapCapacity() + // ensures there are empty (i.e. UNDEFINED) spaces in the table. + while (true) + { + MapEntry* entry = &map->entries[index]; + + // If we found an empty slot, the key is not in the table. + if (IS_UNDEFINED(entry->key)) return NULL_VAL; + + // If the key matches, we found it. + if (wrenValuesEqual(entry->key, key)) return entry->value; + + // Try the next slot. + index = (index + 1) % map->capacity; + } +} + +void wrenMapSet(WrenVM* vm, ObjMap* map, Value key, Value value) +{ + ensureMapCapacity(vm, map); + if (addEntry(map->entries, map->capacity, key, value)) + { + // A new key was added. + map->count++; + } +} + Value wrenNewRange(WrenVM* vm, double from, double to, bool isInclusive) { ObjRange* range = ALLOCATE(vm, ObjRange); @@ -496,10 +707,26 @@ static void markList(WrenVM* vm, ObjList* list) // Keep track of how much memory is still in use. vm->bytesAllocated += sizeof(ObjList); - if (list->elements != NULL) + vm->bytesAllocated += sizeof(Value) * list->capacity; +} + +static void markMap(WrenVM* vm, ObjMap* map) +{ + if (setMarkedFlag(&map->obj)) return; + + // Mark the entries. + for (int i = 0; i < map->capacity; i++) { - vm->bytesAllocated += sizeof(Value) * list->capacity; + MapEntry* entry = &map->entries[i]; + if (IS_UNDEFINED(entry->key)) continue; + + wrenMarkValue(vm, entry->key); + wrenMarkValue(vm, entry->value); } + + // Keep track of how much memory is still in use. + vm->bytesAllocated += sizeof(ObjMap); + vm->bytesAllocated += sizeof(MapEntry) * map->capacity; } static void markUpvalue(WrenVM* vm, Upvalue* upvalue) @@ -590,6 +817,7 @@ void wrenMarkObj(WrenVM* vm, Obj* obj) case OBJ_FN: markFn( vm, (ObjFn*) obj); break; case OBJ_INSTANCE: markInstance(vm, (ObjInstance*)obj); break; case OBJ_LIST: markList( vm, (ObjList*) obj); break; + case OBJ_MAP: markMap( vm, (ObjMap*) obj); break; case OBJ_RANGE: setMarkedFlag(obj); break; case OBJ_STRING: markString( vm, (ObjString*) obj); break; case OBJ_UPVALUE: markUpvalue( vm, (Upvalue*) obj); break; @@ -635,6 +863,10 @@ void wrenFreeObj(WrenVM* vm, Obj* obj) wrenReallocate(vm, ((ObjList*)obj)->elements, 0, 0); break; + case OBJ_MAP: + wrenReallocate(vm, ((ObjMap*)obj)->entries, 0, 0); + break; + case OBJ_STRING: case OBJ_CLOSURE: case OBJ_FIBER: @@ -652,15 +884,44 @@ ObjClass* wrenGetClass(WrenVM* vm, Value value) return wrenGetClassInline(vm, value); } -static void printList(ObjList* list) +bool wrenValuesEqual(Value a, Value b) { - printf("["); - for (int i = 0; i < list->count; i++) + if (wrenValuesSame(a, b)) return true; + + // If we get here, it's only possible for two heap-allocated immutable objects + // to be equal. + if (!IS_OBJ(a) || !IS_OBJ(b)) return false; + + Obj* aObj = AS_OBJ(a); + Obj* bObj = AS_OBJ(b); + + // Must be the same type. + if (aObj->type != bObj->type) return false; + + switch (aObj->type) { - if (i > 0) printf(", "); - wrenPrintValue(list->elements[i]); + case OBJ_RANGE: + { + ObjRange* aRange = (ObjRange*)aObj; + ObjRange* bRange = (ObjRange*)bObj; + return aRange->from == bRange->from && + aRange->to == bRange->to && + aRange->isInclusive == bRange->isInclusive; + } + + case OBJ_STRING: + { + ObjString* aString = (ObjString*)aObj; + ObjString* bString = (ObjString*)bObj; + return aString->length == bString->length && + strncmp(aString->value, bString->value, aString->length) == 0; + } + + default: + // All other types are only equal if they are same, which they aren't if + // we get here. + return false; } - printf("]"); } static void printObject(Obj* obj) @@ -672,7 +933,8 @@ static void printObject(Obj* obj) case OBJ_FIBER: printf("[fiber %p]", obj); break; case OBJ_FN: printf("[fn %p]", obj); break; case OBJ_INSTANCE: printf("[instance %p]", obj); break; - case OBJ_LIST: printList((ObjList*)obj); break; + case OBJ_LIST: printf("[list %p]", obj); break; + case OBJ_MAP: printf("[map %p]", obj); break; case OBJ_RANGE: printf("[fn %p]", obj); break; case OBJ_STRING: printf("%s", ((ObjString*)obj)->value); break; case OBJ_UPVALUE: printf("[upvalue %p]", obj); break; diff --git a/src/wren_value.h b/src/wren_value.h index 16c0f964..0745206b 100644 --- a/src/wren_value.h +++ b/src/wren_value.h @@ -24,10 +24,14 @@ // Wren implementation calls these "Obj", or objects, though to a user, all // values are objects. // -// There is also a special singleton value "undefined". It is used to identify -// globals that have been implicitly declared by use in a forward reference but -// not yet explicitly declared. They only exist during compilation and do not -// appear at runtime. +// There is also a special singleton value "undefined". It is used internally +// but never appears as a real value to a user. It has two uses: +// +// - It is used to identify globals that have been implicitly declared by use +// in a forward reference but not yet explicitly declared. These only exist +// during compilation and do not appear at runtime. +// +// - It is used to represent unused map entries in an ObjMap. // // There are two supported Value representations. The main one uses a technique // called "NaN tagging" (explained in detail below) to store a number, any of @@ -52,6 +56,7 @@ typedef enum { OBJ_FN, OBJ_INSTANCE, OBJ_LIST, + OBJ_MAP, OBJ_RANGE, OBJ_STRING, OBJ_UPVALUE @@ -330,6 +335,8 @@ typedef struct { Obj obj; + // TODO: Make these uint32_t to match ObjMap, or vice versa. + // The number of elements allocated. int capacity; @@ -340,6 +347,30 @@ typedef struct Value* elements; } ObjList; +typedef struct +{ + // The entry's key, or UNDEFINED if the entry is not in use. + Value key; + + // The value associated with the key. + Value value; +} MapEntry; + +// A hash table mapping keys to values. +typedef struct +{ + Obj obj; + + // The number of entries allocated. + uint32_t capacity; + + // The number of entries in the map. + uint32_t count; + + // Pointer to a contiguous array of [capacity] entries. + MapEntry* entries; +} ObjMap; + typedef struct { Obj obj; @@ -373,6 +404,9 @@ typedef struct // Value -> ObjList*. #define AS_LIST(value) ((ObjList*)AS_OBJ(value)) +// Value -> ObjMap*. +#define AS_MAP(value) ((ObjMap*)AS_OBJ(value)) + // Value -> double. #define AS_NUM(value) (wrenValueToNum(value)) @@ -542,6 +576,14 @@ typedef struct #endif +// A union to let us reinterpret a double as raw bits and back. +typedef union +{ + uint64_t bits64; + uint32_t bits32[2]; + double num; +} DoubleBits; + // Creates a new "raw" class. It has no metaclass or superclass whatsoever. // This is only used for bootstrapping the initial Object and Class classes, // which are a little special. @@ -593,6 +635,16 @@ void wrenListInsert(WrenVM* vm, ObjList* list, Value value, int index); // Removes and returns the item at [index] from [list]. Value wrenListRemoveAt(WrenVM* vm, ObjList* list, int index); +// Creates a new empty map. +ObjMap* wrenNewMap(WrenVM* vm); + +// Returns the value in [map] associated with [key] or `NULL_VALUE` if it wasn't +// found. +Value wrenMapGet(ObjMap* map, Value key); + +// Associates [key] with [value] in [map]. +void wrenMapSet(WrenVM* vm, ObjMap* map, Value key, Value value); + // Creates a new range from [from] to [to]. Value wrenNewRange(WrenVM* vm, double from, double to, bool isInclusive); @@ -637,9 +689,9 @@ void wrenFreeObj(WrenVM* vm, Obj* obj); // benchmarks. ObjClass* wrenGetClass(WrenVM* vm, Value value); -// Returns true if [a] and [b] are strictly equal using built-in equality -// semantics. This is identity for object values, and value equality for others. -static inline bool wrenValuesEqual(Value a, Value b) +// Returns true if [a] and [b] are strictly the same value. This is identity +// for object values, and value equality for unboxed values. +static inline bool wrenValuesSame(Value a, Value b) { #if WREN_NAN_TAGGING // Value types have unique bit representations and we compare object types @@ -652,6 +704,11 @@ static inline bool wrenValuesEqual(Value a, Value b) #endif } +// Returns true if [a] and [b] are equivalent. Immutable values (null, bools, +// numbers, ranges, and strings) are equal if they have the same data. All +// other values are equal if they are identical objects. +bool wrenValuesEqual(Value a, Value b); + // TODO: Need to decide if this is for user output of values, or for debug // tracing. void wrenPrintValue(Value value); @@ -696,15 +753,8 @@ static inline Value wrenObjectToValue(Obj* obj) static inline double wrenValueToNum(Value value) { #if WREN_NAN_TAGGING - // Use a union to let us reinterpret the uint64_t bits back to the double - // value it actually stores. - union - { - uint64_t bits; - double num; - } data; - - data.bits = value; + DoubleBits data; + data.bits64 = value; return data.num; #else return value.num; @@ -715,16 +765,9 @@ static inline double wrenValueToNum(Value value) static inline Value wrenNumToValue(double num) { #if WREN_NAN_TAGGING - // Use a union to let us reinterpret the bits making up the double as an - // opaque blob of bits. - union - { - uint64_t bits; - double num; - } data; - + DoubleBits data; data.num = num; - return data.bits; + return data.bits64; #else return (Value){ VAL_NUM, n, NULL }; #endif diff --git a/src/wren_vm.h b/src/wren_vm.h index 223addfb..f15195f1 100644 --- a/src/wren_vm.h +++ b/src/wren_vm.h @@ -188,6 +188,7 @@ struct WrenVM ObjClass* fiberClass; ObjClass* fnClass; ObjClass* listClass; + ObjClass* mapClass; ObjClass* nullClass; ObjClass* numClass; ObjClass* objectClass; diff --git a/test/map/count.wren b/test/map/count.wren new file mode 100644 index 00000000..2c034d42 --- /dev/null +++ b/test/map/count.wren @@ -0,0 +1,12 @@ +var map = new Map +IO.print(map.count) // expect: 0 +map["one"] = "value" +IO.print(map.count) // expect: 1 +map["two"] = "value" +IO.print(map.count) // expect: 2 +map["three"] = "value" +IO.print(map.count) // expect: 3 + +// Adding existing key does not increase count. +map["two"] = "new value" +IO.print(map.count) // expect: 3 diff --git a/test/map/grow.wren b/test/map/grow.wren new file mode 100644 index 00000000..f1b3d54d --- /dev/null +++ b/test/map/grow.wren @@ -0,0 +1,73 @@ +// Make sure it can grow to some size. + +var fishes = [ + "Aeneus corydoras", "African glass catfish", "African lungfish", + "Aholehole", "Airbreathing catfish", "Airsac catfish", "Alaska blackfish", + "Albacore", "Alewife", "Alfonsino", "Algae eater", "Alligatorfish", + "Alligator gar", "American sole", "Amur pike", "Anchovy", "Anemonefish", + "Angelfish", "Angler", "Angler catfish", "Anglerfish", "Antarctic cod", + "Antarctic icefish", "Antenna codlet", "Arapaima", "Archerfish", + "Arctic char", "Armored gurnard", "Armored searobin", "Armorhead", + "Armorhead catfish", "Armoured catfish", "Arowana", "Arrowtooth eel", + "Aruana", "Asian carps", "Asiatic glassfish", "Atka mackerel", + "Atlantic cod", "Atlantic eel", "Atlantic herring", "Atlantic salmon", + "Atlantic saury", "Atlantic silverside", "Atlantic Trout", + "Australasian salmon", "Australian grayling", "Australian herring", + "Australian lungfish", "Australian prowfish", "Ayu", "Alooh", + "Baikal oilfish", "Bala shark", "Ballan wrasse", "Bamboo shark", + "Banded killifish", "Bandfish", "Banjo", "Bangus", "Banjo catfish", "Barb", + "Barbel", "Barbeled dragonfish", "Barbeled houndshark", "Barblless catfish", + "Barfish", "Barracuda", "Barracudina", "Barramundi", "Barred danio", + "Barreleye", "Basking shark", "Bass", "Basslet", "Batfish", "Bat ray", + "Beachsalmon", "Beaked salmon", "Beaked sandfish", "Beardfish", + "Beluga sturgeon", "Bengal danio", "Bent tooth", "Betta", "Bichir", + "Bicolor goat fish", "Bigeye", "Bigeye squaretail", "Bighead carp", + "Bigmouth buffalo", "Bigscale", "Bigscale pomfret", "Billfish", "Bitterling", + "Black angelfish", "Black bass", "Black dragonfish", "Blackchin", + "Blackfish", "black neon tetra", "Blacktip reef shark", "Black mackerel", + "Black pickerel", "Black prickleback", "Black scalyfin", "Black sea bass", + "Black scabbardfish", "Blacksmelt", "Black swallower", "Black tetra", + "Black triggerfish", "Bleak", "Blenny", "Blind goby", "Blind shark", + "Blobfish", "Blowfish", "Blue catfish", "Blue danio", "Blue-redstripe danio", + "Blue eye", "Bluefin tuna", "Bluefish", "Bluegill", "Blue gourami", + "Blue shark", "Blue triggerfish", "Blue whiting", "Bluntnose knifefish", + "Bluntnose minnow", "Boafish", "Boarfish", "Bobtail snipe eel", "Bocaccio", + "Boga", "Bombay duck", "Bonefish", "Bonito", "Bonnetmouth", "Bonytail chub", + "Bonytongue", "Bowfin", "Boxfish", "Bramble shark", "Bream", "Brill", + "Bristlemouth", "Bristlenose catfish", "Broadband dogfish", "Brook lamprey", + "Brook trout", "Brotula", "Brown trout", "Buffalo fish", "Bullhead", + "Bullhead shark", "Bull shark", "Bull trout", "Burbot", "Bumblebee goby", + "Buri", "Burma danio", "Burrowing goby", "Butterfly ray", "Butterflyfish", + "California flyingfish", "California halibut", "California smoothtongue", + "Canary rockfish", "Candiru", "Candlefish", "Capelin", "Cardinalfish", + "Cardinal tetra", "Carp", "Carpetshark", "Carpsucker", "Catalufa", "Catfish", + "Catla", "Cat shark", "Cavefish", "Celebes rainbowfish", "Central mudminnow", + "Cepalin", "Chain pickerel", "Channel bass", "Channel catfish", "Char", + "Cherry salmon", "Chimaera", "Chinook salmon", "Cherubfish", "Chub", + "Chubsucker", "Chum salmon", "Cichlid", "Cisco", "Climbing catfish", + "Climbing gourami", "Climbing perch", "Clingfish", "Clownfish", + "Clown loach", "Clown triggerfish", "Cobbler", "Cobia", "Cod", "Cod icefish", + "Codlet", "Codling", "Coelacanth", "Coffinfish", "Coho salmon", "Coley", + "Collared carpetshark", "Collared dogfish", "Colorado squawfish", "Combfish", + "Combtail gourami", "Combtooth blenny", "Common carp", "Common tunny", + "Conger eel", "Convict blenny", "Convict cichlid", "Cookie-cutter shark", + "Coolie loach", "Cornish Spaktailed Bream", "Cornetfish", "Cowfish", + "Cownose ray", "Cow shark", "Crappie", "Creek chub", "Crestfish", + "Crevice kelpfish", "Croaker", "Crocodile icefish", "Crocodile shark", + "Crucian carp", "Cuchia", "Cuckoo wrasse", "Cusk-eel", "Cuskfish", + "Cutlassfish", "Cutthroat eel", "Cutthroat trout" +] + +var map = new Map +for (fish in fishes) { + map[fish] = fish.count +} + +IO.print(map.count) // expect: 249 + +// Re-add some keys. +for (n in 20..50) { + map[fishes[n]] = n +} + +IO.print(map.count) // expect: 249 diff --git a/test/map/key_types.wren b/test/map/key_types.wren new file mode 100644 index 00000000..c91bdf45 --- /dev/null +++ b/test/map/key_types.wren @@ -0,0 +1,43 @@ +var map = new Map + +map[null] = "null value" +map[true] = "true value" +map[false] = "false value" +map[0] = "zero" +map[1.2] = "1 point 2" +map[List] = "list class" +map["null"] = "string value" +map[1..3] = "1 to 3" + +IO.print(map[null]) // expect: null value +IO.print(map[true]) // expect: true value +IO.print(map[false]) // expect: false value +IO.print(map[0]) // expect: zero +IO.print(map[1.2]) // expect: 1 point 2 +IO.print(map[List]) // expect: list class +IO.print(map["null"]) // expect: string value +IO.print(map[1..3]) // expect: 1 to 3 + +IO.print(map.count) // expect: 8 + +// Use the same keys (but sometimes different objects) to ensure keys have the +// right equality semantics. +map[null] = "new null value" +map[!false] = "new true value" +map[!true] = "new false value" +map[2 - 2] = "new zero" +map[1.2] = "new 1 point 2" +map[[].type] = "new list class" +map["nu" + "ll"] = "new string value" +map[(3 - 2)..(1 + 2)] = "new 1 to 3" + +IO.print(map[null]) // expect: new null value +IO.print(map[true]) // expect: new true value +IO.print(map[false]) // expect: new false value +IO.print(map[0]) // expect: new zero +IO.print(map[1.2]) // expect: new 1 point 2 +IO.print(map[List]) // expect: new list class +IO.print(map["null"]) // expect: new string value +IO.print(map[1..3]) // expect: new 1 to 3 + +IO.print(map.count) // expect: 8 diff --git a/test/map/new.wren b/test/map/new.wren new file mode 100644 index 00000000..88f36298 --- /dev/null +++ b/test/map/new.wren @@ -0,0 +1,4 @@ +var map = new Map + +IO.print(map.count) // expect: 0 +IO.print(map) // expect: {} diff --git a/test/map/type.wren b/test/map/type.wren new file mode 100644 index 00000000..fb808e3c --- /dev/null +++ b/test/map/type.wren @@ -0,0 +1,7 @@ +// TODO: Use map literal. + +IO.print(new Map is Map) // expect: true +// TODO: Abstract base class for associations. +IO.print(new Map is Object) // expect: true +IO.print(new Map is Bool) // expect: false +IO.print((new Map).type == Map) // expect: true