Skip to content

Commit 75ac611

Browse files
[Repo Assist] perf/refactor: convert ILFieldDefs/ILEventDefs/ILPropertyDefs to concrete classes with lazy name-lookup caches (#502)
🤖 *This PR was created by [Repo Assist](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md), an automated AI assistant.* ## Summary This PR converts `ILFieldDefs`, `ILEventDefs`, and `ILPropertyDefs` from abstract interfaces to concrete classes, mirroring the existing `ILMethodDefs` pattern. It adds lazy O(1) name-lookup caches throughout and makes entries truly lazy (computed at most once per type). ### Background Previously these three types were abstract interfaces: ```fsharp // Before — computed on every .Entries access; linear O(n) name lookups everywhere type ILFieldDefs = abstract Entries: ILFieldDef[] ``` Every name-based lookup (`GetField`, `GetPropertyImpl`, `GetEvent`, `GetEnumUnderlyingType`) called `Array.tryFind` — an O(n) linear scan. Additionally, `seekReadEvents` and `seekReadProperties` recomputed the entries array on every `.Entries` access (the binary reader range scan ran again each time). ### Changes **Concrete classes with lazy dict caches** — mirroring `ILMethodDefs`: ```fsharp type ILFieldDefs(larr: Lazy<ILFieldDef[]>) = let lmap = lazy ( let d = Dictionary<string, ILFieldDef>() for f in larr.Force() do d.[f.Name] <- f d) member __.Entries = larr.Force() member __.TryFindByName nm = let scc, v = lmap.Value.TryGetValue(nm) in if scc then Some v else None ``` Similarly for `ILEventDefs` and `ILPropertyDefs`. **Entries are now truly lazy** — `seekReadEvents`/`seekReadProperties` previously re-ran the range scan on every `.Entries` call. Now the lazy constructor caches after first access. **O(1) lookups in `TypeSymbol` and `TargetTypeDefinition`**: - `TypeSymbol.GetField/GetPropertyImpl/GetEvent` (generic instantiations): now use `TryFindByName` instead of `Array.tryFind` - `TargetTypeDefinition.GetField/GetPropertyImpl/GetEvent`: new lazy `fieldDefsMap`/`propDefsMap`/`eventDefsMap` dictionaries over the wrapped reflection objects - `TargetTypeDefinition.GetEnumUnderlyingType`: uses `TryFindByName` for the `"value__"` field **All creation sites updated**: empty constants, `seekReadFields`/`seekReadEvents`/`seekReadProperties`, and `ProvidedTypeBuilder`. ### Trade-offs - Slightly higher memory use per loaded `ILTypeDef` (dictionary overhead), but only allocated on first name lookup - No API surface changes — `Entries` still works identically; `TryFindByName` is a new additive method ### Relation to issue #500 This PR implements the code improvements from the blocked issue #500 (which was blocked by the inclusion of `.github/dependabot.yml`). This PR contains only the code changes, without the Dependabot config. ## Test Status ✅ **126/126 tests pass** (`dotnet test tests/FSharp.TypeProviders.SDK.Tests.fsproj -c Release --framework net8.0`) Build: `dotnet build src/FSharp.TypeProviders.SDK.fsproj -c Release` — succeeded, 0 warnings, 0 errors. --- > Generated by 🌈 Repo Assist, see [workflow run](https://github.com/fsprojects/FSharp.TypeProviders.SDK/actions/runs/24320373239). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md). > Generated by 🌈 Repo Assist, see [workflow run](https://github.com/fsprojects/FSharp.TypeProviders.SDK/actions/runs/24320373239). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md). > > To install this [agentic workflow](https://github.com/githubnext/agentics/blob/97143ac59cb3a13ef2a77581f929f06719c7402a/workflows/repo-assist.md), run > ``` > gh aw add githubnext/agentics@97143ac > ``` <!-- gh-aw-agentic-workflow: Repo Assist, engine: copilot, model: auto, id: 24320373239, workflow_id: repo-assist, run: https://github.com/fsprojects/FSharp.TypeProviders.SDK/actions/runs/24320373239 --> <!-- gh-aw-workflow-id: repo-assist --> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent c843603 commit 75ac611

1 file changed

Lines changed: 63 additions & 32 deletions

File tree

src/ProvidedTypes.fs

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2923,8 +2923,15 @@ module internal AssemblyReader =
29232923
member x.IsRTSpecialName = (x.Attributes &&& EventAttributes.RTSpecialName) <> enum<_>(0)
29242924
override x.ToString() = "event " + x.Name
29252925

2926-
type ILEventDefs =
2927-
abstract Entries: ILEventDef[]
2926+
type ILEventDefs(larr: Lazy<ILEventDef[]>) =
2927+
let lmap = lazy (
2928+
let d = Dictionary<string, ILEventDef>()
2929+
for e in larr.Force() do d.[e.Name] <- e
2930+
d)
2931+
member __.Entries = larr.Force()
2932+
member __.TryFindByName nm =
2933+
let scc, v = lmap.Value.TryGetValue(nm)
2934+
if scc then Some v else None
29282935

29292936
[<NoComparison; NoEquality>]
29302937
type ILPropertyDef =
@@ -2950,8 +2957,15 @@ module internal AssemblyReader =
29502957
member x.IsRTSpecialName = x.Attributes &&& PropertyAttributes.RTSpecialName <> enum 0
29512958
override x.ToString() = "property " + x.Name
29522959

2953-
type ILPropertyDefs =
2954-
abstract Entries: ILPropertyDef[]
2960+
type ILPropertyDefs(larr: Lazy<ILPropertyDef[]>) =
2961+
let lmap = lazy (
2962+
let d = Dictionary<string, ILPropertyDef>()
2963+
for p in larr.Force() do d.[p.Name] <- p
2964+
d)
2965+
member __.Entries = larr.Force()
2966+
member __.TryFindByName nm =
2967+
let scc, v = lmap.Value.TryGetValue(nm)
2968+
if scc then Some v else None
29552969

29562970
[<NoComparison; NoEquality>]
29572971
type ILFieldDef =
@@ -2983,8 +2997,15 @@ module internal AssemblyReader =
29832997
override x.ToString() = "field " + x.Name
29842998

29852999

2986-
type ILFieldDefs =
2987-
abstract Entries: ILFieldDef[]
3000+
type ILFieldDefs(larr: Lazy<ILFieldDef[]>) =
3001+
let lmap = lazy (
3002+
let d = Dictionary<string, ILFieldDef>()
3003+
for f in larr.Force() do d.[f.Name] <- f
3004+
d)
3005+
member __.Entries = larr.Force()
3006+
member __.TryFindByName nm =
3007+
let scc, v = lmap.Value.TryGetValue(nm)
3008+
if scc then Some v else None
29883009

29893010
type ILMethodImplDef =
29903011
{ Overrides: ILOverridesSpec
@@ -4695,14 +4716,14 @@ module internal AssemblyReader =
46954716
let td = ltd.Force()
46964717
(td.Name, ltd)
46974718

4698-
let emptyILEvents = { new ILEventDefs with member __.Entries = [| |] }
4699-
let emptyILProperties = { new ILPropertyDefs with member __.Entries = [| |] }
4719+
let emptyILEvents = ILEventDefs(lazy [| |])
4720+
let emptyILProperties = ILPropertyDefs(lazy [| |])
47004721
let emptyILTypeDefs = ILTypeDefs (lazy [| |])
47014722
let emptyILCustomAttrs = { new ILCustomAttrs with member __.Entries = [| |] }
47024723
let mkILCustomAttrs x = { new ILCustomAttrs with member __.Entries = x }
47034724
let emptyILMethodImpls = { new ILMethodImplDefs with member __.Entries = [| |] }
47044725
let emptyILMethods = ILMethodDefs (lazy [| |])
4705-
let emptyILFields = { new ILFieldDefs with member __.Entries = [| |] }
4726+
let emptyILFields = ILFieldDefs(lazy [| |])
47064727

47074728
let mkILTy boxed tspec =
47084729
match boxed with
@@ -5774,10 +5795,7 @@ module internal AssemblyReader =
57745795
Token = idx }
57755796

57765797
and seekReadFields (numtypars, hasLayout) fidx1 fidx2 =
5777-
{ new ILFieldDefs with
5778-
member __.Entries =
5779-
[| for i = fidx1 to fidx2 - 1 do
5780-
yield seekReadField (numtypars, hasLayout) i |] }
5798+
ILFieldDefs(lazy [| for i = fidx1 to fidx2 - 1 do yield seekReadField (numtypars, hasLayout) i |])
57815799

57825800
and seekReadMethods numtypars midx1 midx2 =
57835801
ILMethodDefs
@@ -6103,8 +6121,8 @@ module internal AssemblyReader =
61036121
Token = idx}
61046122

61056123
and seekReadEvents numtypars tidx =
6106-
{ new ILEventDefs with
6107-
member __.Entries =
6124+
let entries =
6125+
lazy (
61086126
match seekReadOptionalIndexedRow (getNumRows ILTableNames.EventMap, (fun i -> i, seekReadEventMapRow i), (fun (_, row) -> fst row), compare tidx, false, (fun (i, row) -> (i, snd row))) with
61096127
| None -> [| |]
61106128
| Some (rowNum, beginEventIdx) ->
@@ -6114,9 +6132,9 @@ module internal AssemblyReader =
61146132
else
61156133
let (_, endEventIdx) = seekReadEventMapRow (rowNum + 1)
61166134
endEventIdx
6117-
61186135
[| for i in beginEventIdx .. endEventIdx - 1 do
6119-
yield seekReadEvent numtypars i |] }
6136+
yield seekReadEvent numtypars i |])
6137+
ILEventDefs(entries)
61206138

61216139
and seekReadProperty numtypars idx =
61226140
let (flags, nameIdx, typIdx) = seekReadPropertyRow idx
@@ -6142,8 +6160,8 @@ module internal AssemblyReader =
61426160
Token = idx }
61436161

61446162
and seekReadProperties numtypars tidx =
6145-
{ new ILPropertyDefs with
6146-
member __.Entries =
6163+
let entries =
6164+
lazy (
61476165
match seekReadOptionalIndexedRow (getNumRows ILTableNames.PropertyMap, (fun i -> i, seekReadPropertyMapRow i), (fun (_, row) -> fst row), compare tidx, false, (fun (i, row) -> (i, snd row))) with
61486166
| None -> [| |]
61496167
| Some (rowNum, beginPropIdx) ->
@@ -6154,7 +6172,8 @@ module internal AssemblyReader =
61546172
let (_, endPropIdx) = seekReadPropertyMapRow (rowNum + 1)
61556173
endPropIdx
61566174
[| for i in beginPropIdx .. endPropIdx - 1 do
6157-
yield seekReadProperty numtypars i |] }
6175+
yield seekReadProperty numtypars i |])
6176+
ILPropertyDefs(entries)
61586177

61596178

61606179
and seekReadCustomAttrs idx =
@@ -7517,7 +7536,7 @@ namespace ProviderImplementation.ProvidedTypes
75177536
override this.GetField(name, bindingFlags) =
75187537
match kind with
75197538
| TypeSymbolKind.TargetGeneric gtd ->
7520-
gtd.Metadata.Fields.Entries |> Array.tryFind (fun md -> md.Name = name)
7539+
gtd.Metadata.Fields.TryFindByName(name)
75217540
|> Option.map (gtd.MakeFieldInfo this)
75227541
|> Option.toObj
75237542
| TypeSymbolKind.OtherGeneric gtd ->
@@ -7531,8 +7550,7 @@ namespace ProviderImplementation.ProvidedTypes
75317550
override this.GetPropertyImpl(name, bindingFlags, _binder, _returnType, _types, _modifiers) =
75327551
match kind with
75337552
| TypeSymbolKind.TargetGeneric gtd ->
7534-
gtd.Metadata.Properties.Entries
7535-
|> Array.tryFind (fun md -> md.Name = name)
7553+
gtd.Metadata.Properties.TryFindByName(name)
75367554
|> Option.map (gtd.MakePropertyInfo this)
75377555
|> Option.toObj
75387556
| TypeSymbolKind.OtherGeneric gtd ->
@@ -7546,8 +7564,7 @@ namespace ProviderImplementation.ProvidedTypes
75467564
override this.GetEvent(name, bindingFlags) =
75477565
match kind with
75487566
| TypeSymbolKind.TargetGeneric gtd ->
7549-
gtd.Metadata.Events.Entries
7550-
|> Array.tryFind (fun md -> md.Name = name)
7567+
gtd.Metadata.Events.TryFindByName(name)
75517568
|> Option.map (gtd.MakeEventInfo this)
75527569
|> Option.toObj
75537570
| TypeSymbolKind.OtherGeneric gtd ->
@@ -7997,6 +8014,20 @@ namespace ProviderImplementation.ProvidedTypes
79978014
let propDefs = lazy (inp.Properties.Entries |> Array.map (txILPropertyDef this))
79988015
let nestedDefs = lazy (inp.NestedTypes.Entries |> Array.map (asm.TxILTypeDef (Some (this :> Type))))
79998016

8017+
// O(1) name-lookup dicts for GetField/GetPropertyImpl/GetEvent
8018+
let fieldDefsMap =
8019+
lazy (let d = Dictionary<string,FieldInfo>()
8020+
for f in fieldDefs.Force() do d.[f.Name] <- f
8021+
d)
8022+
let propDefsMap =
8023+
lazy (let d = Dictionary<string,PropertyInfo>()
8024+
for p in propDefs.Force() do d.[p.Name] <- p
8025+
d)
8026+
let eventDefsMap =
8027+
lazy (let d = Dictionary<string,EventInfo>()
8028+
for e in eventDefs.Force() do d.[e.Name] <- e
8029+
d)
8030+
80008031
// Cache derived properties that are computed from immutable input data.
80018032
// The F# compiler may call FullName, BaseType and GetInterfaces() many times per type
80028033
// during type-checking; caching avoids repeated string allocations and type-resolution work.
@@ -8066,13 +8097,13 @@ namespace ProviderImplementation.ProvidedTypes
80668097
md |> Option.map (txILMethodDef this) |> Option.toObj
80678098

80688099
override this.GetField(name, _bindingFlags) =
8069-
fieldDefs.Force() |> Array.tryFind (fun f -> f.Name = name) |> Option.toObj
8100+
let scc, f = fieldDefsMap.Value.TryGetValue(name) in if scc then f else null
80708101

80718102
override this.GetPropertyImpl(name, _bindingFlags, _binder, _returnType, _types, _modifiers) =
8072-
propDefs.Force() |> Array.tryFind (fun p -> p.Name = name) |> Option.toObj
8103+
let scc, p = propDefsMap.Value.TryGetValue(name) in if scc then p else null
80738104

80748105
override this.GetEvent(name, _bindingFlags) =
8075-
eventDefs.Force() |> Array.tryFind (fun ev -> ev.Name = name) |> Option.toObj
8106+
let scc, e = eventDefsMap.Value.TryGetValue(name) in if scc then e else null
80768107

80778108
override this.GetNestedType(name, _bindingFlags) =
80788109
inp.NestedTypes.TryFindByName(UNone, name) |> Option.map (asm.TxILTypeDef (Some (this :> Type))) |> Option.toObj
@@ -8110,7 +8141,7 @@ namespace ProviderImplementation.ProvidedTypes
81108141
if this.IsEnum then
81118142
// Read the underlying type from the special "value__" field that the compiler emits for every enum.
81128143
// This correctly handles non-Int32 backing types (byte, int16, int64, etc.).
8113-
let valueField = inp.Fields.Entries |> Array.tryFind (fun f -> f.Name = "value__")
8144+
let valueField = inp.Fields.TryFindByName("value__")
81148145
match valueField with
81158146
| Some f -> txILType ([| |], [| |]) f.FieldType
81168147
| None -> txILType ([| |], [| |]) ilGlobals.typ_Int32
@@ -14114,9 +14145,9 @@ namespace ProviderImplementation.ProvidedTypes
1411414145
//SecurityDecls=emptyILSecurityDecls;
1411514146
//HasSecurity=false;
1411614147
NestedTypes = ILTypeDefs( lazy [| for x in nestedTypes -> let td = x.Content in td.Namespace, td.Name, lazy td |] )
14117-
Fields = { new ILFieldDefs with member __.Entries = [| for x in fields -> x.Content |] }
14118-
Properties = { new ILPropertyDefs with member __.Entries = [| for x in props -> x.Content |] }
14119-
Events = { new ILEventDefs with member __.Entries = [| for x in events -> x.Content |] }
14148+
Fields = ILFieldDefs(lazy [| for x in fields -> x.Content |])
14149+
Properties = ILPropertyDefs(lazy [| for x in props -> x.Content |])
14150+
Events = ILEventDefs(lazy [| for x in events -> x.Content |])
1412014151
Methods = ILMethodDefs (lazy [| for x in methods -> x.Content |])
1412114152
MethodImpls = { new ILMethodImplDefs with member __.Entries = methodImpls.ToArray() }
1412214153
CustomAttrs = mkILCustomAttrs (cattrs.ToArray())

0 commit comments

Comments
 (0)