| | | 1 | | namespace FSharp.Interop.Dlr |
| | | 2 | | |
| | | 3 | | open System |
| | | 4 | | open System.Dynamic |
| | | 5 | | open System.Linq.Expressions |
| | | 6 | | open System.Reflection |
| | | 7 | | open System.Runtime.CompilerServices |
| | | 8 | | open Microsoft.CSharp.RuntimeBinder |
| | | 9 | | open FSharp.Quotations |
| | | 10 | | |
| | | 11 | | /// The call sites of an operation whose binder inputs are only known at run time (`(?) x name` |
| | | 12 | | /// with `name` a variable): one set of sites per distinct key, created on first use from a |
| | | 13 | | /// quotation template the translator built for the operation, and read back out of it. The |
| | | 14 | | /// delegate that invokes them is compiled once, at translation time, with the sites as |
| | | 15 | | /// parameters — so a new key costs the binders and sites (~µs, a few hundred bytes), not a |
| | | 16 | | /// `LambdaExpression.Compile()`, and a repeated key costs one dictionary lookup. Bounded: at |
| | | 17 | | /// `Capacity` entries the cache is cleared and refills, a miss being cheap. |
| | | 18 | | /// Not part of the supported API: public only because compiled blocks call it, and it may change in any release. |
| | | 19 | | [<System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)>] |
| | | 20 | | type SiteCache<'Key when 'Key: equality>(template: 'Key -> Expr) = |
| | | 21 | | let entries = System.Collections.Concurrent.ConcurrentDictionary<'Key, CallSite[]>() |
| | | 22 | | |
| | | 23 | | /// The `CallSite` constants in a template's expression, one per distinct site, in traversal |
| | | 24 | | /// order — the order the translator's parameters follow. |
| | | 25 | | static member Sites(e: Expr) : CallSite list = |
| | | 26 | | let found = System.Collections.Generic.List<CallSite>() |
| | | 27 | | let rec walk (e: Expr) = |
| | | 28 | | match e with |
| | | 29 | | | Patterns.Value(v, _) -> |
| | | 30 | | match v with |
| | | 31 | | | :? CallSite as site when not (found |> Seq.exists (fun s -> obj.ReferenceEquals(s, site))) -> found.Ad |
| | | 32 | | | _ -> () |
| | | 33 | | | ExprShape.ShapeVar _ -> () |
| | | 34 | | | ExprShape.ShapeLambda(_, body) -> walk body |
| | | 35 | | | ExprShape.ShapeCombination(_, args) -> List.iter walk args |
| | | 36 | | walk e |
| | | 37 | | List.ofSeq found |
| | | 38 | | |
| | | 39 | | /// Entries kept per cache before it is cleared. |
| | | 40 | | static member val Capacity = 256 with get, set |
| | | 41 | | |
| | | 42 | | member _.Count = entries.Count |
| | | 43 | | |
| | | 44 | | member _.Get(key: 'Key) : CallSite[] = |
| | | 45 | | match entries.TryGetValue key with |
| | | 46 | | | true, sites -> sites |
| | | 47 | | | _ -> |
| | | 48 | | // Misses only: the admission (clear at capacity, then add) is one critical section, |
| | | 49 | | // so concurrent misses cannot each pass the check and push the count past the bound. |
| | | 50 | | lock entries (fun () -> |
| | | 51 | | match entries.TryGetValue key with |
| | | 52 | | | true, sites -> sites |
| | | 53 | | | _ -> |
| | | 54 | | // A key from data: a null name or type is an argument error here, not a |
| | | 55 | | // NullReferenceException from inside the binder. |
| | | 56 | | match box key with |
| | | 57 | | | :? (string * Type list) as k -> |
| | | 58 | | let name, types = k |
| | | 59 | | if isNull name then nullArg "a computed member name is null" |
| | | 60 | | if isNull (box types) then nullArg "Dlr.typeArgsOf: the list is null" |
| | | 61 | | if types |> List.exists isNull then invalidArg "types" "Dlr.typeArgsOf: a type in the list is nu |
| | | 62 | | | _ -> () |
| | | 63 | | if entries.Count >= SiteCache<'Key>.Capacity then entries.Clear() |
| | | 64 | | let sites = Array.ofList (SiteCache<'Key>.Sites(template key)) |
| | | 65 | | entries.[key] <- sites |
| | | 66 | | sites) |
| | | 67 | | |
| | | 68 | | /// `sites.[i]`, for the quotation (array indexing has no direct quotation form the converter takes). |
| | | 69 | | static member At(sites: CallSite[], i: int) : CallSite = sites.[i] |
| | | 70 | | |
| | | 71 | | /// One compiled `NamedOfCache` shape: its names (an empty name per positional), their hash, the delegate. |
| | | 72 | | /// Not part of the supported API: public only because compiled blocks call it, and it may change in any release. |
| | | 73 | | [<AllowNullLiteral; System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)>] |
| | | 74 | | type NamedOfEntry(names: string[], hash: int, d: Delegate) = |
| | | 75 | | member _.Names = names |
| | | 76 | | member _.Hash = hash |
| | | 77 | | member _.Delegate = d |
| | | 78 | | |
| | | 79 | | /// For `Dlr.argsOf` / `Dlr.namedOf`: the site's operation compiled once per distinct argument |
| | | 80 | | /// shape — the ordered names, an empty name standing for a positional argument (`argsOf` |
| | | 81 | | /// values first, then `namedOf` names) — since the shape changes the site's arity, so the |
| | | 82 | | /// whole delegate is per key, not only its sites; bounded like `SiteCache`. The delegate takes |
| | | 83 | | /// the target, the fixed arguments and the splatted values as one `obj[]` (positional, then |
| | | 84 | | /// named). |
| | | 85 | | /// Not part of the supported API: public only because compiled blocks call it, and it may change in any release. |
| | | 86 | | [<System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)>] |
| | | 87 | | type NamedOfCache(compile: string list -> Delegate) = |
| | | 88 | | /// The entries by the hash of their names, an immutable snapshot replaced whole under the |
| | | 89 | | /// lock on a miss, so a lookup reads it without locking; the hash is computed in place from |
| | | 90 | | /// the argument lists, so a lookup allocates nothing. |
| | 188 | 91 | | let mutable entries : System.Collections.Generic.Dictionary<int, NamedOfEntry list> = System.Collections.Generic.Dic |
| | 188 | 92 | | let mutable count = 0 |
| | | 93 | | /// The last two shapes served: a site that repeats or alternates shapes hits here in one or |
| | | 94 | | /// two name compares, whatever the capacity. |
| | 188 | 95 | | let mutable last : NamedOfEntry = null |
| | 188 | 96 | | let mutable previous : NamedOfEntry = null |
| | | 97 | | |
| | | 98 | | /// Entries kept per cache before it is cleared: a miss is a `Compile()`. The lookup is a |
| | | 99 | | /// hash, so hits cost the same at any size; the bound is memory on a site that fills it |
| | | 100 | | /// (a shape is ~13 KB at one arity: 256 is ~3.5 MB), the same 256 as `SiteCache`. |
| | | 101 | | static member val Capacity = 256 with get, set |
| | | 102 | | |
| | | 103 | | /// The most positional arguments `Dlr.argsOf` accepts. Each distinct count is a new call-site |
| | | 104 | | /// arity — a delegate type (past Func's 17 type parameters, one emitted into a non-collectible dynamic |
| | | 105 | | /// assembly), a binder Microsoft.CSharp interns for the life of the process, a `Compile()` — |
| | | 106 | | /// so a count from data must not be unbounded, as a C# call site's arity is fixed by its |
| | | 107 | | /// source. A collection that could be long is one argument, not many. |
| | | 108 | | static member val MaxPositional = 64 with get, set |
| | | 109 | | |
| | 0 | 110 | | member _.Count = count |
| | | 111 | | |
| | | 112 | | /// The hash of a shape: the positional count, then each name in order. |
| | | 113 | | static member private HashOf(positional: obj list, pairs: (string * obj) list) = |
| | 1394 | 114 | | let mutable h = 17 |
| | 7018 | 115 | | for _ in positional do h <- h * 31 |
| | 8858 | 116 | | for (n, _) in pairs do h <- h * 31 + (if isNull n then 0 else n.GetHashCode()) |
| | 1394 | 117 | | h |
| | | 118 | | |
| | | 119 | | /// Whether an entry's names are the shape of these arguments: `positional.Length` empty |
| | | 120 | | /// names, then the pairs' names in order. Loops, no closures: the hit path allocates nothing. |
| | | 121 | | static member private Matches(names: string[], positional: obj list, pairs: (string * obj) list) = |
| | 3646 | 122 | | let mutable i = 0 |
| | 3646 | 123 | | let mutable ok = true |
| | 3646 | 124 | | let mutable ps = positional |
| | 16434 | 125 | | while ok && not ps.IsEmpty do |
| | 8988 | 126 | | if i < names.Length && names.[i].Length = 0 then i <- i + 1; ps <- ps.Tail |
| | 132 | 127 | | else ok <- false |
| | 3646 | 128 | | let mutable rest = pairs |
| | 19114 | 129 | | while ok && not rest.IsEmpty do |
| | 3602 | 130 | | let (n, _) = rest.Head |
| | | 131 | | // An empty name is the positional marker in `names`: a pair never matches one. |
| | 16612 | 132 | | if i < names.Length && not (String.IsNullOrEmpty n) && String.Equals(n, names.[i]) then i <- i + 1; rest <- |
| | 2498 | 133 | | else ok <- false |
| | 4662 | 134 | | ok && i = names.Length |
| | | 135 | | |
| | | 136 | | /// The entry of this shape in a bucket, or null: a loop, no closure, no option, so the hash |
| | | 137 | | /// path allocates nothing. |
| | | 138 | | static member private Find(bucket: NamedOfEntry list, hash: int, positional: obj list, pairs: (string * obj) list) : |
| | 108 | 139 | | let mutable rest = bucket |
| | 108 | 140 | | let mutable found : NamedOfEntry = null |
| | 540 | 141 | | while isNull found && not rest.IsEmpty do |
| | 108 | 142 | | let e = rest.Head |
| | 432 | 143 | | if e.Hash = hash && NamedOfCache.Matches(e.Names, positional, pairs) then found <- e |
| | 108 | 144 | | rest <- rest.Tail |
| | 108 | 145 | | found |
| | | 146 | | |
| | | 147 | | /// The delegate for these arguments' shape. |
| | | 148 | | member this.Get(positional: obj list, pairs: (string * obj) list) : Delegate = |
| | 2194 | 149 | | if isNull (box positional) then nullArg "Dlr.argsOf: the list is null" |
| | 2198 | 150 | | if isNull (box pairs) then nullArg "Dlr.namedOf: the list is null" |
| | | 151 | | // The last two shapes served, compared by name first: cheaper than hashing the names. |
| | 2190 | 152 | | let l = last |
| | 5671 | 153 | | if not (isNull l) && NamedOfCache.Matches(l.Names, positional, pairs) then l.Delegate |
| | | 154 | | else |
| | 1794 | 155 | | let p = previous |
| | 5439 | 156 | | if not (isNull p) && NamedOfCache.Matches(p.Names, positional, pairs) then previous <- l; last <- p; p.Delegate |
| | | 157 | | else |
| | 1394 | 158 | | let hash = NamedOfCache.HashOf(positional, pairs) |
| | 1394 | 159 | | let mutable bucket = Unchecked.defaultof<NamedOfEntry list> // out parameter: no tuple |
| | 2788 | 160 | | let hit = if entries.TryGetValue(hash, &bucket) then NamedOfCache.Find(bucket, hash, positional, pairs) else nul |
| | 1718 | 161 | | if not (isNull hit) then previous <- last; last <- hit; hit.Delegate |
| | | 162 | | else |
| | 1286 | 163 | | lock this (fun () -> |
| | 1286 | 164 | | let current = entries |
| | 2572 | 165 | | let again = match current.TryGetValue hash with | true, b -> NamedOfCache.Find(b, hash, positional, pair |
| | 1286 | 166 | | if not (isNull again) then previous <- last; last <- again; again.Delegate |
| | 1286 | 167 | | else |
| | 1286 | 168 | | // Names from data: a null or empty one would be taken for a positional slot. |
| | 4972 | 169 | | for (n, _) in pairs do |
| | 1210 | 170 | | if isNull n then nullArg "Dlr.namedOf: an argument name is null" |
| | 1210 | 171 | | if n.Length = 0 then invalidArg "pairs" "Dlr.namedOf: an argument name is empty (positional argu |
| | 1274 | 172 | | let positionalCount = List.length positional |
| | 1274 | 173 | | if positionalCount > NamedOfCache.MaxPositional then |
| | 4 | 174 | | invalidArg "positional" (sprintf "Dlr.argsOf: %d positional arguments; at most %d. Each distinct |
| | 4744 | 175 | | let names = (positional |> List.map (fun _ -> "")) @ (pairs |> List.map fst) |
| | 1270 | 176 | | let e = NamedOfEntry(Array.ofList names, hash, compile names) |
| | 1286 | 177 | | let next = |
| | 1278 | 178 | | if count >= NamedOfCache.Capacity then (count <- 0; System.Collections.Generic.Dictionary()) |
| | 1266 | 179 | | else System.Collections.Generic.Dictionary(current) |
| | 3810 | 180 | | next.[hash] <- e :: (match next.TryGetValue hash with | true, b -> b | _ -> []) |
| | 1270 | 181 | | count <- count + 1 |
| | 1270 | 182 | | entries <- next |
| | 1270 | 183 | | previous <- last |
| | 1270 | 184 | | last <- e |
| | 2556 | 185 | | e.Delegate) |
| | | 186 | | |
| | | 187 | | /// The splatted values, positional then named, for the quotation. |
| | | 188 | | static member Values(positional: obj list, pairs: (string * obj) list) : obj[] = |
| | 2174 | 189 | | let values = Array.zeroCreate (List.length positional + List.length pairs) |
| | 2174 | 190 | | let mutable i = 0 |
| | 8752 | 191 | | for v in positional do |
| | 2202 | 192 | | values.[i] <- v |
| | 2202 | 193 | | i <- i + 1 |
| | 8936 | 194 | | for (_, v) in pairs do |
| | 2294 | 195 | | values.[i] <- v |
| | 2294 | 196 | | i <- i + 1 |
| | 2174 | 197 | | values |
| | 4836 | 198 | | static member At(values: obj[], i: int) : obj = values.[i] |