From 3f06553f7f36dd35d5f7e5cf9918f2711ec70bf5 Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Sun, 3 May 2015 11:10:31 -0700 Subject: [PATCH] Allow fibers as map keys. --- doc/site/maps.markdown | 42 ++++++++++++++----- src/vm/wren_primitive.c | 4 +- src/vm/wren_value.c | 4 ++ src/vm/wren_value.h | 7 +++- src/vm/wren_vm.h | 5 +++ test/core/map/contains_key_not_value.wren | 2 +- test/core/map/key_types.wren | 10 +++-- test/core/map/remove_key_not_value.wren | 2 +- test/core/map/subscript_key_not_value.wren | 2 +- .../map/subscript_setter_key_not_value.wren | 2 +- 10 files changed, 60 insertions(+), 20 deletions(-) diff --git a/doc/site/maps.markdown b/doc/site/maps.markdown index 21e85110..b3924868 100644 --- a/doc/site/maps.markdown +++ b/doc/site/maps.markdown @@ -17,7 +17,9 @@ curly braces. Each entry is a key and a value separated by a colon: } This creates a map that maps the first names of the Beatles to their last -names. Syntactically, in a map literal, keys can be any literal, a variable name, or a parenthesized expression. Values can be any expression. Here, we're using string literals for both keys and values. +names. Syntactically, in a map literal, keys can be any literal, a variable +name, or a parenthesized expression. Values can be any expression. Here, we're +using string literals for both keys and values. *Semantically*, values can be any object, and multiple keys may map to the same value. Keys have a few limitations. They must be one of the immutable @@ -25,7 +27,15 @@ built-in [value types](values.html) in Wren. That means a number, string, range, bool, or `null`. You can also use a [class object](classes.html) as a key. -The reason for this limitation—and the reason maps are called "*hash* tables" in other languages—is that each key is used to generate a numeric *hash code*. This lets a map locate the value associated with a key in constant time, even in very large maps. Since Wren only knows how to hash certain built-in types, only those can be used as keys. +In addition, even though they aren't strictly immutable, [fibers](fibers.html) +can be used as map keys. This is handy for storing data that's roughly +"thread-local" by using the current fiber as a map key. + +The reason for this limitation—and the reason maps are called "*hash* +tables" in other languages—is that each key is used to generate a numeric +*hash code*. This lets a map locate the value associated with a key in constant +time, even in very large maps. Since Wren only knows how to hash certain +built-in types, only those can be used as keys. ## Adding entries @@ -37,18 +47,22 @@ You add new key-value pairs to the map by using the [subscript operator][]: capitals["Idaho"] = "Boise" capitals["Maine"] = "Augusta" -If the key isn't already present, this adds it and associates it with the given value. If the key is already there, this just replaces its value. +If the key isn't already present, this adds it and associates it with the given +value. If the key is already there, this just replaces its value. [subscript operator]: expressions.html#subscript-operators ## Looking up values -To find the value associated with some key, again you use your friend the subscript operator: +To find the value associated with some key, again you use your friend the +subscript operator: :::dart IO.print(capitals["Idaho"]) // "Boise". -If the key is present, this returns its value. Otherwise, it returns `null`. Of course, `null` itself can also be used as a value, so seeing `null` here doesn't necessarily mean the key wasn't found. +If the key is present, this returns its value. Otherwise, it returns `null`. Of +course, `null` itself can also be used as a value, so seeing `null` here +doesn't necessarily mean the key wasn't found. To tell definitively if a key exists, you can call `containsKey()`: @@ -67,7 +81,8 @@ You can see how many entries a map contains using `count`: ## Removing entries -To remove an entry from a map, call `remove()` and pass in the key for the entry you want to delete: +To remove an entry from a map, call `remove()` and pass in the key for the +entry you want to delete: :::dart capitals.remove("Maine") @@ -91,14 +106,18 @@ can just call `clear()`: ## Iterating over the contents -The subscript operator works well for finding values when you know the key you're looking for, but sometimes you want to see everything that's in the map. For that, map exposes two methods: `keys` and `values`. +The subscript operator works well for finding values when you know the key +you're looking for, but sometimes you want to see everything that's in the map. +For that, map exposes two methods: `keys` and `values`. -The first returns a [Sequence][] that [iterates][] over all of the keys in the map, and the second returns one that iterates over the values. +The first returns a [Sequence][] that [iterates][] over all of the keys in the +map, and the second returns one that iterates over the values. [sequence]: core/sequence.html [iterates]: control-flow.html#the-iterator-protocol -If you want to see all of the key-value pairs in a map, the easiest way is to iterate over the keys and use each to look up its value: +If you want to see all of the key-value pairs in a map, the easiest way is to +iterate over the keys and use each to look up its value: :::dart var stateBirds = { @@ -111,4 +130,7 @@ If you want to see all of the key-value pairs in a map, the easiest way is to it IO.print("The state bird of ", state, " is ", stateBirds[state]) } -This program will print the three states and their birds. However, the *order* that they are printed isn't defined. Wren makes no promises about what order keys and values will be iterated in when you use these methods. All it promises is that every entry will appear exactly once. +This program will print the three states and their birds. However, the *order* +that they are printed isn't defined. Wren makes no promises about what order +keys and values will be iterated in when you use these methods. All it promises +is that every entry will appear exactly once. diff --git a/src/vm/wren_primitive.c b/src/vm/wren_primitive.c index 668d67d3..358534c4 100644 --- a/src/vm/wren_primitive.c +++ b/src/vm/wren_primitive.c @@ -53,13 +53,13 @@ uint32_t validateIndexValue(WrenVM* vm, Value* args, uint32_t count, bool validateKey(WrenVM* vm, Value* args, int index) { Value arg = args[index]; - if (IS_BOOL(arg) || IS_CLASS(arg) || IS_NULL(arg) || + if (IS_BOOL(arg) || IS_CLASS(arg) || IS_FIBER(arg) || IS_NULL(arg) || IS_NUM(arg) || IS_RANGE(arg) || IS_STRING(arg)) { return true; } - args[0] = CONST_STRING(vm, "Key must be a value type."); + args[0] = CONST_STRING(vm, "Key must be a value type or fiber."); return false; } diff --git a/src/vm/wren_value.c b/src/vm/wren_value.c index dc3cb029..ebca63b8 100644 --- a/src/vm/wren_value.c +++ b/src/vm/wren_value.c @@ -136,6 +136,7 @@ ObjFiber* wrenNewFiber(WrenVM* vm, Obj* fn) { ObjFiber* fiber = ALLOCATE(vm, ObjFiber); initObj(vm, &fiber->obj, OBJ_FIBER, vm->fiberClass); + fiber->id = vm->nextFiberId++; wrenResetFiber(fiber, fn); @@ -322,6 +323,9 @@ static uint32_t hashObject(Obj* object) // Classes just use their name. return hashObject((Obj*)((ObjClass*)object)->name); + case OBJ_FIBER: + return ((ObjFiber*)object)->id; + case OBJ_RANGE: { ObjRange* range = (ObjRange*)object; diff --git a/src/vm/wren_value.h b/src/vm/wren_value.h index 2962c96c..df2bc1f7 100644 --- a/src/vm/wren_value.h +++ b/src/vm/wren_value.h @@ -133,7 +133,8 @@ typedef enum typedef struct { ValueType type; - union { + union + { double num; Obj* obj; } as; @@ -226,6 +227,10 @@ typedef struct sObjFiber // error message. Otherwise, it will be NULL. ObjString* error; + // A unique-ish numeric ID for the fiber. Lets fibers be used as map keys. + // Unique-ish since IDs may overflow and wrap around. + uint16_t id; + // This will be true if the caller that called this fiber did so using "try". // In that case, if this fiber fails with an error, the error will be given // to the caller. diff --git a/src/vm/wren_vm.h b/src/vm/wren_vm.h index eee96211..18c9c54f 100644 --- a/src/vm/wren_vm.h +++ b/src/vm/wren_vm.h @@ -50,6 +50,11 @@ struct WrenVM // for the module. ObjMap* modules; + // The ID that will be assigned to the next fiber that is allocated. Fibers + // are given unique-ish (not completely unique since this can overflow) IDs + // so that they can be used as map keys. + uint16_t nextFiberId; + // Memory management data: // The externally-provided function used to allocate memory. diff --git a/test/core/map/contains_key_not_value.wren b/test/core/map/contains_key_not_value.wren index 0477be74..90c8363f 100644 --- a/test/core/map/contains_key_not_value.wren +++ b/test/core/map/contains_key_not_value.wren @@ -1 +1 @@ -var result = {}.containsKey([]) // expect runtime error: Key must be a value type. +var result = {}.containsKey([]) // expect runtime error: Key must be a value type or fiber. diff --git a/test/core/map/key_types.wren b/test/core/map/key_types.wren index 31054b6f..78895b9b 100644 --- a/test/core/map/key_types.wren +++ b/test/core/map/key_types.wren @@ -1,3 +1,5 @@ +var fiber = new Fiber {} + var map = { null: "null value", true: "true value", @@ -6,7 +8,8 @@ var map = { 1.2: "1 point 2", List: "list class", "null": "string value", - (1..3): "1 to 3" + (1..3): "1 to 3", + fiber: "fiber" } IO.print(map[null]) // expect: null value @@ -17,8 +20,9 @@ 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[fiber]) // expect: fiber -IO.print(map.count) // expect: 8 +IO.print(map.count) // expect: 9 // Use the same keys (but sometimes different objects) to ensure keys have the // right equality semantics. @@ -40,4 +44,4 @@ 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 +IO.print(map.count) // expect: 9 diff --git a/test/core/map/remove_key_not_value.wren b/test/core/map/remove_key_not_value.wren index 6ebab4dc..8252d543 100644 --- a/test/core/map/remove_key_not_value.wren +++ b/test/core/map/remove_key_not_value.wren @@ -1 +1 @@ -var result = {}.remove([]) // expect runtime error: Key must be a value type. +var result = {}.remove([]) // expect runtime error: Key must be a value type or fiber. diff --git a/test/core/map/subscript_key_not_value.wren b/test/core/map/subscript_key_not_value.wren index 8e813e01..f68980c1 100644 --- a/test/core/map/subscript_key_not_value.wren +++ b/test/core/map/subscript_key_not_value.wren @@ -1 +1 @@ -var result = {}[[]] // expect runtime error: Key must be a value type. +var result = {}[[]] // expect runtime error: Key must be a value type or fiber. diff --git a/test/core/map/subscript_setter_key_not_value.wren b/test/core/map/subscript_setter_key_not_value.wren index c8ec79b1..158b8f58 100644 --- a/test/core/map/subscript_setter_key_not_value.wren +++ b/test/core/map/subscript_setter_key_not_value.wren @@ -1 +1 @@ -var result = {}[[]] = "value" // expect runtime error: Key must be a value type. +var result = {}[[]] = "value" // expect runtime error: Key must be a value type or fiber.