diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 9ac3371f43..61c5151a17 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +* [JS/TS] Add `ConditionalWeakTable` (by @chkn) + ## 4.15.0 - 2024-03-18 ### Fixed diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 5eeeabe1ad..573b10491c 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2636,6 +2636,60 @@ let dictionaries (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp Some c -> Helper.InstanceCall(c, methName, t, args, i.SignatureArgTypes, ?loc = r) |> Some | _ -> None +let conditionalWeakTable (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + match i.CompiledName, thisArg with + | ".ctor", _ -> + match i.GenericArgs with + | [ keyType; _ ] -> + match keyType with + | Boolean + | String + | Number _ -> + $"ConditionalWeakTable does not support primitive keys in JS" + |> addError com ctx.InlinePath r + | _ -> () + | _ -> + $"Unexpected number of generic arguments for ConditionalWeakTable: %A{i.GenericArgs}" + |> addError com ctx.InlinePath r + + Helper.LibCall( + com, + "ConditionalWeakTable", + "default", + t, + args, + i.SignatureArgTypes, + isConstructor = true, + genArgs = i.GenericArgs, + ?loc = r + ) + |> Some + | "Add", _ -> + Helper.LibCall(com, "MapUtil", "addToDict", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + |> Some + | "GetOrCreateValue", _ -> None + | "GetValue", _ -> + Helper.LibCall( + com, + "MapUtil", + "getItemFromDictOrCreate", + t, + args, + i.SignatureArgTypes, + ?thisArg = thisArg, + ?loc = r + ) + |> Some + | "TryAdd", _ -> + Helper.LibCall(com, "MapUtil", "tryAddToDict", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + |> Some + | "TryGetValue", _ -> + Helper.LibCall(com, "MapUtil", "tryGetValue", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + |> Some + | ReplaceName [ "AddOrUpdate", "set"; "Clear", "clear"; "Remove", "delete" ] meth, Some c -> + Helper.InstanceCall(c, meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some + | _ -> None + let hashSets (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | ".ctor", _, _ -> @@ -3864,6 +3918,7 @@ let private replacedModules = Types.dictionary, dictionaries Types.idictionary, dictionaries Types.ireadonlydictionary, dictionaries + Types.conditionalWeakTable, conditionalWeakTable Types.ienumerableGeneric, enumerables Types.ienumerable, enumerables Types.valueCollection, enumerables diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 0060846929..1fb7e02f7f 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -330,6 +330,9 @@ module Types = [] let ireadonlydictionary = "System.Collections.Generic.IReadOnlyDictionary`2" + [] + let conditionalWeakTable = "System.Runtime.CompilerServices.ConditionalWeakTable`2" + [] let hashset = "System.Collections.Generic.HashSet`1" diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index e95b48f133..0c80d98ed7 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +* [JS/TS] Add `ConditionalWeakTable` (by @chkn) + ## 1.3.0 - 2024-03-18 * [JS/TS] `Boolean.tryParse` should not crash on `null` string (@goswinr) diff --git a/src/fable-library-ts/ConditionalWeakTable.ts b/src/fable-library-ts/ConditionalWeakTable.ts new file mode 100644 index 0000000000..36ce2790e1 --- /dev/null +++ b/src/fable-library-ts/ConditionalWeakTable.ts @@ -0,0 +1,26 @@ + +export class ConditionalWeakTable { + private weakMap: WeakMap = new WeakMap(); + + public delete(key: TKey) { + return this.weakMap.delete(key); + } + + public get(key: TKey) { + return this.weakMap.get(key); + } + + public has(key: TKey) { + return this.weakMap.has(key); + } + + public set(key: TKey, value: TValue) { + return this.weakMap.set(key, value); + } + + public clear() { + this.weakMap = new WeakMap(); + } +} + +export default ConditionalWeakTable; diff --git a/src/fable-library-ts/MapUtil.ts b/src/fable-library-ts/MapUtil.ts index 058746958b..7946a0d632 100644 --- a/src/fable-library-ts/MapUtil.ts +++ b/src/fable-library-ts/MapUtil.ts @@ -1,4 +1,4 @@ -import { equals, IMap, ISet } from "./Util.js"; +import { equals, IMap, IMapOrWeakMap, ISet } from "./Util.js"; import { FSharpRef, Union } from "./Types.js"; const CaseRules = { @@ -88,7 +88,7 @@ export function containsValue(v: V, map: IMap) { return false; } -export function tryGetValue(map: IMap, key: K, defaultValue: FSharpRef): boolean { +export function tryGetValue(map: IMapOrWeakMap, key: K, defaultValue: FSharpRef): boolean { if (map.has(key)) { defaultValue.contents = map.get(key) as V; return true; @@ -104,7 +104,15 @@ export function addToSet(v: T, set: ISet) { return true; } -export function addToDict(dict: IMap, k: K, v: V) { +export function tryAddToDict(dict: IMapOrWeakMap, k: K, v: V) { + if (dict.has(k)) { + return false; + } + dict.set(k, v); + return true; +} + +export function addToDict(dict: IMapOrWeakMap, k: K, v: V) { if (dict.has(k)) { throw new Error("An item with the same key has already been added. Key: " + k); } @@ -118,3 +126,12 @@ export function getItemFromDict(map: IMap, key: K) { throw new Error(`The given key '${key}' was not present in the dictionary.`); } } + +export function getItemFromDictOrCreate(map: IMapOrWeakMap, key: K, createValue: (key: K) => V) { + if (map.has(key)) { + return map.get(key) as V; + } + const value = createValue(key); + map.set(key, value); + return value; +} diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index 9bcb089558..1f2e01d165 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -183,13 +183,18 @@ export interface ISet { values(): Iterable; } -export interface IMap { - clear(): void; +export interface IMapOrWeakMap { delete(key: K): boolean; - forEach(callbackfn: (value: V, key: K, map: IMap) => void, thisArg?: any): void; get(key: K): V | undefined; has(key: K): boolean; + set(key: K, value: V): IMapOrWeakMap; +} + +export interface IMap extends IMapOrWeakMap { + clear(): void; set(key: K, value: V): IMap; + + forEach(callbackfn: (value: V, key: K, map: IMap) => void, thisArg?: any): void; readonly size: number; [Symbol.iterator](): Iterator<[K, V]>; diff --git a/tests/Js/Main/ConditionalWeakTableTests.fs b/tests/Js/Main/ConditionalWeakTableTests.fs new file mode 100644 index 0000000000..691bd1bf61 --- /dev/null +++ b/tests/Js/Main/ConditionalWeakTableTests.fs @@ -0,0 +1,69 @@ +module Fable.Tests.ConditionalWeakTable + +open System +open System.Collections.Generic +open System.Runtime.CompilerServices +open Util +open Util.Testing + +let tests = + testList "ConditionalWeakTables" [ + testCase "ConditionalWeakTable.TryGetValue works" <| fun () -> + let key1, key2 = obj(), obj() + let expected1, expected2 = obj(), obj() + let dic1 = ConditionalWeakTable() + let dic2 = ConditionalWeakTable() + dic1.Add(key1, expected1) + dic2.Add(key2, expected2) + let success1, val1 = dic1.TryGetValue(key1) + let success2, val2 = dic1.TryGetValue(key2) + let success3, val3 = dic2.TryGetValue(key2) + let success4, val4 = dic2.TryGetValue(obj()) + equal success1 true + equal success2 false + equal success3 true + equal success4 false + equal val1 expected1 + equal val2 null + equal val3 expected2 + equal val4 null + + testCase "ConditionalWeakTable.Clear works" <| fun () -> + let dic = ConditionalWeakTable<_,_>() + let key1, key2 = obj(), obj() + dic.Add(key1, obj()) + dic.Add(key2, obj()) + dic.Clear() + dic.TryGetValue(key1) |> equal (false, null) + dic.TryGetValue(key2) |> equal (false, null) + + testCase "ConditionalWeakTable.Remove works" <| fun () -> + let dic = ConditionalWeakTable<_,_>() + let key1, key2 = obj(), obj() + dic.Add(key1, "Hello") + dic.Add(key2, "World!") + dic.Remove(key1) |> equal true + dic.Remove("C") |> equal false + dic.TryGetValue(key1) |> equal (false, null) + dic.TryGetValue(key2) |> equal (true, "World!") + + testCase "Adding 2 items with the same key throws" <| fun () -> + let dic = ConditionalWeakTable<_,_>() + let key = obj() + dic.Add(key, "foo") + #if FABLE_COMPILER + throwsError "An item with the same key has already been added. Key: [object Object]" (fun _ -> dic.Add(key, "bar")) + #else + throwsError "An item with the same key has already been added." (fun _ -> dic.Add(key, "bar")) + #endif + + testCase "ConditionalWeakTable.GetValue works" <| fun () -> + let dic = ConditionalWeakTable<_,_>() + let key1, key2 = obj(), obj() + let val1, val2 = obj(), obj() + dic.Add(key1, val1) + let value = dic.GetValue(key1, fun _ -> obj()) + value |> equal val1 + let value = dic.GetValue(key2, fun _ -> val2) + value |> equal val2 + ] \ No newline at end of file diff --git a/tests/Js/Main/Fable.Tests.fsproj b/tests/Js/Main/Fable.Tests.fsproj index 983cf5539c..26d71597c0 100644 --- a/tests/Js/Main/Fable.Tests.fsproj +++ b/tests/Js/Main/Fable.Tests.fsproj @@ -39,6 +39,7 @@ + diff --git a/tests/Js/Main/Main.fs b/tests/Js/Main/Main.fs index 17837f9aa3..5882ee29fc 100644 --- a/tests/Js/Main/Main.fs +++ b/tests/Js/Main/Main.fs @@ -10,6 +10,7 @@ let allTests = Async.tests Chars.tests Comparison.tests + ConditionalWeakTable.tests Convert.tests CustomOperators.tests DateTimeOffset.tests diff --git a/tests/TypeScript/Fable.Tests.TypeScript.fsproj b/tests/TypeScript/Fable.Tests.TypeScript.fsproj index 46a2de3147..3e237355bb 100644 --- a/tests/TypeScript/Fable.Tests.TypeScript.fsproj +++ b/tests/TypeScript/Fable.Tests.TypeScript.fsproj @@ -32,6 +32,7 @@ + diff --git a/tests/TypeScript/Main.fs b/tests/TypeScript/Main.fs index 37eacbc368..45658b58da 100644 --- a/tests/TypeScript/Main.fs +++ b/tests/TypeScript/Main.fs @@ -12,6 +12,7 @@ let allTests = Async.tests Chars.tests Comparison.tests + ConditionalWeakTable.tests Convert.tests CustomOperators.tests DateTimeOffset.tests