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