Generator Extensibility — Wholesale Refactor Plan
Goal
Replace the ad-hoc Interceptors fields on GeneratorContext.Customisation with a single, uniform hook model that:
- Lets consumers intercept and rewrite type-definition rendering and type-ref rendering at well-defined pipeline stages.
- Composes multiple consumers deterministically.
- Costs one branch and zero allocations on the no-extension path.
- Carries enough ambient context (position, owner, render mode) that a handler can disambiguate heritage refs, type args, return types, etc.
Non-goal: preserving the existing four bespoke interceptor fields. They are subsumed by the new model.
Breaking changes
This is a wholesale break of the public Customisation / Interceptors API. Every downstream consumer of Interceptors.IgnorePathRender, Interceptors.Paths.*, Interceptors.ResolvedTypePrelude, Interceptors.AnchoredRender, and the kind-keyed pipe* helpers must migrate to the new slot-based API. In-repo consumers known to break:
src/Xantham.Generator/Generator/Render.fs(the live customisation, not just an example).src/Xantham.Fable/Program.fs(audit during migration).tests/Xantham.Generator.Tests/**andtests/Xantham.Fable.Tests/**(audit during migration).
The migration is mechanical but not optional. There is no compatibility shim.
Pipeline
The generator has seven natural transformation boundaries. Every hook attaches to one of them:
Stage |
Input |
Output |
Skippable |
Replaces today |
|---|---|---|---|---|
|
|
|
yes |
|
|
|
|
no |
(new — covers encoder policies) |
|
|
|
no |
(new) |
|
|
|
yes |
|
|
|
|
yes |
|
|
|
|
no |
(new) |
|
|
|
yes |
|
Build = AST-stage transformation (cache-friendly, idempotent). Emit = render-stage transformation (wraps the produced widget — attributes, annotations).
TypeDefBuild and Anchored are split into per-shape slots (see the Customisation record under "Core types") so handlers don't have to match-and-ignore irrelevant cases. TypeDefEmit is intentionally not split: by the time we have a WidgetBuilder<TypeDefn> the shape distinction has been erased, so a single slot is the natural type.
Member-level interception (rename, skip, decorate methods/properties/fields) is not a separate slot. Members are part of Concrete.TypeLikeRender; consumers register a TypeDefBuildClass handler that returns a Replace with the transformed member list. Same for record fields and enum cases via their respective TypeDefBuild* slots.
RenderScopeBuild and TypeDefBuild* divide today's ResolvedTypePrelude along its real seams: scope-shell concerns (anchor, render mode, prelude metadata) go to RenderScopeBuild; typed payload concerns go to the matching TypeDefBuild*. Today's hook conflated both.
Core types
Two slot variants — one that admits Skip, one that doesn't — so the type system enforces the table above. F# disambiguates the case names by type, so both DUs use plain Pass / Replace / Skip:
type HookResult<'T> =
| Pass // identity; chain continues with the running value unchanged
| Replace of 'T // swap the value; subsequent handlers see 'T
type SkippableHookResult<'T> =
| Pass
| Replace of 'T
| Skip // drop this node entirely (export elision, member skip, ...)
type Hook<'T> = GeneratorContext -> RenderContext -> 'T -> HookResult<'T>
type SkippableHook<'T> = GeneratorContext -> RenderContext -> 'T -> SkippableHookResult<'T>
[<Struct>]
type RenderContext = {
Position : RenderPosition // a DU sum: TypeRef positions ∪ Path positions, see below
Owner : ResolvedType voption
Render : RenderMode // the existing render-mode discriminator (RefOnly | Concrete | Transient), propagated from the active RenderScope
Stage : RenderStage // tag, debug only
}
and RenderPosition =
| RefPos of TypeRefPosition
| PathPos of PathPosition
| NotApplicable // for Emit/Anchored slots where position is meaningless
and TypeRefPosition =
| Standalone
| InheritanceRef
| TypeArg
| TupleElement
| UnionMember
| FunctionParameter
| FunctionReturn
| AliasTarget
| MemberType
and PathPosition =
| TopLevelType // PathResolution: a top-level type path
| MemberPath // PathResolution: a member's anchor path
| VariablePath // PathResolution: a top-level variable
| FunctionPath // PathResolution: a top-level function
and RenderStage =
| PathResolution | TypeRefBuild | TypeRefEmit
| RenderScopeBuild | TypeDefBuild | TypeDefEmit | Anchored
Owner semantics:
PathResolutionwithPathPos TopLevelType⇒ValueSome <the type itself>(the resolved type for which the path is being computed).PathResolutionwithPathPos MemberPath⇒ValueSome <containing type>.PathResolutionwithPathPos VariablePath/PathPos FunctionPath⇒ValueNone.TypeRef*slots ⇒ValueSomeof the surrounding type when the ref is rendered inside a member/heritage/return position;ValueNonefor free-standing refs.RenderScopeBuild/TypeDefBuild*/Anchored*⇒ValueSome <the type the scope/payload describes>.
type HookSlot<'T> = { Handlers : Hook<'T> list; HasAny : bool }
type SkippableHookSlot<'T> = { Handlers : SkippableHook<'T> list; HasAny : bool }
type Customisation = {
PathResolution : SkippableHookSlot<TypePath>
TypeRefBuild : HookSlot<TypeRefRender>
TypeRefEmit : HookSlot<WidgetBuilder<Type>>
RenderScopeBuild : SkippableHookSlot<RenderScope>
TypeDefBuildClass : SkippableHookSlot<Concrete.TypeLikeRender>
TypeDefBuildAlias : SkippableHookSlot<Concrete.TypeAliasRender>
TypeDefBuildEnum : SkippableHookSlot<Concrete.LiteralUnionRender<int>>
TypeDefBuildStringUnion: SkippableHookSlot<Concrete.LiteralUnionRender<TsLiteral>>
TypeDefEmit : HookSlot<WidgetBuilder<TypeDefn>>
AnchoredRef : SkippableHookSlot<Anchored.TypeRefRender>
AnchoredScope : SkippableHookSlot<Anchored.RenderScope>
}
PathResolution carries kind via PathPosition plus Owner for the containing type. This replaces the six pipe* helpers without growing the slot table.
Interpreter
run is inline only as far as the HasAny check; the recursive walker is a module-level let rec so the JIT sees a normal tail-recursive function rather than an inlined-but-escaping closure.
Tokens are the boxed handler reference plus a small slot tag (so Customisation.remove knows which slot to rebuild). Comparison is Object.ReferenceEquals on the boxed function. No counter, no global state:
type SlotId =
| PathResolutionSlot
| TypeRefBuildSlot | TypeRefEmitSlot
| RenderScopeBuildSlot
| TypeDefBuildClassSlot | TypeDefBuildAliasSlot
| TypeDefBuildEnumSlot | TypeDefBuildStringUnionSlot
| TypeDefEmitSlot
| AnchoredRefSlot | AnchoredScopeSlot
type HandlerToken = HandlerToken of SlotId * obj
type HookSlot<'T> = { Handlers : Hook<'T> list; HasAny : bool }
type SkippableHookSlot<'T> = { Handlers : SkippableHook<'T> list; HasAny : bool }
module Hook =
let ofMap (f: 'T -> 'T) : Hook<'T> = fun _ _ v -> Replace (f v)
module SkippableHook =
let ofMap (f: 'T -> 'T) : SkippableHook<'T> =
fun _ _ v -> SkippableHookResult.Replace (f v)
// Convenience for predicate-driven Skip:
let ofPredicate (shouldSkip: 'T -> bool) : SkippableHook<'T> =
fun _ _ v -> if shouldSkip v then SkippableHookResult.Skip else SkippableHookResult.Pass
module HookSlot =
let empty<'T> : HookSlot<'T> = { Handlers = []; HasAny = false }
// Chainable: most consumers don't need a token.
let add (h: Hook<'T>) (slot: HookSlot<'T>) : HookSlot<'T> =
{ Handlers = h :: slot.Handlers; HasAny = true }
// Tracked: returns the slot plus the boxed handler reference for use as a token.
// The `Customisation.add{Slot}Tracked` wrapper is responsible for tagging the
// boxed reference with its SlotId before exposing a `HandlerToken`.
let addTracked (h: Hook<'T>) (slot: HookSlot<'T>) : HookSlot<'T> * obj =
{ Handlers = h :: slot.Handlers; HasAny = true }, box h
let remove (token: obj) (slot: HookSlot<'T>) : HookSlot<'T> =
let h' = slot.Handlers |> List.filter (fun h -> not (System.Object.ReferenceEquals(box h, token)))
{ Handlers = h'; HasAny = not (List.isEmpty h') }
let clear<'T> : HookSlot<'T> -> HookSlot<'T> = fun _ -> empty<'T>
let rec private loop result current handlers ctx rctx =
match handlers with
| [] -> result
| h :: rest ->
match h ctx rctx current with
| Pass -> loop result current rest ctx rctx
| Replace v -> loop (Replace v) v rest ctx rctx
let inline run slot ctx rctx value =
if not slot.HasAny then Pass
else loop Pass value slot.Handlers ctx rctx
module SkippableHookSlot =
let empty<'T> : SkippableHookSlot<'T> = { Handlers = []; HasAny = false }
let add (h: SkippableHook<'T>) (slot: SkippableHookSlot<'T>) : SkippableHookSlot<'T> =
{ Handlers = h :: slot.Handlers; HasAny = true }
let addTracked (h: SkippableHook<'T>) (slot: SkippableHookSlot<'T>) : SkippableHookSlot<'T> * obj =
{ Handlers = h :: slot.Handlers; HasAny = true }, box h
let remove (token: obj) (slot: SkippableHookSlot<'T>) : SkippableHookSlot<'T> =
let h' = slot.Handlers |> List.filter (fun h -> not (System.Object.ReferenceEquals(box h, token)))
{ Handlers = h'; HasAny = not (List.isEmpty h') }
let clear<'T> : SkippableHookSlot<'T> -> SkippableHookSlot<'T> = fun _ -> empty<'T>
let rec private loop result current handlers ctx rctx =
match handlers with
| [] -> result
| h :: rest ->
match h ctx rctx current with
| SkippableHookResult.Pass -> loop result current rest ctx rctx
| SkippableHookResult.Replace v -> loop (SkippableHookResult.Replace v) v rest ctx rctx
| SkippableHookResult.Skip -> SkippableHookResult.Skip
let inline run slot ctx rctx value =
if not slot.HasAny then SkippableHookResult.Pass
else loop SkippableHookResult.Pass value slot.Handlers ctx rctx
Customisation exposes both add{Slot} (chainable, returns Customisation) and add{Slot}Tracked (returns Customisation * HandlerToken where the token wraps the slot tag). Customisation.remove : HandlerToken -> Customisation -> Customisation matches on the slot tag and delegates to HookSlot.remove / SkippableHookSlot.remove on that field. Most call sites use the chainable form and never touch tokens.
Truth table
h₁ result |
h₂ result |
h₃ result |
Final |
|
Notes |
|---|---|---|---|---|---|
|
|
— |
|
original |
|
|
|
— |
|
|
|
|
|
— |
|
|
|
|
|
— |
|
|
h₂ saw |
|
|
|
|
|
h₃ saw |
|
|
— |
|
(elide) |
|
|
(anything) |
— |
|
(chain stops) |
no "un-skip" |
Ordering
add prepends, so registration order is reverse of execution order:
register a; register b; register c
Handlers = [c; b; a]
Execution: c
The latest-registered handler runs first and sees the original input. Earlier handlers see whatever the later ones produced. This matches "latest registration wraps everything underneath" — useful when an application registers a base policy at startup and tests layer overrides on top.
HookSlot.add is chainable (no token). HookSlot.addTracked returns the slot plus the boxed handler reference; Customisation.add{Slot}Tracked wraps that with a SlotId to produce a HandlerToken. HookSlot.clear resets a slot to empty.
Consumer usage
End-to-end registration looks like this:
let customisation =
Customisation.Default
|> Customisation.addTypeRefBuild encoderInvariantPolicy // Hook<TypeRefRender>
|> Customisation.addPathResolution renameMembers // SkippableHook<TypePath>
|> Customisation.addTypeDefBuildClass injectAttribute // SkippableHook<TypeLikeRender>
// For dynamic reconfiguration (tests, REPL):
let customisation', token =
customisation |> Customisation.addAnchoredScopeTracked debugProbe
// ... later:
let customisation'' = Customisation.remove token customisation'
Call-site pattern
Non-skippable slot:
let rctx = {
Position = RefPos TypeArg
Owner = ValueSome owner
Render = scope.Render
Stage = TypeRefBuild
}
let render =
match HookSlot.run ctx.Customisation.TypeRefBuild ctx rctx render with
| Pass -> render
| Replace v -> v
Skippable slot:
match SkippableHookSlot.run ctx.Customisation.PathResolution ctx rctx path with
| SkippableHookResult.Pass -> emit path
| SkippableHookResult.Replace p -> emit p
| SkippableHookResult.Skip -> () // elide
Performance
- Per-slot
HasAny: a singleboolfield per slot. The no-handler path is one branch and zero allocations. - Struct
RenderContext: passed by value orinref— no heap allocation per node. - Inlining:
runinlines theHasAnycheck; the loop is module-levellet recso the JIT applies normal tail-call optimisation rather than relying on closure inlining. - Cache ordering:
Anchored*runs beforeAnchorRenderswrite.RenderScopeBuildandTypeDefBuild*run beforePreludeRenderswrite. Today's ordering preserved. - SRTP smart constructors in
Types/RenderScope.Prelude.fsstay inline at the leaves — hooks attach at the renderer boundaries, never inside leaf allocators. - Lifecycle: registration is expected to happen once at startup. Each
add{Slot}allocates a newCustomisationrecord (small, ~10 fields). Dynamic re-registration viaaddTracked/removeis supported but not the optimised path; budget accordingly.
Cache invalidation policy
The interpreter always emits Replace v when any handler returns Replace. The reference-equality short-circuit lives at the call site that owns the cache, not in the interpreter:
match HookSlot.run slot ctx rctx value with
| Pass -> useCached value
| Replace v when System.Object.ReferenceEquals(v, value) -> useCached value
| Replace v -> writeCacheEntry v; useFresh v
This relies on TypeRefRender / *Render records being reference types (F# default). If any of them are ever marked [<Struct>], the call site must switch to structural equality. Documented in "Risks".
Anchored cache keys are computed from the post-hook output, not the pre-hook input. Two handlers producing the same anchored form for different inputs will hit the cache; a handler producing different anchored forms for the same input will miss. This matches today's behaviour for AnchoredRender.
Determinism contract
A handler's output must be a pure function of (ctx, rctx, input). Two handlers producing different output for the same (ResolvedType, RenderContext) would corrupt PreludeRenders / AnchorRenders.
Optional debug-mode enforcement: when a cached entry is hit, re-run the chain on the original input and assert reference (or structural) equality against the cached output. Cheap insurance, off in release.
Migration map
Today |
Replacement |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
The matching |
|
|
(none — encoder policy tests) |
|
(none — attribute injection) |
|
The kind-keyed pipeInterface/pipeClass/pipeEnum/pipeTypeAlias/pipeVariable/pipeFunction helpers in TypeRefRender.Paths.fs collapse into a single PathResolution chain that branches on rctx.Owner.Kind and rctx.Position (PathPos MemberPath/PathPos VariablePath/PathPos FunctionPath).
Alternatives considered
- Single
TypeDefBuildslot over aTypeRenderTargetDU. Rejected — every handler would have to match four cases. The per-shape slot table costs four field names and gains type-safe handlers and per-shapeHasAny. Trade-off: a handler that wants to apply uniformly to all shapes must register on all four slots. Tolerable; that handler is rare and registration is a one-line per slot. - Splitting
TypeDefEmitper shape. Rejected — by emit time the value is already aWidgetBuilder<TypeDefn>; the shape distinction has been erased. - *
Pass | Replace | Skipin one DU for every slot.* Rejected —Skipis meaningless forTypeRefEmit/TypeDefEmit(every node must produce a widget). Two slot variants encode this in the type system rather than as a runtime check. - One
RenderPositionDU without partitioning. Rejected — TypeRef positions and Path positions never co-occur. Splitting viaRefPos/PathPoskeepsRenderContexthonest about which slot it belongs to. - Pre/post hook pairs. Rejected —
Replace (wrap input)already expresses "wrap after default render", and pre/post would double the slot count. - Conflating scope and payload mutation under one
TypeDefBuildslot. Rejected — the existingResolvedTypePreludedoes exactly this and the seams are visible.RenderScopeBuild(scope) andTypeDefBuild*(payload) are the natural split.
File-level changes
-
*
src/Xantham.Generator/Types/Generator.fs*- Remove
Interceptors,InterceptorPath,InterceptorPathDispatchand the kind-keyed path fields. - Add
HookResult<'T>,SkippableHookResult<'T>,Hook<'T>,SkippableHook<'T>,HookSlot<'T>,SkippableHookSlot<'T>,RenderContext,RenderPosition,TypeRefPosition,PathPosition,RenderStage,HandlerToken. - Replace
Customisationwith the per-shape slot record.Customisation.Default= all(_HookSlot).empty. Add chainable
Customisation.add{Slot}helpers (returnCustomisation) and trackedCustomisation.add{Slot}Trackedhelpers (returnCustomisation * HandlerToken) for every slot:PathResolution,TypeRefBuild,TypeRefEmit,RenderScopeBuild,TypeDefBuildClass,TypeDefBuildAlias,TypeDefBuildEnum,TypeDefBuildStringUnion,TypeDefEmit,AnchoredRef,AnchoredScope. PlusCustomisation.remove : HandlerToken -> Customisation -> Customisationthat finds the slot by token and rebuilds it.
- Remove
-
*
src/Xantham.Generator/Generator/HookSlot.fs* (new)Hook.ofMap,HookSlot.{empty, add, remove, clear, run},SkippableHookSlot.{empty, add, remove, clear, run}.
-
*
src/Xantham.Generator/Generator/TypeRefRender.Paths.fs*- Delete
Path.Interceptors.shouldIgnoreRender/shouldIgnoreExportand thepipe*family. - Replace each former call site with
SkippableHookSlot.run ctx.Customisation.PathResolution ....Skiptriggers the existing elision path thatIgnorePathRenderused. Thread
RenderContext(computeOwner/Positionfrom caller; member-path callers setOwnerto the containingResolvedType; top-level type paths setOwnerto the type itself).
- Delete
-
*
src/Xantham.Generator/Generator/TypeRefRender.Render.fs*- Wrap
Implementation.renderAtom/renderMolecule/renderwithTypeRefBuildinvocation at entry; wrap the finalWidgetBuilder<Type>withTypeRefEmit. - Mirror in the
Anchoredsub-module (usesAnchoredRef). Thread
RenderContextthrough recursion:RefPos TypeArgfor prefix args,RefPos UnionMember/RefPos TupleElementfor unions/tuples,RefPos FunctionParameter/RefPos FunctionReturninside Function arms,RefPos InheritanceReffromrenderInheritance,Render = scope.Render.
- Wrap
-
*
src/Xantham.Generator/Generator/Render.TypeShapes.fs/Render.TypeAlias.fs/Render.Enum.fs*- After producing each
*Render, run the matchingTypeDefBuild*slot.Skip⇒ skip the export. Replace direct calls to former
Paths.pipe*with the unifiedPathResolutionalready wired in step 3.
- After producing each
-
*
src/Xantham.Generator/Generator/TypeRender.Render.fs*In
TypeLikeRender.renderClass/renderRecord/renderTag/renderAnonymousRecordandTypeAliasRender.renderTypeAlias, after producing theWidgetBuilder<TypeDefn>, runTypeDefEmit.
-
*
src/Xantham.Generator/Generator/RenderScope.Prelude.fs*Replace the
Customisation.Interceptors.ResolvedTypePreludeinvocation inaddOrReplaceScopewithRenderScopeBuild(scope-level), then dispatch to the matchingTypeDefBuild*slot for the payload. Cache write happens after both hooks resolve (today's ordering preserved). The cache invalidation policy from "Performance" applies at this site.
-
*
src/Xantham.Generator/Types/Generator.fs—Anchored.addOrReplace*Replace
Customisation.Interceptors.AnchoredRenderwithAnchoredRef/AnchoredScopeper target shape.Skipremoves the entry from anchored emission.
-
*
src/Xantham.Generator/Generator/Render.fs* (required migration, not example)Migrate the live customisation from
Interceptors.*toCustomisation.add*builders. Add a worked example registering an encoder-invariant policy as aTypeRefBuildhandler keyed onrctx.Position.
-
*
src/Xantham.Fable/Program.fsand tests* (audit)- Search for any
Interceptors.reference and migrate. Confirm zero remaining references before merging.
- Search for any
Tests
Create tests/baselines/ (does not currently exist) and, before any code changes, capture a baseline of Xantham.Fable/output.json from a clean checkout of the commit agreed-upon as golden (e.g. master HEAD). Store at tests/baselines/output.json. The working-tree copy is dirty and not a valid baseline.
*`tests/Xantham.Generator.Tests/Tests/TypeRefRender.EncoderInvariant.fs
** — flesh out the three scaffolds againstTypeRefBuildhandlers (Invariant / Achievable / NotAchievable). Assert AST output:Promise<>vsPromise,option<…>wrapping for nullable, erasedU2` for union policies.Composition — register two
TypeRefBuildhandlers; assert latest-registered runs first and the earlier handler sees the later one's output viaReplace.Three-handler chain — register
Replace v₁,Pass,Replace v₃; assert final =Replace v₃and h₃'s input =v₁(regression test for the truth table).*
Skipsemantics* — register aPathResolutionhandler returningSkipfor a known export; assert it disappears from the output. Replicate the currentIgnorePathRendertest cases against the new mechanism.*
Skipshort-circuit* — register two handlers where the first returnsSkip; assert the second never runs.Position context — register a
TypeRefBuildhandler that assertsPosition = RefPos InheritanceRefonly when invoked fromrenderInheritance; assert it never fires elsewhere.*
Ownerfor member paths* — register aPathResolutionhandler withPosition = PathPos MemberPath; assertOwneris the containingResolvedType, not the member's type.*
Ownerfor top-level type paths* — register aPathResolutionhandler withPosition = PathPos TopLevelType; assertOwneris the resolved type itself.*
Rendermode threading* — register aTypeRefBuildhandler; assertrctx.Rendermatches the activeRenderScope.Renderat the call site.*
HookSlot.remove/clear* — register, run, remove via token, run again; assert the removed handler is no longer invoked.Cache invalidation phys-eq — register a
TypeDefBuild*handler that returnsReplace input; assert the cache is not invalidated (reference-equality short-circuit).Zero-cost no-op — with
Customisation.Default, instrumentHookSlot.runandSkippableHookSlot.runwith a counter and assert zero invocations of the loop body across a full generation ofXantham.Fable/output.json.Snapshot regression — byte-identical output against
tests/baselines/output.jsonwhenCustomisation.Defaultis in effect.
Risks
- Nullable invariant:
TypeRefRender.Nullableis computed at the molecule level forPrefix. ATypeRefBuildhandler that swapsPrefix → Atommust restore the nullability bit. Document; consider a debug-build assertion. - Cache coherence:
RenderScopeBuild/TypeDefBuild*/Anchored*outputs reachPreludeRenders/AnchorRenders. Two handlers producing different output for the same input would corrupt the cache. Document the determinism contract; consider the optional debug-mode re-run check. - Recursive context propagation: every recursive call inside
Implementation.renderMoleculeneeds an explicitRenderContextargument. Mechanical but touches every recursion site — easy to miss one and end up with a stalePositionorRendermode. - Reference-equality assumption: the phys-eq fast-path at cache-write sites assumes
TypeRefRender/*Renderrecords are reference types. If any of them is later marked[<Struct>], the call site needs structural equality. - Inline tail-call:
HookSlot.runisinlinebut delegates to a module-levellet rec loop. Verify the JIT actually emits a tail call; if not, the loop may need to be rewritten as awhileover an explicit cursor.
Cost summary
- 1 file rewritten for the surface (
Types/Generator.fs). - 1 new file (
Generator/HookSlot.fs). - 7 wiring files (
TypeRefRender.Paths.fs,TypeRefRender.Render.fs,Render.TypeShapes.fs,Render.TypeAlias.fs,Render.Enum.fs,TypeRender.Render.fs,RenderScope.Prelude.fs). - 1 new test directory (
tests/baselines/) with the captured baseline. - All call sites that previously used
Interceptors.*migrate mechanically toHookSlot.run/SkippableHookSlot.run. - Runtime cost on the no-handler path: one
boolbranch per stage entry, zero allocations.
type StructAttribute = inherit Attribute new: unit -> StructAttribute
--------------------
new: unit -> StructAttribute
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
union case HandlerToken.HandlerToken: SlotId * obj -> HandlerToken
--------------------
type HandlerToken = | HandlerToken of SlotId * obj
module Hook from generatorextensibilityrefactor
--------------------
type Hook<'T> = obj -> obj -> 'T -> HookResult<'T>
module List from Microsoft.FSharp.Collections
--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
type Object = new: unit -> unit member Equals: obj: obj -> bool + 1 overload member GetHashCode: unit -> int member GetType: unit -> Type member ToString: unit -> string static member ReferenceEquals: objA: obj * objB: obj -> bool
<summary>Supports all classes in the .NET class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all .NET classes; it is the root of the type hierarchy.</summary>
--------------------
System.Object() : System.Object
module SkippableHook from generatorextensibilityrefactor
--------------------
type SkippableHook<'T> = obj -> obj -> 'T -> SkippableHookResult<'T>
module HookSlot from generatorextensibilityrefactor
--------------------
type HookSlot<'T> = { Handlers: Hook<'T> list HasAny: bool }
module SkippableHookSlot from generatorextensibilityrefactor
--------------------
type SkippableHookSlot<'T> = { Handlers: SkippableHook<'T> list HasAny: bool }