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;