Skip to content

Commit

Permalink
[JS] Add ConditionalWeakTable (#3782)
Browse files Browse the repository at this point in the history
* Add ConditionalWeakTable implementation for JS

* Add ConditionalWeakTableTests

* Update changelog

---------

Co-authored-by: Maxime Mangel <mangel.maxime@protonmail.com>
  • Loading branch information
chkn and MangelMaxime authored Mar 20, 2024
1 parent ca15c97 commit 31406dc
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 6 deletions.
4 changes: 4 additions & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions src/Fable.Transforms/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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", _, _ ->
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/Fable.Transforms/Transforms.Util.fs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@ module Types =
[<Literal>]
let ireadonlydictionary = "System.Collections.Generic.IReadOnlyDictionary`2"

[<Literal>]
let conditionalWeakTable = "System.Runtime.CompilerServices.ConditionalWeakTable`2"

[<Literal>]
let hashset = "System.Collections.Generic.HashSet`1"

Expand Down
4 changes: 4 additions & 0 deletions src/fable-library-ts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions src/fable-library-ts/ConditionalWeakTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

export class ConditionalWeakTable<TKey extends object, TValue> {
private weakMap: WeakMap<TKey, TValue> = new WeakMap<TKey, TValue>();

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<TKey, TValue>();
}
}

export default ConditionalWeakTable;
23 changes: 20 additions & 3 deletions src/fable-library-ts/MapUtil.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -88,7 +88,7 @@ export function containsValue<K, V>(v: V, map: IMap<K, V>) {
return false;
}

export function tryGetValue<K, V>(map: IMap<K, V>, key: K, defaultValue: FSharpRef<V>): boolean {
export function tryGetValue<K, V>(map: IMapOrWeakMap<K, V>, key: K, defaultValue: FSharpRef<V>): boolean {
if (map.has(key)) {
defaultValue.contents = map.get(key) as V;
return true;
Expand All @@ -104,7 +104,15 @@ export function addToSet<T>(v: T, set: ISet<T>) {
return true;
}

export function addToDict<K, V>(dict: IMap<K, V>, k: K, v: V) {
export function tryAddToDict<K, V>(dict: IMapOrWeakMap<K, V>, k: K, v: V) {
if (dict.has(k)) {
return false;
}
dict.set(k, v);
return true;
}

export function addToDict<K, V>(dict: IMapOrWeakMap<K, V>, k: K, v: V) {
if (dict.has(k)) {
throw new Error("An item with the same key has already been added. Key: " + k);
}
Expand All @@ -118,3 +126,12 @@ export function getItemFromDict<K, V>(map: IMap<K, V>, key: K) {
throw new Error(`The given key '${key}' was not present in the dictionary.`);
}
}

export function getItemFromDictOrCreate<K, V>(map: IMapOrWeakMap<K, V>, 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;
}
11 changes: 8 additions & 3 deletions src/fable-library-ts/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,18 @@ export interface ISet<T> {
values(): Iterable<T>;
}

export interface IMap<K, V> {
clear(): void;
export interface IMapOrWeakMap<K, V> {
delete(key: K): boolean;
forEach(callbackfn: (value: V, key: K, map: IMap<K, V>) => void, thisArg?: any): void;
get(key: K): V | undefined;
has(key: K): boolean;
set(key: K, value: V): IMapOrWeakMap<K, V>;
}

export interface IMap<K, V> extends IMapOrWeakMap<K, V> {
clear(): void;
set(key: K, value: V): IMap<K, V>;

forEach(callbackfn: (value: V, key: K, map: IMap<K, V>) => void, thisArg?: any): void;
readonly size: number;

[Symbol.iterator](): Iterator<[K, V]>;
Expand Down
69 changes: 69 additions & 0 deletions tests/Js/Main/ConditionalWeakTableTests.fs
Original file line number Diff line number Diff line change
@@ -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
]
1 change: 1 addition & 0 deletions tests/Js/Main/Fable.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<Compile Include="AsyncTests.fs" />
<Compile Include="CharTests.fs" />
<Compile Include="ComparisonTests.fs" />
<Compile Include="ConditionalWeakTableTests.fs" />
<Compile Include="ConvertTests.fs" />
<Compile Include="CustomOperatorsTests.fs" />
<Compile Include="DateTimeOffsetTests.fs" />
Expand Down
1 change: 1 addition & 0 deletions tests/Js/Main/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ let allTests =
Async.tests
Chars.tests
Comparison.tests
ConditionalWeakTable.tests
Convert.tests
CustomOperators.tests
DateTimeOffset.tests
Expand Down
1 change: 1 addition & 0 deletions tests/TypeScript/Fable.Tests.TypeScript.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<Compile Include="../Js/Main/AsyncTests.fs" />
<Compile Include="../Js/Main/CharTests.fs" />
<Compile Include="../Js/Main/ComparisonTests.fs" />
<Compile Include="../Js/Main/ConditionalWeakTableTests.fs" />
<Compile Include="../Js/Main/ConvertTests.fs" />
<Compile Include="../Js/Main/CustomOperatorsTests.fs" />
<Compile Include="../Js/Main/DateTimeOffsetTests.fs" />
Expand Down
1 change: 1 addition & 0 deletions tests/TypeScript/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ let allTests =
Async.tests
Chars.tests
Comparison.tests
ConditionalWeakTable.tests
Convert.tests
CustomOperators.tests
DateTimeOffset.tests
Expand Down

0 comments on commit 31406dc

Please sign in to comment.