diff --git a/builtin/core.wren b/builtin/core.wren index 598f6168..34713981 100644 --- a/builtin/core.wren +++ b/builtin/core.wren @@ -89,8 +89,39 @@ class List is Sequence { } class Map { - // TODO: Implement this. - toString { "{}" } + keys { new MapKeySequence(this) } + values { new MapValueSequence(this) } + + toString { + var first = true + var result = "{" + + for (key in keys) { + if (!first) result = result + ", " + first = false + result = result + key.toString + ": " + this[key].toString + } + + return result + "}" + } +} + +class MapKeySequence is Sequence { + new(map) { + _map = map + } + + iterate(n) { _map.iterate_(n) } + iteratorValue(iterator) { _map.keyIteratorValue_(iterator) } +} + +class MapValueSequence is Sequence { + new(map) { + _map = map + } + + iterate(n) { _map.iterate_(n) } + iteratorValue(iterator) { _map.valueIteratorValue_(iterator) } } class Range is Sequence {} diff --git a/src/wren_core.c b/src/wren_core.c index 9d77f55d..fe06e07b 100644 --- a/src/wren_core.c +++ b/src/wren_core.c @@ -132,8 +132,39 @@ static const char* libSource = "}\n" "\n" "class Map {\n" -" // TODO: Implement this.\n" -" toString { \"{}\" }\n" +" keys { new MapKeySequence(this) }\n" +" values { new MapValueSequence(this) }\n" +"\n" +" toString {\n" +" var first = true\n" +" var result = \"{\"\n" +"\n" +" for (key in keys) {\n" +" if (!first) result = result + \", \"\n" +" first = false\n" +" result = result + key.toString + \": \" + this[key].toString\n" +" }\n" +"\n" +" return result + \"}\"\n" +" }\n" +"}\n" +"\n" +"class MapKeySequence is Sequence {\n" +" new(map) {\n" +" _map = map\n" +" }\n" +"\n" +" iterate(n) { _map.iterate_(n) }\n" +" iteratorValue(iterator) { _map.keyIteratorValue_(iterator) }\n" +"}\n" +"\n" +"class MapValueSequence is Sequence {\n" +" new(map) {\n" +" _map = map\n" +" }\n" +"\n" +" iterate(n) { _map.iterate_(n) }\n" +" iteratorValue(iterator) { _map.valueIteratorValue_(iterator) }\n" "}\n" "\n" "class Range is Sequence {}\n"; @@ -714,6 +745,62 @@ DEF_NATIVE(map_count) RETURN_NUM(AS_MAP(args[0])->count); } +DEF_NATIVE(map_iterate) +{ + ObjMap* map = AS_MAP(args[0]); + + if (map->count == 0) RETURN_FALSE; + + // If we're starting the iteration, return the first entry. + int index = -1; + if (!IS_NULL(args[1])) + { + if (!validateInt(vm, args, 1, "Iterator")) return PRIM_ERROR; + index = (int)AS_NUM(args[1]); + + if (index < 0 || index >= map->capacity) RETURN_FALSE; + } + + // Find the next used entry, if any. + for (index++; index < map->capacity; index++) + { + if (!IS_UNDEFINED(map->entries[index].key)) RETURN_NUM(index); + } + + // If we get here, walked all of the entries. + RETURN_FALSE; +} + +DEF_NATIVE(map_keyIteratorValue) +{ + ObjMap* map = AS_MAP(args[0]); + int index = validateIndex(vm, args, map->capacity, 1, "Iterator"); + if (index == -1) return PRIM_ERROR; + + MapEntry* entry = &map->entries[index]; + if (IS_UNDEFINED(entry->key)) + { + RETURN_ERROR("Invalid map iterator value."); + } + + RETURN_VAL(entry->key); +} + +DEF_NATIVE(map_valueIteratorValue) +{ + ObjMap* map = AS_MAP(args[0]); + int index = validateIndex(vm, args, map->capacity, 1, "Iterator"); + if (index == -1) return PRIM_ERROR; + + MapEntry* entry = &map->entries[index]; + if (IS_UNDEFINED(entry->key)) + { + RETURN_ERROR("Invalid map iterator value."); + } + + RETURN_VAL(entry->value); +} + DEF_NATIVE(null_not) { RETURN_VAL(TRUE_VAL); @@ -1382,6 +1469,9 @@ void wrenInitializeCore(WrenVM* vm) NATIVE(vm->mapClass, "[ ]=", map_subscriptSetter); NATIVE(vm->mapClass, "clear", map_clear); NATIVE(vm->mapClass, "count", map_count); + NATIVE(vm->mapClass, "iterate_ ", map_iterate); + NATIVE(vm->mapClass, "keyIteratorValue_ ", map_keyIteratorValue); + NATIVE(vm->mapClass, "valueIteratorValue_ ", map_valueIteratorValue); // TODO: More map methods. vm->rangeClass = AS_CLASS(findGlobal(vm, "Range")); diff --git a/test/map/key_iterate.wren b/test/map/key_iterate.wren new file mode 100644 index 00000000..f1b169a0 --- /dev/null +++ b/test/map/key_iterate.wren @@ -0,0 +1,45 @@ +var a = {"one": 1, "two": 2, "three": 3, "four": 4}.keys + +// The precise numeric values aren't defined since they are indexes into the +// entry table and the hashing process isn't specified. So we just validate +// what we can assume about them. + +IO.print(a.iterate(null) is Num) // expect: true +IO.print(a.iterate(null) >= 0) // expect: true + +IO.print(a.iterate(0) is Num) // expect: true +IO.print(a.iterate(0) > 0) // expect: true +IO.print(a.iterate(1) is Num) // expect: true +IO.print(a.iterate(1) > 0) // expect: true +IO.print(a.iterate(2) is Num) // expect: true +IO.print(a.iterate(2) > 0) // expect: true +IO.print(a.iterate(3) is Num) // expect: true +IO.print(a.iterate(3) > 0) // expect: true + +var previous = -1 +var iterator = a.iterate(null) +while (iterator) { + IO.print(iterator > previous) + IO.print(iterator is Num) + previous = iterator + iterator = a.iterate(iterator) +} +// First entry: +// expect: true +// expect: true +// Second entry: +// expect: true +// expect: true +// Third entry: +// expect: true +// expect: true +// Fourth entry: +// expect: true +// expect: true + +// Out of bounds. +IO.print(a.iterate(16)) // expect: false +IO.print(a.iterate(-1)) // expect: false + +// Nothing to iterate in an empty map. +IO.print({}.keys.iterate(null)) // expect: false diff --git a/test/map/key_iterate_iterator_not_int.wren b/test/map/key_iterate_iterator_not_int.wren new file mode 100644 index 00000000..56d21bc1 --- /dev/null +++ b/test/map/key_iterate_iterator_not_int.wren @@ -0,0 +1,2 @@ +var a = {1: 2, 3: 4} +a.keys.iterate(1.5) // expect runtime error: Iterator must be an integer. diff --git a/test/map/key_iterate_iterator_not_num.wren b/test/map/key_iterate_iterator_not_num.wren new file mode 100644 index 00000000..f6d9193f --- /dev/null +++ b/test/map/key_iterate_iterator_not_num.wren @@ -0,0 +1,2 @@ +var a = {1: 2, 3: 4} +a.keys.iterate("2") // expect runtime error: Iterator must be a number. diff --git a/test/map/to_string.wren b/test/map/to_string.wren new file mode 100644 index 00000000..46b6b5b9 --- /dev/null +++ b/test/map/to_string.wren @@ -0,0 +1,27 @@ +// Handle empty map. +IO.print({}.toString) // expect: {} + +// Does not quote strings. +IO.print({"1": "2"}.toString) // expect: {1: 2} + +// Nested maps. +IO.print({1: {2: {}}}) // expect: {1: {2: {}}} + +// Calls toString on elements. +class Foo { + toString { "Foo.toString" } +} + +IO.print({1: new Foo}) // expect: {1: Foo.toString} + +// Since iteration order is unspecified, we don't know what order the results +// will be. +var s = {1: 2, 3: 4, 5: 6}.toString +IO.print(s == "{1: 2, 3: 4, 5: 6}" || + s == "{1: 2, 5: 6, 3: 4}" || + s == "{3: 4, 1: 2, 5: 6}" || + s == "{3: 4, 5: 6, 1: 2}" || + s == "{5: 6, 1: 2, 3: 4}" || + s == "{5: 6, 3: 4, 1: 2}") // expect: true + +// TODO: Handle maps that contain themselves. \ No newline at end of file diff --git a/test/map/value_iterate.wren b/test/map/value_iterate.wren new file mode 100644 index 00000000..cf185abf --- /dev/null +++ b/test/map/value_iterate.wren @@ -0,0 +1,45 @@ +var a = {"one": 1, "two": 2, "three": 3, "four": 4}.values + +// The precise numeric values aren't defined since they are indexes into the +// entry table and the hashing process isn't specified. So we just validate +// what we can assume about them. + +IO.print(a.iterate(null) is Num) // expect: true +IO.print(a.iterate(null) >= 0) // expect: true + +IO.print(a.iterate(0) is Num) // expect: true +IO.print(a.iterate(0) > 0) // expect: true +IO.print(a.iterate(1) is Num) // expect: true +IO.print(a.iterate(1) > 0) // expect: true +IO.print(a.iterate(2) is Num) // expect: true +IO.print(a.iterate(2) > 0) // expect: true +IO.print(a.iterate(3) is Num) // expect: true +IO.print(a.iterate(3) > 0) // expect: true + +var previous = -1 +var iterator = a.iterate(null) +while (iterator) { + IO.print(iterator > previous) + IO.print(iterator is Num) + previous = iterator + iterator = a.iterate(iterator) +} +// First entry: +// expect: true +// expect: true +// Second entry: +// expect: true +// expect: true +// Third entry: +// expect: true +// expect: true +// Fourth entry: +// expect: true +// expect: true + +// Out of bounds. +IO.print(a.iterate(16)) // expect: false +IO.print(a.iterate(-1)) // expect: false + +// Nothing to iterate in an empty map. +IO.print({}.values.iterate(null)) // expect: false diff --git a/test/map/value_iterate_iterator_not_int.wren b/test/map/value_iterate_iterator_not_int.wren new file mode 100644 index 00000000..792cdaf4 --- /dev/null +++ b/test/map/value_iterate_iterator_not_int.wren @@ -0,0 +1,2 @@ +var a = {1: 2, 3: 4} +a.values.iterate(1.5) // expect runtime error: Iterator must be an integer. diff --git a/test/map/value_iterate_iterator_not_num.wren b/test/map/value_iterate_iterator_not_num.wren new file mode 100644 index 00000000..dd2764a4 --- /dev/null +++ b/test/map/value_iterate_iterator_not_num.wren @@ -0,0 +1,2 @@ +var a = {1: 2, 3: 4} +a.values.iterate("2") // expect runtime error: Iterator must be a number.