Logo Xantham

GeneratorContext

GeneratorContext is the mutable scratch space that every render pass threads through. It carries four caches, an in-flight set, and a customisation record. The dictionary type — Dictionary<,> or ConcurrentDictionary<,> — is selected at compile time by the CONCURRENT_DICT constant; this is enabled by default in Xantham.Generator.fsproj so test runners that exercise the generator from multiple threads do not race.

Shape

type GeneratorContext = {
    TypeAliasRemap   : DictionaryImpl<ResolvedType, TypeRefRender>
    PreludeGetTypeRef: PreludeGetTypeRefFunc
    PreludeRenders   : DictionaryImpl<ResolvedType, RenderScope>
    AnchorRenders    : DictionaryImpl<Choice<ResolvedType, ResolvedExport>,
                                      Choice<Anchored.TypeRefRender, Anchored.RenderScope>>
    InFlight         : HashSet<ResolvedType>
    Customisation    : Customisation
}

Construction

let ctx =
    GeneratorContext.EmptyWithCustomisation (fun c ->
        { c with
            Customisation.Interceptors.IgnorePathRender.Source = 
            Customisation.Interceptors.Paths.TypePaths        = 
            Customisation.Interceptors.ResolvedTypePrelude    = 
        })

The EmptyWithCustomisation helper wires up a default PreludeGetTypeRef and lets the caller mutate the customisation record through a function. GeneratorContext.Create(preludeFn, ?customisation) is the lower-level form that takes the prelude function explicitly — used by tests that want to inject their own resolution strategy.

Operations

The GeneratorContext module is partitioned by which dictionary you are acting on:

GeneratorContext.Prelude.tryGet    ctx key
GeneratorContext.Prelude.addOrReplace ctx key scope
GeneratorContext.Prelude.canFlight ctx resolvedType         // false on re-entry
GeneratorContext.Prelude.addTypeAliasRemap ctx key value

GeneratorContext.Anchored.tryGet           ctx (Choice1Of2 t | Choice2Of2 e)
GeneratorContext.Anchored.tryGetResolvedType  ctx t
GeneratorContext.Anchored.tryGetResolvedExport ctx e
GeneratorContext.Anchored.addOrReplace     ctx key value
GeneratorContext.Anchored.addResolvedType  ctx t value
GeneratorContext.Anchored.addResolvedExport ctx e value

The Anchored.addOrReplace path runs the Customisation.Interceptors.AnchoredRender interceptor on every write — this is the last opportunity to rewrite a render before it lands in the emit-time store.

Customisation surface

type InterceptorIgnorePathRender = {
    Source        : ArenaInterner.QualifiedNamePart -> bool
    QualifiedName : QualifiedName -> bool
}

type InterceptorPaths = {
    TypePaths    : GeneratorContext -> Choice<Interface, EnumType, Class, TypeAlias> -> TypePath -> TypePath
    MemberPaths  : GeneratorContext -> Choice<Variable, Function> -> MemberPath -> MemberPath
}

type Interceptors = {
    IgnorePathRender    : InterceptorIgnorePathRender
    Paths               : InterceptorPaths
    ResolvedTypePrelude : GeneratorContext -> ResolvedType -> RenderScope -> RenderScope
    AnchoredRender      : GeneratorContext -> Choice<ResolvedType, ResolvedExport>
                           -> Choice<Anchored.TypeRefRender, Anchored.RenderScope>
                           -> Choice<Anchored.TypeRefRender, Anchored.RenderScope>
}

type Customisation = { Interceptors: Interceptors }

Customisation.Default provides identity functions for every hook; Customisation.Create fn is the recommended entry point because it preserves any hooks the runtime adds in the future.

Worked example

let ctx =
    GeneratorContext.EmptyWithCustomisation (fun c ->
        { c with
            Customisation.Interceptors.IgnorePathRender.Source = function
                | QualifiedNamePart.Normal text
                | QualifiedNamePart.Abnormal(text, _) ->
                    text.Contains("typescript", StringComparison.OrdinalIgnoreCase)

            Customisation.Interceptors.ResolvedTypePrelude = fun _ -> function
                | ResolvedType.Interface { IsLibEs = true }
                | ResolvedType.Class     { IsLibEs = true }
                | ResolvedType.Enum      { IsLibEs = true } ->
                    fun scope -> { scope with Render = Render.RefOnly scope.TypeRef }
                | _ -> id

            Customisation.Interceptors.Paths.TypePaths = fun _ typ p ->
                match typ with
                | Choice1Of4 { IsLibEs = true }
                | Choice2Of4 { IsLibEs = true }
                | Choice3Of4 { IsLibEs = true }
                | Choice4Of4 { IsLibEs = true } ->
                    TypePath.pruneParent
                        (_.Name >> Name.Case.valueOrModified >> (=) "Typescript") p
                | _ -> p })

This is the exact configuration in Generator/Render.fs. It says:

  1. Drop the typescript source qualifier when computing module paths.
  2. For every lib-ES type, do not emit a definition — emit only the reference.
  3. For every lib-ES path, strip the synthetic Typescript parent module so the result reads like MyTypes.Foo rather than Typescript.MyTypes.Foo.
type GeneratorContext = { TypeAliasRemap: obj PreludeGetTypeRef: obj PreludeRenders: obj AnchorRenders: obj InFlight: obj Customisation: obj }
Multiple items
type Choice<'T1,'T2> = | Choice1Of2 of 'T1 | Choice2Of2 of 'T2

--------------------
type Choice<'T1,'T2,'T3> = | Choice1Of3 of 'T1 | Choice2Of3 of 'T2 | Choice3Of3 of 'T3

--------------------
type Choice<'T1,'T2,'T3,'T4> = | Choice1Of4 of 'T1 | Choice2Of4 of 'T2 | Choice3Of4 of 'T3 | Choice4Of4 of 'T4

--------------------
type Choice<'T1,'T2,'T3,'T4,'T5> = | Choice1Of5 of 'T1 | Choice2Of5 of 'T2 | Choice3Of5 of 'T3 | Choice4Of5 of 'T4 | Choice5Of5 of 'T5

--------------------
type Choice<'T1,'T2,'T3,'T4,'T5,'T6> = | Choice1Of6 of 'T1 | Choice2Of6 of 'T2 | Choice3Of6 of 'T3 | Choice4Of6 of 'T4 | Choice5Of6 of 'T5 | Choice6Of6 of 'T6

--------------------
type Choice<'T1,'T2,'T3,'T4,'T5,'T6,'T7> = | Choice1Of7 of 'T1 | Choice2Of7 of 'T2 | Choice3Of7 of 'T3 | Choice4Of7 of 'T4 | Choice5Of7 of 'T5 | Choice6Of7 of 'T6 | Choice7Of7 of 'T7
val ctx: obj
union case Choice.Choice1Of2: 'T1 -> Choice<'T1,'T2>
union case Choice.Choice2Of2: 'T2 -> Choice<'T1,'T2>
type bool = System.Boolean
Multiple items
type InterfaceAttribute = inherit Attribute new: unit -> InterfaceAttribute

--------------------
new: unit -> InterfaceAttribute
Multiple items
type ClassAttribute = inherit Attribute new: unit -> ClassAttribute

--------------------
new: unit -> ClassAttribute
val id: x: 'T -> 'T
union case Choice.Choice1Of4: 'T1 -> Choice<'T1,'T2,'T3,'T4>
union case Choice.Choice2Of4: 'T2 -> Choice<'T1,'T2,'T3,'T4>
union case Choice.Choice3Of4: 'T3 -> Choice<'T1,'T2,'T3,'T4>
union case Choice.Choice4Of4: 'T4 -> Choice<'T1,'T2,'T3,'T4>

Type something to start searching.