Logo Xantham

ArenaInterner — the lazy resolved object graph

ArenaInterner lifts the flat, key-addressed DecodedResult into an in-memory object graph in which every TypeKey reference is replaced with a Lazy<ResolvedType>. Following a reference becomes "force a lazy"; no map lookup is needed at the call site.

Why a second representation?

The wire format is two flat maps keyed by TypeKey:

TypeMap          : Map<TypeKey, TsType>
ExportTypeMap    : Map<TypeKey, TsExportDeclaration>

References between types are also TypeKey values, which means every dereference inside a generator has to:

  1. Carry both maps (or close over them).
  2. Look up the key.
  3. Pattern match on the resulting TsType / TsExportDeclaration.

That's fine for one or two hops, but rendering walks the type graph recursively. The arena representation removes the ceremony:

Combined with [<ReferenceEquality>] on the resolved record types, this gives generators identity-based sharing detection essentially for free — you can recognise that two members reference the same TypeParameter without carrying keys around.

How cycles are handled

TypeScript declarations are routinely self-referential:

interface Node { parent?: Node; children: Node[] }
type Json = string | number | boolean | null | Json[] | { [k: string]: Json }

Under the arena representation, every outgoing reference is a Lazy<_> and construction of a node never forces those lazies. By the time anything is forced, the entire graph (including the cyclic partner) has already been shelled and inserted into the cache, so re-entrance is impossible.

You don't need to write any cycle detection in your generator. Walk the graph with normal pattern matching; lazy boundaries break the recursion for you.

Active pattern

The module exposes (|Resolve|) for ergonomic forcing:

match someLazyResolvedType with
| Resolve t ->
    // t is a fully forced ResolvedType
    ...

This is purely sugar over .Value, but it composes with nested patterns — e.g. Resolve (ResolvedType.Union { Members = members }).

When to use it

Prefer ArenaInterner when:

Stay with DecodedResult maps when:

Construction

XanthamTree lazily creates the interner the first time you ask for it:

let interner = tree.GetArenaInterner()

Subsequent calls return the cached value. To go directly:

let interner = ArenaInterner.ArenaInterner.create decodedResult

In both cases, every key in ExportTypeMap is shelled eagerly into the cache. Nested types are still deferred; only the outer envelope of each exported declaration is materialised on construction.

Diagnostics

QualifiedNamePartDiagnostic is a [<Flags>] enum surfaced when a name part contains characters that would break a generated F# qualified name:

Generators can use this to decide whether to backtick-wrap, replace, or skip a name segment.

Multiple items
module Map from Microsoft.FSharp.Collections

--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> interface IEnumerable interface IStructuralEquatable interface IComparable interface IEnumerable<KeyValuePair<'Key,'Value>> interface ICollection<KeyValuePair<'Key,'Value>> interface IDictionary<'Key,'Value> new: elements: ('Key * 'Value) seq -> Map<'Key,'Value> member Add: key: 'Key * value: 'Value -> Map<'Key,'Value> ...

--------------------
new: elements: ('Key * 'Value) seq -> Map<'Key,'Value>

Type something to start searching.