Logo Xantham

Generator Extensibility — Wholesale Refactor Plan

Goal

Replace the ad-hoc Interceptors fields on GeneratorContext.Customisation with a single, uniform hook model that:

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:

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

PathResolution

TypePath

TypePath

yes

Paths.*, IgnorePathRender

TypeRefBuild

TypeReference

TypeRefRender

no

(new — covers encoder policies)

TypeRefEmit

TypeRefRender

WidgetBuilder<Type>

no

(new)

RenderScopeBuild

RenderScope

RenderScope

yes

ResolvedTypePrelude (scope-level)

TypeDefBuild

Concrete.*Render (per shape)

Concrete.*Render

yes

ResolvedTypePrelude (payload-level)

TypeDefEmit

WidgetBuilder<TypeDefn>

WidgetBuilder<TypeDefn>

no

(new)

Anchored

Anchored.* (per shape)

Anchored.*

yes

AnchoredRender

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:

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

current after

Notes

Pass

Pass

Pass

original v

Pass

Replace v₂

Replace v₂

v₂

Replace v₁

Pass

Replace v₁

v₁

Pass forwards current, no reset

Replace v₁

Replace v₂

Replace v₂

v₂

h₂ saw v₁

Replace v₁

Pass

Replace v₃

Replace v₃

v₃

h₃ saw v₁

Replace v₁

Skip

Skip

(elide)

v₁ discarded

Skip

(anything)

Skip

(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

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

Interceptors.IgnorePathRender

PathResolution handler returning Skip

Interceptors.Paths.TypePaths

PathResolution handler returning Replace path'; reads rctx.Owner for kind

Interceptors.Paths.MemberPaths

PathResolution handler with Position = PathPos MemberPath

Interceptors.Paths.{Variable,Function}

PathResolution handler with Position = PathPos VariablePath / FunctionPath

Interceptors.ResolvedTypePrelude (scope-level mutation)

RenderScopeBuild handler

Interceptors.ResolvedTypePrelude (payload mutation)

The matching TypeDefBuild* slot for the shape

Interceptors.AnchoredRender

AnchoredRef or AnchoredScope per target shape

(none — encoder policy tests)

TypeRefBuild handler keyed on Position

(none — attribute injection)

TypeDefEmit handler

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

File-level changes

  1. *src/Xantham.Generator/Types/Generator.fs*
    • Remove Interceptors, InterceptorPath, InterceptorPathDispatch and 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 Customisation with the per-shape slot record. Customisation.Default = all (_HookSlot).empty.
    • Add chainable Customisation.add{Slot} helpers (return Customisation) and tracked Customisation.add{Slot}Tracked helpers (return Customisation * HandlerToken) for every slot: PathResolution, TypeRefBuild, TypeRefEmit, RenderScopeBuild, TypeDefBuildClass, TypeDefBuildAlias, TypeDefBuildEnum, TypeDefBuildStringUnion, TypeDefEmit, AnchoredRef, AnchoredScope. Plus Customisation.remove : HandlerToken -> Customisation -> Customisation that finds the slot by token and rebuilds it.

  2. *src/Xantham.Generator/Generator/HookSlot.fs* (new)
    • Hook.ofMap, HookSlot.{empty, add, remove, clear, run}, SkippableHookSlot.{empty, add, remove, clear, run}.

  3. *src/Xantham.Generator/Generator/TypeRefRender.Paths.fs*
    • Delete Path.Interceptors.shouldIgnoreRender/shouldIgnoreExport and the pipe* family.
    • Replace each former call site with SkippableHookSlot.run ctx.Customisation.PathResolution .... Skip triggers the existing elision path that IgnorePathRender used.
    • Thread RenderContext (compute Owner/Position from caller; member-path callers set Owner to the containing ResolvedType; top-level type paths set Owner to the type itself).

  4. *src/Xantham.Generator/Generator/TypeRefRender.Render.fs*
    • Wrap Implementation.renderAtom / renderMolecule / render with TypeRefBuild invocation at entry; wrap the final WidgetBuilder<Type> with TypeRefEmit.
    • Mirror in the Anchored sub-module (uses AnchoredRef).
    • Thread RenderContext through recursion: RefPos TypeArg for prefix args, RefPos UnionMember/RefPos TupleElement for unions/tuples, RefPos FunctionParameter/RefPos FunctionReturn inside Function arms, RefPos InheritanceRef from renderInheritance, Render = scope.Render.

  5. *src/Xantham.Generator/Generator/Render.TypeShapes.fs / Render.TypeAlias.fs / Render.Enum.fs*
    • After producing each *Render, run the matching TypeDefBuild* slot. Skip ⇒ skip the export.
    • Replace direct calls to former Paths.pipe* with the unified PathResolution already wired in step 3.

  6. *src/Xantham.Generator/Generator/TypeRender.Render.fs*
    • In TypeLikeRender.renderClass / renderRecord / renderTag / renderAnonymousRecord and TypeAliasRender.renderTypeAlias, after producing the WidgetBuilder<TypeDefn>, run TypeDefEmit.

  7. *src/Xantham.Generator/Generator/RenderScope.Prelude.fs*
    • Replace the Customisation.Interceptors.ResolvedTypePrelude invocation in addOrReplaceScope with RenderScopeBuild (scope-level), then dispatch to the matching TypeDefBuild* 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.

  8. *src/Xantham.Generator/Types/Generator.fsAnchored.addOrReplace*
    • Replace Customisation.Interceptors.AnchoredRender with AnchoredRef / AnchoredScope per target shape. Skip removes the entry from anchored emission.

  9. *src/Xantham.Generator/Generator/Render.fs* (required migration, not example)
    • Migrate the live customisation from Interceptors.* to Customisation.add* builders. Add a worked example registering an encoder-invariant policy as a TypeRefBuild handler keyed on rctx.Position.

  10. *src/Xantham.Fable/Program.fs and tests* (audit)
    • Search for any Interceptors. reference and migrate. Confirm zero remaining references before merging.

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.

Risks

Cost summary

type HookResult<'T> = | Pass | Replace of 'T
'T
type SkippableHookResult<'T> = | Pass | Replace of 'T | Skip
union case HookResult.Pass: HookResult<'T>
union case HookResult.Replace: 'T -> HookResult<'T>
type Hook<'T> = obj -> obj -> 'T -> HookResult<'T>
type SkippableHook<'T> = obj -> obj -> 'T -> SkippableHookResult<'T>
Multiple items
type StructAttribute = inherit Attribute new: unit -> StructAttribute

--------------------
new: unit -> StructAttribute
type RenderContext = { Position: RenderPosition Owner: obj Render: obj Stage: RenderStage }
type RenderPosition = | RefPos of TypeRefPosition | PathPos of PathPosition | NotApplicable
type 'T voption = ValueOption<'T>
type RenderStage = | PathResolution | TypeRefBuild | TypeRefEmit | RenderScopeBuild | TypeDefBuild | TypeDefEmit | Anchored
type TypeRefPosition = | Standalone | InheritanceRef | TypeArg | TupleElement | UnionMember | FunctionParameter | FunctionReturn | AliasTarget | MemberType
type PathPosition = | TopLevelType | MemberPath | VariablePath | FunctionPath
type HookSlot<'T> = { Handlers: Hook<'T> list HasAny: bool }
type 'T list = List<'T>
type bool = System.Boolean
type SkippableHookSlot<'T> = { Handlers: SkippableHook<'T> list HasAny: bool }
type Customisation = { PathResolution: obj TypeRefBuild: obj TypeRefEmit: obj RenderScopeBuild: obj TypeDefBuildClass: obj TypeDefBuildAlias: obj TypeDefBuildEnum: obj TypeDefBuildStringUnion: obj TypeDefEmit: obj AnchoredRef: obj ... }
union case RenderStage.PathResolution: RenderStage
union case RenderStage.TypeRefBuild: RenderStage
union case RenderStage.TypeRefEmit: RenderStage
union case RenderStage.RenderScopeBuild: RenderStage
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
union case RenderStage.TypeDefEmit: RenderStage
union case RenderStage.Anchored: RenderStage
type SlotId = | PathResolutionSlot | TypeRefBuildSlot | TypeRefEmitSlot | RenderScopeBuildSlot | TypeDefBuildClassSlot | TypeDefBuildAliasSlot | TypeDefBuildEnumSlot | TypeDefBuildStringUnionSlot | TypeDefEmitSlot | AnchoredRefSlot ...
Multiple items
union case HandlerToken.HandlerToken: SlotId * obj -> HandlerToken

--------------------
type HandlerToken = | HandlerToken of SlotId * obj
type HandlerToken = | HandlerToken of SlotId * obj
type obj = System.Object
val ofMap: f: ('T -> 'T) -> obj -> obj -> v: 'T -> HookResult<'T>
val f: ('T -> 'T)
val v: 'T
union case SkippableHookResult.Replace: 'T -> SkippableHookResult<'T>
val ofMap: f: ('T -> 'T) -> obj -> obj -> v: 'T -> SkippableHookResult<'T>
val ofPredicate: shouldSkip: ('T -> bool) -> obj -> obj -> v: 'T -> SkippableHookResult<'T>
val shouldSkip: ('T -> bool)
union case SkippableHookResult.Skip: SkippableHookResult<'T>
union case SkippableHookResult.Pass: SkippableHookResult<'T>
val empty<'T> : HookSlot<'T>
val add: h: Hook<'T> -> slot: HookSlot<'T> -> HookSlot<'T>
val h: Hook<'T>
Multiple items
module Hook from generatorextensibilityrefactor

--------------------
type Hook<'T> = obj -> obj -> 'T -> HookResult<'T>
val slot: HookSlot<'T>
HookSlot.Handlers: Hook<'T> list
val addTracked: h: Hook<'T> -> slot: HookSlot<'T> -> HookSlot<'T> * obj
val box: value: 'T -> objnull
val remove: token: obj -> slot: HookSlot<'T> -> HookSlot<'T>
val token: obj
val h': Hook<'T> list
Multiple items
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 ...
val filter: predicate: ('T -> bool) -> list: 'T list -> 'T list
namespace System
Multiple items
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
System.Object.ReferenceEquals(objA: obj, objB: obj) : bool
val isEmpty: list: 'T list -> bool
val clear: HookSlot<'T> -> HookSlot<'T>
val private loop: result: SkippableHookResult<'a> -> current: 'a -> handlers: ('b -> 'c -> 'a -> SkippableHookResult<'a>) list -> ctx: 'b -> rctx: 'c -> SkippableHookResult<'a>
val result: SkippableHookResult<'a>
val current: 'a
val handlers: ('b -> 'c -> 'a -> SkippableHookResult<'a>) list
val ctx: 'b
val rctx: 'c
val h: ('b -> 'c -> 'a -> SkippableHookResult<'a>)
val rest: ('b -> 'c -> 'a -> SkippableHookResult<'a>) list
val v: 'a
val run: slot: SkippableHookSlot<'a> -> ctx: obj -> rctx: obj -> value: 'a -> SkippableHookResult<'a>
val slot: SkippableHookSlot<'a>
val ctx: obj
val rctx: obj
val value: 'a
SkippableHookSlot.HasAny: bool
SkippableHookSlot.Handlers: SkippableHook<'a> list
val empty<'T> : SkippableHookSlot<'T>
val add: h: SkippableHook<'T> -> slot: SkippableHookSlot<'T> -> SkippableHookSlot<'T>
val h: SkippableHook<'T>
Multiple items
module SkippableHook from generatorextensibilityrefactor

--------------------
type SkippableHook<'T> = obj -> obj -> 'T -> SkippableHookResult<'T>
val slot: SkippableHookSlot<'T>
SkippableHookSlot.Handlers: SkippableHook<'T> list
val addTracked: h: SkippableHook<'T> -> slot: SkippableHookSlot<'T> -> SkippableHookSlot<'T> * obj
val remove: token: obj -> slot: SkippableHookSlot<'T> -> SkippableHookSlot<'T>
val h': SkippableHook<'T> list
val clear: SkippableHookSlot<'T> -> SkippableHookSlot<'T>
union case RenderPosition.RefPos: TypeRefPosition -> RenderPosition
union case TypeRefPosition.TypeArg: TypeRefPosition
union case ValueOption.ValueSome: 'T -> ValueOption<'T>
Multiple items
module HookSlot from generatorextensibilityrefactor

--------------------
type HookSlot<'T> = { Handlers: Hook<'T> list HasAny: bool }
Multiple items
module SkippableHookSlot from generatorextensibilityrefactor

--------------------
type SkippableHookSlot<'T> = { Handlers: SkippableHook<'T> list HasAny: bool }

Type something to start searching.