From cccd57b128299c705c214c3de7d0efa8443733eb Mon Sep 17 00:00:00 2001 From: Julien Verlaguet Date: Mon, 11 Nov 2019 19:28:48 -0500 Subject: [PATCH] New implementation of reactive tables. (#113) * New implementation of reactive tables. Up until now, the values used in a reactive table had to implement a bunch of traits (such as Equality, Orderable etc ...). The reason was that the implementation was relying on a sort of those values to function properly (compute the diffs etc ...). The other weird thing is that the implementation was returning a sorted array of the values inserted and was perform a "unique" operation on them for no good reason other than it was convenient for the old implementation. This new implementation fixes that, the values don't have any constraint other than being frozen (the interned pointer is used to do the operations). The values returned by "get" preserve repetitions. PS: Turns out the new implementation fixed a long standing bug in the incremental type-checker when a module was changing name. PS2: I used the opportunity to also fix issue #72. * removed constraints on Reactive table type parameter * revert changes in skip_to_native --- .../native/include/skip/Reactive-extc.h | 2 + lkg/runtime/native/src/Reactive.cpp | 4 + src/frontend/skipExpand.sk | 45 ++-- src/frontend/skipTyping.sk | 3 +- .../native/include/skip/Reactive-extc.h | 1 + src/runtime/native/src/Reactive.cpp | 4 + src/runtime/prelude/native/Reactive.sk | 214 ++++++++++++++---- .../native/include/skip/Reactive-extc.h | 1 + tests/runtime/native/src/Reactive.cpp | 4 + tests/runtime/prelude/native/Reactive.sk | 214 ++++++++++++++---- 10 files changed, 388 insertions(+), 104 deletions(-) diff --git a/lkg/runtime/native/include/skip/Reactive-extc.h b/lkg/runtime/native/include/skip/Reactive-extc.h index d438db08..8773982f 100644 --- a/lkg/runtime/native/include/skip/Reactive-extc.h +++ b/lkg/runtime/native/include/skip/Reactive-extc.h @@ -31,3 +31,5 @@ extern SkipIObj* SKIP_Reactive_reactiveGlobalCacheGet( extern void SKIP_Reactive_withTransaction(SkipRObj* callback); } + +extern SkipInt SKIP_Reactive_unsafe(SkipRObj* value); diff --git a/lkg/runtime/native/src/Reactive.cpp b/lkg/runtime/native/src/Reactive.cpp index 7ad28d3e..5f93abf3 100644 --- a/lkg/runtime/native/src/Reactive.cpp +++ b/lkg/runtime/native/src/Reactive.cpp @@ -162,3 +162,7 @@ void SKIP_Reactive_withTransaction(RObj* callback) { void Reactive::shutdown() { getReactiveGlobalCache()->cleanup(); } + +SkipInt SKIP_Reactive_unsafe(RObj* value) { + return (SkipInt)value; +} diff --git a/src/frontend/skipExpand.sk b/src/frontend/skipExpand.sk index 7f151ec5..839d3091 100644 --- a/src/frontend/skipExpand.sk +++ b/src/frontend/skipExpand.sk @@ -63,7 +63,7 @@ module alias A = SkipAst; /*****************************************************************************/ module ChildMap; -const childCache: Reactive.DiffCache = Reactive.DiffCache::create(); +const childCache: Reactive.Table = Reactive.Table::create(); memoized fun getChildren(className: String): SSet { childCache[className].foldl( @@ -74,6 +74,13 @@ memoized fun getChildren(className: String): SSet { untracked fun populateCache(classDefs: UMap): void { fileMap = mutable Map[]; + cacheMap = mutable Map[]; + for (classDef in classDefs) { + (fileRange, className) = classDef.name; + fileName = fileRange.filename; + fileMap![fileName] = mutable Vector[]; + cacheMap![(fileName, className)] = Array[]; + }; for (classDef in classDefs) addClass(fileMap, classDef); for (filename => assocs in fileMap) { defMap = mutable Map>[]; @@ -86,8 +93,12 @@ untracked fun populateCache(classDefs: UMap): void { }; }; for (parentName => childSet in defMap) { - childCache.set(filename, parentName, freeze(childSet)) - } + cacheMap![(filename, parentName)] = childSet.toArray(); + }; + }; + for (key => childSet in cacheMap) { + (fileName, parentName) = key; + childCache.set(fileName, parentName, childSet); } } @@ -103,11 +114,8 @@ untracked private fun addClass( | A.Tclass((_, x)) -> x | _ -> invariant_violation("The name should have been expanded") }; - if (map.containsKey(fileName)) { - map[fileName].push((parentStr, childStr)) - } else { - map![fileName] = mutable Vector[(parentStr, childStr)]; - } + invariant(map.containsKey(fileName)); + map[fileName].push((parentStr, childStr)); } } @@ -171,7 +179,7 @@ module end; module ModuleMap; -const moduleCache: Reactive.DiffCache = Reactive.DiffCache::create(); +const moduleCache: Reactive.Table = Reactive.Table::create(); fun containsKey(key: String): Bool { moduleCache.get(key).size() > 0 @@ -211,7 +219,7 @@ untracked fun populateCache(moduleDefs: SkipExpand.Module_map_t): void { }; for (fileName => moduleDefs2 in fileMap) { for (moduleName => defs in moduleDefs2) { - moduleCache.set(fileName, moduleName, defs.chill()) + moduleCache.set(fileName, moduleName, defs.toArray()) } }; } @@ -221,7 +229,7 @@ module end; module GlobalEnv; private memoized fun getDefinitions(defName: String): A.Program { - files = SkipExpand.fileCache.get(defName); + files = SkipExpand.fileCache.get(defName).sorted().collect(Vector).unique(); asts = SkipParse.parse_files(files, true); moduleDefsVector = asts.map(SkipExpand.definitions); module_defs = moduleDefsVector.foldl(SkipExpand.mergeModuleMap, SortedMap[]); @@ -296,7 +304,7 @@ class Defs{ type_defs: UMap, } -class DefNames(defs: Vector) { +class DefNames(defs: Array) { private fun binarySearch(elt: String, i: Int, j: Int): Bool { if (i > j) return false; pivot = (i + j) / 2; @@ -320,7 +328,7 @@ class DefNames(defs: Vector) { this.binarySearch("type:" + typeName, 0, this.defs.size() - 1) } - fun names(): Vector { + fun names(): Array { this.defs.map(x -> x.split(":")[1]) } } @@ -336,7 +344,10 @@ fun name_of_module(x: Module_): String { fun module_names_find(x: Module_): ?DefNames { // TODO: this should return none when the module is not defined - Some(DefNames(ModuleMap.moduleCache.get(name_of_module(x)))) + mresult = ModuleMap.moduleCache.get(name_of_module(x)) + .sorted() + .collect(Vector); + Some(DefNames(mresult.unique().toArray())) } fun module_map_find(x: Module_, y: Module_map_t): ?Defs { @@ -415,7 +426,7 @@ fun empty_defs(mod_name: Module_): Defs { } fun empty_defs_names(): DefNames { - DefNames(Vector[]) + DefNames(Array[]) } fun makeEnv(): Env { @@ -744,7 +755,7 @@ fun malias_type_def_( * file). */ /*****************************************************************************/ -const fileCache: Reactive.DiffCache = Reactive.DiffCache::create(); +const fileCache: Reactive.Table = Reactive.Table::create(); mutable private class FileMapBuilder{private mutable currentModule: Module_} { static fun create(): mutable this { @@ -759,7 +770,7 @@ mutable private class FileMapBuilder{private mutable currentModule: Module_} { (fileRange, _) = name; defName = make_qualified_name(fileRange, this.currentModule, name); filename = fileRange.filename; - fileCache.set(filename, defName.i1, Vector[filename]) + fileCache.set(filename, defName.i1, Array[filename]) } untracked private mutable fun file(defs: List): void { diff --git a/src/frontend/skipTyping.sk b/src/frontend/skipTyping.sk index 048e6301..a4d9ad4b 100644 --- a/src/frontend/skipTyping.sk +++ b/src/frontend/skipTyping.sk @@ -814,7 +814,8 @@ fun merge_extend( memoized fun fun_def(funName: SkipAst.Name): TAst.Fun_def { fd = SkipNaming.getFun(funName); - named_fun_def(fd) + result = named_fun_def(fd); + result } fun named_fun_def(fd: SkipNamedAst.Fun_def): TAst.Fun_def { diff --git a/src/runtime/native/include/skip/Reactive-extc.h b/src/runtime/native/include/skip/Reactive-extc.h index d438db08..f5f07f00 100644 --- a/src/runtime/native/include/skip/Reactive-extc.h +++ b/src/runtime/native/include/skip/Reactive-extc.h @@ -30,4 +30,5 @@ extern SkipIObj* SKIP_Reactive_reactiveGlobalCacheGet( SkipString key); extern void SKIP_Reactive_withTransaction(SkipRObj* callback); +extern SkipInt SKIP_Reactive_unsafe(SkipRObj* value); } diff --git a/src/runtime/native/src/Reactive.cpp b/src/runtime/native/src/Reactive.cpp index 7ad28d3e..5f93abf3 100644 --- a/src/runtime/native/src/Reactive.cpp +++ b/src/runtime/native/src/Reactive.cpp @@ -162,3 +162,7 @@ void SKIP_Reactive_withTransaction(RObj* callback) { void Reactive::shutdown() { getReactiveGlobalCache()->cleanup(); } + +SkipInt SKIP_Reactive_unsafe(RObj* value) { + return (SkipInt)value; +} diff --git a/src/runtime/prelude/native/Reactive.sk b/src/runtime/prelude/native/Reactive.sk index 9500620c..a2f9893c 100644 --- a/src/runtime/prelude/native/Reactive.sk +++ b/src/runtime/prelude/native/Reactive.sk @@ -36,6 +36,9 @@ private native fun reactiveGlobalCacheGetHelper( key: String, ): ?Ref; +@cpp_runtime("SKIP_Reactive_unsafe") +private native fun unsafe(T): Int; + private memoized fun reactiveGlobalCacheGet(id: Int, key: String): ?Ref { reactiveGlobalCacheGetHelper(id, key) } @@ -61,21 +64,55 @@ class GlobalCache private (id: Int) { @cpp_runtime native fun withTransaction(() -> void): void; -private class RefCountedCache(cache: Reactive.GlobalCache>) { +base class Boxed +class Box private ( + value: T, +) extends Boxed uses Hashable, Equality, Orderable { + static fun create(originalValue: T): this { + intern(Box(originalValue)) + } + fun hash(): Int { + unsafe(this) + } + + fun ==(value2: Box): Bool { + unsafe(this) == unsafe(value2) + } + + fun compare(value2: Box): Order { + unsafe(this).compare(unsafe(value2)) + } +} +class Box2(T, T) extends Boxed + +native base class Pointer { + fun compare(): void; +} + +private class RefCountedTable( + cache: Reactive.GlobalCache, Int>>, +) { fun create(): this { static(Reactive.GlobalCache::make()) } - untracked fun add(key: String, values: Vector): void { + untracked fun add(key: String, values: Array>): void { newMap = this.cache.maybeGet(key) match { | None() -> newMap = mutable Map[]; - values.each(value -> newMap![value] = 1); + values.each(value -> { + refCount = if (newMap.containsKey(value)) newMap[value] else { + 0 + }; + newMap![value] = refCount + 1; + }); newMap | Some(map) -> newMap = map.clone(); values.each(value -> { - refCount = if (map.containsKey(value)) map[value] else 0; + refCount = if (newMap.containsKey(value)) newMap[value] else { + 0 + }; newMap![value] = refCount + 1; }); newMap @@ -83,13 +120,15 @@ private class RefCountedCache(cache: Reactive.GlobalCache>) { this.cache.set(key, unsafe_chill_trust_me(newMap)); } - untracked fun remove(key: String, values: Vector): void { + untracked fun remove(key: String, values: Array>): void { newMap = this.cache.maybeGet(key) match { | None() -> invariant_violation("Cannot remove a nonexistent entry") | Some(map) -> newMap = map.clone(); values.each(value -> { - refCount = if (map.containsKey(value)) map[value] else 0; + refCount = if (newMap.containsKey(value)) newMap[value] else { + 0 + }; !refCount = refCount - 1; if (refCount > 0) { newMap![value] = refCount; @@ -102,26 +141,27 @@ private class RefCountedCache(cache: Reactive.GlobalCache>) { this.cache.set(key, unsafe_chill_trust_me(newMap)); } - memoized fun get(key: String): Vector { + memoized fun get(key: String): Array> { result = mutable Vector[]; this.cache.maybeGet(key).each(map -> map.each((key, refCount) -> { invariant(refCount > 0); - result.push(key) + for (_ in Range(0, refCount)) { + result.push(key) + } }) ); result.sort(); - unsafe_chill_trust_me(result) + result.toArray() } } -private mutable class VectorDiff{ - oldValues: Vector, - newValues: Vector, +private mutable class ArrayDiff{ + oldValues: Array>, + newValues: Array>, mutable index1: Int = 0, mutable index2: Int = 0, - toRemove: mutable Vector = mutable Vector[], - toAdd: mutable Vector = mutable Vector[], + toChange: mutable Vector<(Box, Int)> = mutable Vector[], } { private mutable fun loopThis(): void { valueOpt1 = this.oldValues.maybeGet(this.index1); @@ -129,20 +169,20 @@ private mutable class VectorDiff{ (valueOpt1, valueOpt2) match { | (None(), None()) -> void | (None(), Some(value)) -> - this.toAdd.push(value); + this.toChange.push((value, 1)); this.!index2 = this.index2 + 1; this.loopThis() | (Some(value), None()) -> - this.toRemove.push(value); + this.toChange.push((value, -1)); this.!index1 = this.index1 + 1; this.loopThis() | (Some(value1), Some(value2)) -> compare(value1, value2) match { | LT() -> - this.toRemove.push(value1); + this.toChange.push((value1, -1)); this.!index1 = this.index1 + 1 | GT() -> - this.toAdd.push(value2); + this.toChange.push((value2, 1)); this.!index2 = this.index2 + 1 | EQ() -> this.!index1 = this.index1 + 1; @@ -151,51 +191,139 @@ private mutable class VectorDiff{ this.loopThis() } } - mutable fun getResult(): (Vector, Vector) { + mutable fun getResult(): (Array<(Box, Int)>) { this.loopThis(); - (this.toRemove.chill(), this.toAdd.chill()) + this.toChange.toArray() } } -private fun diffVectors( - oldValues: Vector, - newValues: Vector, -): (Vector, Vector) { - vecDiff = mutable VectorDiff{oldValues, newValues}; - vecDiff.getResult() +private fun diffArrays( + oldValues: Array>, + newValues: Array>, +): (Array>, Array>) { + vecDiff = mutable ArrayDiff{oldValues, newValues}; + toChange = vecDiff.getResult(); + toAdd = mutable Vector[]; + toRemove = mutable Vector[]; + for (valueAndIncr in toChange) { + (value, incr) = valueAndIncr; + if (incr < 0) toRemove.push(value) else toAdd.push(value); + }; + (toAdd.toArray(), toRemove.toArray()) } -class DiffCache private { - refCountedCache: RefCountedCache, - sourceCache: Reactive.GlobalCache>, +class Table private { + refCountedTable: RefCountedTable, + sourceTable: Reactive.GlobalCache>>, } { static fun create(): this { static{ - refCountedCache => RefCountedCache(Reactive.GlobalCache::make()), - sourceCache => Reactive.GlobalCache::make(), + refCountedTable => RefCountedTable(Reactive.GlobalCache::make()), + sourceTable => Reactive.GlobalCache::make(), } } - untracked fun set(source: String, key: String, values: Vector): void { + untracked fun set(source: String, key: String, origValues: Array): void { + values = origValues.map(x -> Box::create(x)); sourceKey = source + ":" + key; newValues = { - x = values.clone(); + x = Vector::mcreateFromItems(values); x.sort(); - unsafe_chill_trust_me(x) + x.toArray() }; - this.sourceCache.maybeGet(sourceKey) match { + this.sourceTable.maybeGet(sourceKey) match { | None() -> - this.sourceCache.set(sourceKey, newValues); - this.refCountedCache.add(key, newValues) + this.sourceTable.set(sourceKey, newValues); + this.refCountedTable.add(key, newValues) | Some(oldValues) -> - this.sourceCache.set(sourceKey, newValues); - (toRemove, toAdd) = diffVectors(oldValues, newValues); - if (toRemove.size() > 0) this.refCountedCache.remove(key, toRemove); - if (toAdd.size() > 0) this.refCountedCache.add(key, toAdd) + this.sourceTable.set(sourceKey, newValues); + (toAdd, toRemove) = diffArrays(oldValues, newValues); + if (toRemove.size() > 0) this.refCountedTable.remove(key, toRemove); + if (toAdd.size() > 0) this.refCountedTable.add(key, toAdd) } } - fun get(key: String): Vector { - this.refCountedCache.get(key) + fun get(key: String): Array { + this.refCountedTable.get(key).map(x -> x.value) } } + +module end; + +module TestReactive; + +class TestFailure() extends Exception + +fun testTable( + rtable: Reactive.Table, + map: readonly Map<(String, String), Array>, + keyRange: Int, +): void { + for (keyNbr in Range(0, keyRange)) { + key = "" + keyNbr; + values = mutable Vector[]; + for (sourceKey => array in map) { + (_, k) = sourceKey; + if (k == key) { + array.each(value -> values.push(value)) + } + }; + values.sort(); + valuesMap = values.toArray().map(x -> x.toString()); + valuesTable = rtable.get(key); + mvaluesTable = Array::mcreateFromItems(valuesTable); + mvaluesTable.sort(); + !valuesTable = freeze(mvaluesTable); + if (valuesMap != valuesTable) { + debug(`Inconsistent result for key: ${key}`); + debug(`TestMap says: ${valuesMap}`); + debug(`Rtable says : ${valuesTable}`); + throw TestFailure(); + } + } +} + +untracked fun test(): void { + // Config + sourceRange = 8; + keyRange = 10; + valueSize = 8; + valueRange = 8; + iterations = 10000; + percentChancesOfRemoval = 10; + verbose = false; + + debug("Reactive table Test: STARTING"); + try { + r = Random::mcreate(23); + rtable = Reactive.Table::create(); + + map = mutable Map<(String, String), Array>[]; + for (_ in Range(0, iterations)) { + source = "" + r.random(0, sourceRange); + key = "" + r.random(0, keyRange); + if (r.random(0, 100) < percentChancesOfRemoval) { + rtable.set(source, key, Array[]); + map![(source, key)] = Array[]; + } else { + values = mutable Vector[]; + for (_ in Range(0, valueSize)) { + values.push("" + r.random(0, valueRange)); + }; + if (verbose) { + debug(`Adding: (${source}, ${key}) => ${values})`); + }; + arr = values.toArray(); + rtable.set(source, key, arr); + map![(source, key)] = arr; + rtable.set(source, key, arr); + testTable(rtable, map, keyRange); + } + }; + debug("Reactive table test: OK"); + } catch { + | TestFailure() -> debug("FAILED") + } +} + +module end; diff --git a/tests/runtime/native/include/skip/Reactive-extc.h b/tests/runtime/native/include/skip/Reactive-extc.h index d438db08..f5f07f00 100644 --- a/tests/runtime/native/include/skip/Reactive-extc.h +++ b/tests/runtime/native/include/skip/Reactive-extc.h @@ -30,4 +30,5 @@ extern SkipIObj* SKIP_Reactive_reactiveGlobalCacheGet( SkipString key); extern void SKIP_Reactive_withTransaction(SkipRObj* callback); +extern SkipInt SKIP_Reactive_unsafe(SkipRObj* value); } diff --git a/tests/runtime/native/src/Reactive.cpp b/tests/runtime/native/src/Reactive.cpp index 7ad28d3e..5f93abf3 100644 --- a/tests/runtime/native/src/Reactive.cpp +++ b/tests/runtime/native/src/Reactive.cpp @@ -162,3 +162,7 @@ void SKIP_Reactive_withTransaction(RObj* callback) { void Reactive::shutdown() { getReactiveGlobalCache()->cleanup(); } + +SkipInt SKIP_Reactive_unsafe(RObj* value) { + return (SkipInt)value; +} diff --git a/tests/runtime/prelude/native/Reactive.sk b/tests/runtime/prelude/native/Reactive.sk index 9500620c..a2f9893c 100644 --- a/tests/runtime/prelude/native/Reactive.sk +++ b/tests/runtime/prelude/native/Reactive.sk @@ -36,6 +36,9 @@ private native fun reactiveGlobalCacheGetHelper( key: String, ): ?Ref; +@cpp_runtime("SKIP_Reactive_unsafe") +private native fun unsafe(T): Int; + private memoized fun reactiveGlobalCacheGet(id: Int, key: String): ?Ref { reactiveGlobalCacheGetHelper(id, key) } @@ -61,21 +64,55 @@ class GlobalCache private (id: Int) { @cpp_runtime native fun withTransaction(() -> void): void; -private class RefCountedCache(cache: Reactive.GlobalCache>) { +base class Boxed +class Box private ( + value: T, +) extends Boxed uses Hashable, Equality, Orderable { + static fun create(originalValue: T): this { + intern(Box(originalValue)) + } + fun hash(): Int { + unsafe(this) + } + + fun ==(value2: Box): Bool { + unsafe(this) == unsafe(value2) + } + + fun compare(value2: Box): Order { + unsafe(this).compare(unsafe(value2)) + } +} +class Box2(T, T) extends Boxed + +native base class Pointer { + fun compare(): void; +} + +private class RefCountedTable( + cache: Reactive.GlobalCache, Int>>, +) { fun create(): this { static(Reactive.GlobalCache::make()) } - untracked fun add(key: String, values: Vector): void { + untracked fun add(key: String, values: Array>): void { newMap = this.cache.maybeGet(key) match { | None() -> newMap = mutable Map[]; - values.each(value -> newMap![value] = 1); + values.each(value -> { + refCount = if (newMap.containsKey(value)) newMap[value] else { + 0 + }; + newMap![value] = refCount + 1; + }); newMap | Some(map) -> newMap = map.clone(); values.each(value -> { - refCount = if (map.containsKey(value)) map[value] else 0; + refCount = if (newMap.containsKey(value)) newMap[value] else { + 0 + }; newMap![value] = refCount + 1; }); newMap @@ -83,13 +120,15 @@ private class RefCountedCache(cache: Reactive.GlobalCache>) { this.cache.set(key, unsafe_chill_trust_me(newMap)); } - untracked fun remove(key: String, values: Vector): void { + untracked fun remove(key: String, values: Array>): void { newMap = this.cache.maybeGet(key) match { | None() -> invariant_violation("Cannot remove a nonexistent entry") | Some(map) -> newMap = map.clone(); values.each(value -> { - refCount = if (map.containsKey(value)) map[value] else 0; + refCount = if (newMap.containsKey(value)) newMap[value] else { + 0 + }; !refCount = refCount - 1; if (refCount > 0) { newMap![value] = refCount; @@ -102,26 +141,27 @@ private class RefCountedCache(cache: Reactive.GlobalCache>) { this.cache.set(key, unsafe_chill_trust_me(newMap)); } - memoized fun get(key: String): Vector { + memoized fun get(key: String): Array> { result = mutable Vector[]; this.cache.maybeGet(key).each(map -> map.each((key, refCount) -> { invariant(refCount > 0); - result.push(key) + for (_ in Range(0, refCount)) { + result.push(key) + } }) ); result.sort(); - unsafe_chill_trust_me(result) + result.toArray() } } -private mutable class VectorDiff{ - oldValues: Vector, - newValues: Vector, +private mutable class ArrayDiff{ + oldValues: Array>, + newValues: Array>, mutable index1: Int = 0, mutable index2: Int = 0, - toRemove: mutable Vector = mutable Vector[], - toAdd: mutable Vector = mutable Vector[], + toChange: mutable Vector<(Box, Int)> = mutable Vector[], } { private mutable fun loopThis(): void { valueOpt1 = this.oldValues.maybeGet(this.index1); @@ -129,20 +169,20 @@ private mutable class VectorDiff{ (valueOpt1, valueOpt2) match { | (None(), None()) -> void | (None(), Some(value)) -> - this.toAdd.push(value); + this.toChange.push((value, 1)); this.!index2 = this.index2 + 1; this.loopThis() | (Some(value), None()) -> - this.toRemove.push(value); + this.toChange.push((value, -1)); this.!index1 = this.index1 + 1; this.loopThis() | (Some(value1), Some(value2)) -> compare(value1, value2) match { | LT() -> - this.toRemove.push(value1); + this.toChange.push((value1, -1)); this.!index1 = this.index1 + 1 | GT() -> - this.toAdd.push(value2); + this.toChange.push((value2, 1)); this.!index2 = this.index2 + 1 | EQ() -> this.!index1 = this.index1 + 1; @@ -151,51 +191,139 @@ private mutable class VectorDiff{ this.loopThis() } } - mutable fun getResult(): (Vector, Vector) { + mutable fun getResult(): (Array<(Box, Int)>) { this.loopThis(); - (this.toRemove.chill(), this.toAdd.chill()) + this.toChange.toArray() } } -private fun diffVectors( - oldValues: Vector, - newValues: Vector, -): (Vector, Vector) { - vecDiff = mutable VectorDiff{oldValues, newValues}; - vecDiff.getResult() +private fun diffArrays( + oldValues: Array>, + newValues: Array>, +): (Array>, Array>) { + vecDiff = mutable ArrayDiff{oldValues, newValues}; + toChange = vecDiff.getResult(); + toAdd = mutable Vector[]; + toRemove = mutable Vector[]; + for (valueAndIncr in toChange) { + (value, incr) = valueAndIncr; + if (incr < 0) toRemove.push(value) else toAdd.push(value); + }; + (toAdd.toArray(), toRemove.toArray()) } -class DiffCache private { - refCountedCache: RefCountedCache, - sourceCache: Reactive.GlobalCache>, +class Table private { + refCountedTable: RefCountedTable, + sourceTable: Reactive.GlobalCache>>, } { static fun create(): this { static{ - refCountedCache => RefCountedCache(Reactive.GlobalCache::make()), - sourceCache => Reactive.GlobalCache::make(), + refCountedTable => RefCountedTable(Reactive.GlobalCache::make()), + sourceTable => Reactive.GlobalCache::make(), } } - untracked fun set(source: String, key: String, values: Vector): void { + untracked fun set(source: String, key: String, origValues: Array): void { + values = origValues.map(x -> Box::create(x)); sourceKey = source + ":" + key; newValues = { - x = values.clone(); + x = Vector::mcreateFromItems(values); x.sort(); - unsafe_chill_trust_me(x) + x.toArray() }; - this.sourceCache.maybeGet(sourceKey) match { + this.sourceTable.maybeGet(sourceKey) match { | None() -> - this.sourceCache.set(sourceKey, newValues); - this.refCountedCache.add(key, newValues) + this.sourceTable.set(sourceKey, newValues); + this.refCountedTable.add(key, newValues) | Some(oldValues) -> - this.sourceCache.set(sourceKey, newValues); - (toRemove, toAdd) = diffVectors(oldValues, newValues); - if (toRemove.size() > 0) this.refCountedCache.remove(key, toRemove); - if (toAdd.size() > 0) this.refCountedCache.add(key, toAdd) + this.sourceTable.set(sourceKey, newValues); + (toAdd, toRemove) = diffArrays(oldValues, newValues); + if (toRemove.size() > 0) this.refCountedTable.remove(key, toRemove); + if (toAdd.size() > 0) this.refCountedTable.add(key, toAdd) } } - fun get(key: String): Vector { - this.refCountedCache.get(key) + fun get(key: String): Array { + this.refCountedTable.get(key).map(x -> x.value) } } + +module end; + +module TestReactive; + +class TestFailure() extends Exception + +fun testTable( + rtable: Reactive.Table, + map: readonly Map<(String, String), Array>, + keyRange: Int, +): void { + for (keyNbr in Range(0, keyRange)) { + key = "" + keyNbr; + values = mutable Vector[]; + for (sourceKey => array in map) { + (_, k) = sourceKey; + if (k == key) { + array.each(value -> values.push(value)) + } + }; + values.sort(); + valuesMap = values.toArray().map(x -> x.toString()); + valuesTable = rtable.get(key); + mvaluesTable = Array::mcreateFromItems(valuesTable); + mvaluesTable.sort(); + !valuesTable = freeze(mvaluesTable); + if (valuesMap != valuesTable) { + debug(`Inconsistent result for key: ${key}`); + debug(`TestMap says: ${valuesMap}`); + debug(`Rtable says : ${valuesTable}`); + throw TestFailure(); + } + } +} + +untracked fun test(): void { + // Config + sourceRange = 8; + keyRange = 10; + valueSize = 8; + valueRange = 8; + iterations = 10000; + percentChancesOfRemoval = 10; + verbose = false; + + debug("Reactive table Test: STARTING"); + try { + r = Random::mcreate(23); + rtable = Reactive.Table::create(); + + map = mutable Map<(String, String), Array>[]; + for (_ in Range(0, iterations)) { + source = "" + r.random(0, sourceRange); + key = "" + r.random(0, keyRange); + if (r.random(0, 100) < percentChancesOfRemoval) { + rtable.set(source, key, Array[]); + map![(source, key)] = Array[]; + } else { + values = mutable Vector[]; + for (_ in Range(0, valueSize)) { + values.push("" + r.random(0, valueRange)); + }; + if (verbose) { + debug(`Adding: (${source}, ${key}) => ${values})`); + }; + arr = values.toArray(); + rtable.set(source, key, arr); + map![(source, key)] = arr; + rtable.set(source, key, arr); + testTable(rtable, map, keyRange); + } + }; + debug("Reactive table test: OK"); + } catch { + | TestFailure() -> debug("FAILED") + } +} + +module end;