SourceTanglingThe compile layer

Part V, Chapter 1

Part V: Tangling

Chapter 1: The compile layer

Package: packages/loom-lang/src/LoomCompiler.ts

Language: TypeScript

Raw source

The compile layer

Loom compiles a .loom file from its own syntax into code. The Loom syntax — the headings, anchors, and sinks — says how the file composes, and Loom serves it on the source itself. The product is the author's code, composed from the sections those anchors name. LoomCompiler is where the two meet: it runs the chain — parse → product — and projects the result for whoever asks.

The chain and its verbs

LoomCompiler is the higher-order service that runs the chain. It composes the passes rather than re-implementing them, and hands each consumer a finished projection through a verb.

Its spine is one step. buildCorpus runs parse |> product: it loads each module through LoomCorpusAstBuilder — which parses the file and composes its product in the same pass — keeps it in LoomMemo, and discovers the rest of the corpus beside it. The corpus is the through-line every projection reads.

The product is built once, with the parse. LoomCorpusAstBuilder composes each module's product as it loads, so LoomMemo caches the whole module — parse and product together — and a second projection of an unedited file reuses it. An edit evicts the file and its dependents, so the next projection rebuilds only what changed.

The verbs project the corpus for the consumers. compile gives the editor its Volar tree, the source as the loom root and the product sections beneath it. tangle resolves a file's {path} sinks to the content and target the CLI writes. diagnose gives a file's Loom health, the diagnostics the editor shows. definition follows an anchor to the section it names, and references finds every anchor that names the section under the cursor. rename gathers the title and anchors a rename rewrites together, and renameRange gives the span the rename box covers, while navigationRange gives the whole span a definition link underlines over any navigable token. semanticTokens gives each of a file's tokens its colour, so the editor highlights the source. reach gives the files an entry pulls in, so the editor associates their scripts. weave projects the whole corpus into a WovenCorpus, the pages and navigation the website renders. contents reads each chapter's frontmatter and rewrites the book's own table of contents from it, returning the rewritten source. invalidate evicts a file and the dependents that inlined it, naming what an edit must reproject.

Every verb builds the corpus the same way and differs only in what it reads. compile and tangle read the product — the composed sections and the sink files. diagnose, definition, references, rename, renameRange, navigationRange, semanticTokens, reach, weave, and contents read the parse and the corpus graph, since Loom health, navigation, colour, placement, the woven page, and the table of contents need no product. compile takes its Source from the caller, so the editor reads the open buffer, and it guards Volar's synchronous hook: a defect degrades to a bare loom document carrying the file's text, and the editor keeps working rather than crashing. The other verbs read from disk through DocumentSource.

TypeScript
export class LoomCompiler extends Context.Service<LoomCompiler>()('LoomCompiler', {
  make: Effect.gen(function* () {
    const documents = yield* DocumentSource
    const builder = yield* LoomCorpusAstBuilder
    const memo = yield* LoomMemo
    const config = yield* PackageConfig
    const weaver = yield* WeaveBuilder
    const tocBuilder = yield* TableOfContentsBuilder

    const loadOne = (source: Source, path: Path): Effect.Effect<LoomModule> =>
      config
        .resolve(path)
        .pipe(
          Effect.flatMap(({ delims, primaryLanguage, variables }) =>
            memo.get(path, builder.build(source, path, delims, primaryLanguage, variables)),
          ),
        )

    const listFrom = (
      list: (dir: Path, project: Option.Option<LoomProject>) => Effect.Effect<ReadonlyArray<Path>>,
      entry: Path,
    ): Effect.Effect<ReadonlyArray<Path>> =>
      config.resolve(entry).pipe(
        Effect.flatMap((settings) =>
          Option.match(
            Option.filter(projectIn(settings), (project) => holds(project, entry)),
            {
              onNone: () => list(dirname(entry), Option.none()),
              onSome: (project) =>
                list(
                  Option.getOrElse(Option.fromNullishOr(settings.corpusDir), () => dirname(entry)),
                  Option.some(project),
                ),
            },
          ),
        ),
        Effect.map((found) => Array.dedupe([entry, ...found])),
      )

    const corpusPaths = (source: Source, entry: Path): Effect.Effect<ReadonlyArray<Path>> =>
      Option.match(source.list, {
        onNone: () => Effect.succeed<ReadonlyArray<Path>>([entry]),
        onSome: (list) => listFrom(list, entry),
      })

    const buildCorpus = (source: Source, entry: Path): Effect.Effect<Modules> =>
      corpusPaths(source, entry).pipe(
        Effect.flatMap((paths) =>
          Effect.forEach(paths, (path) =>
            loadOne(source, path).pipe(Effect.map((m) => [path, m] as const)),
          ),
        ),
        Effect.map((pairs) => new Map(pairs)),
      )

    return {
      compile: (source: Source, path: Path): Effect.Effect<VirtualCode> =>
        buildCorpus(source, path).pipe(
          Effect.flatMap((modules) => entryOf(modules, path)),
          Effect.map(({ modules, entry }) => toVolar(projectTree(modules, entry))),
          Effect.catchCause((cause) =>
            Effect.logError('loom: projection failed; serving bare document', cause).pipe(
              Effect.andThen(
                source.read(path).pipe(
                  Effect.orElseSucceed(() => ''),
                  Effect.map((text) => toVolar(rootVirtualCode(text, []))),
                ),
              ),
            ),
          ),
        ),

      tangle: (path: Path): Effect.Effect<ReadonlyArray<TangledFile>, TangleError> =>
        buildCorpus(documents, path).pipe(
          Effect.flatMap((modules) => {
            const scoped = scopedTo(modules, reachable(modules, path))
            const failures = corpusErrors({ modules: scoped }, corpusDiagnostics(scoped))
            return failures.length > 0
              ? Effect.fail(new TangleError({ entry: path, failures }))
              : config
                  .resolve(path)
                  .pipe(
                    Effect.map(({ packageRoot }) =>
                      resolveSinks(scoped, path, Option.fromNullishOr(packageRoot)),
                    ),
                  )
          }),
        ),

      composition: (path: Path): Effect.Effect<ReadonlyArray<ComposedFile>> =>
        buildCorpus(documents, path).pipe(
          Effect.flatMap((modules) =>
            Effect.forEach(
              Array.fromIterable(modules.values()),
              (module) =>
                config.resolve(module.path).pipe(
                  Effect.map(({ packageRoot }) =>
                    Array.map(module.product.files, (file): ComposedFile => ({
                      path: outputPath(Option.fromNullishOr(packageRoot), module.path, file.path),
                      content: fromProduct(modules, file.code.origin).code,
                      loomPath: module.path,
                      rootId: file.code.origin.name.toLowerCase(),
                      heading: headingLocationOf(modules, module, file.code.origin.name),
                    })),
                  ),
                ),
              { concurrency: 'unbounded' },
            ).pipe(Effect.map(Array.flatten)),
          ),
        ),

      weave: (path: Path): Effect.Effect<WovenCorpus> =>
        buildCorpus(documents, path).pipe(Effect.flatMap((modules) => weaver.build({ modules }))),

      contents: (path: Path): Effect.Effect<string> =>
        buildCorpus(documents, path).pipe(
          Effect.flatMap((modules) =>
            tocBuilder
              .build({ modules })
              .pipe(Effect.map((toc) => rewriteBook({ modules }, path, toc))),
          ),
        ),

      diagnose: (path: Path): Effect.Effect<ReadonlyArray<Diagnostic>> =>
        buildCorpus(documents, path).pipe(
          Effect.map((modules) =>
            pipe(
              Option.fromNullishOr(modules.get(path)),
              Option.match({
                onNone: () => [],
                onSome: (module) => [
                  ...corpusDiagnostics(modules)(module),
                  ...Array.flatMap(
                    unresolvedTocEntriesIn({ modules }, module),
                    (title) =>
                      faulty(UnresolvedTocEntry({ title: title.value }), title.position)
                        .diagnostics,
                  ),
                ],
              }),
            ),
          ),
        ),

      definition: (path: Path, offset: number): Effect.Effect<FrameLocation | undefined> =>
        buildCorpus(documents, path).pipe(
          Effect.map((modules) =>
            Option.getOrUndefined(
              Option.flatMap(definitionAt({ modules }, path, offset), (loc) =>
                frameLocationOf(modules, loc),
              ),
            ),
          ),
        ),

      references: (path: Path, offset: number): Effect.Effect<ReadonlyArray<FrameLocation>> =>
        buildCorpus(documents, path).pipe(
          Effect.map((modules) =>
            Array.getSomes(
              Array.map(referencesAt({ modules }, path, offset), (loc) =>
                frameLocationOf(modules, loc),
              ),
            ),
          ),
        ),

      rename: (path: Path, offset: number): Effect.Effect<ReadonlyArray<FrameLocation>> =>
        buildCorpus(documents, path).pipe(
          Effect.map((modules) =>
            Array.getSomes(
              Array.map(renameAt({ modules }, path, offset), (loc) =>
                frameLocationOf(modules, loc),
              ),
            ),
          ),
        ),

      renameRange: (path: Path, offset: number): Effect.Effect<FrameLocation | undefined> =>
        buildCorpus(documents, path).pipe(
          Effect.map((modules) =>
            Option.getOrUndefined(
              Option.flatMap(renameRangeAt({ modules }, path, offset), (loc) =>
                frameLocationOf(modules, loc),
              ),
            ),
          ),
        ),

      navigationRange: (path: Path, offset: number): Effect.Effect<FrameLocation | undefined> =>
        buildCorpus(documents, path).pipe(
          Effect.map((modules) =>
            Option.getOrUndefined(
              Option.flatMap(navigationRangeAt({ modules }, path, offset), (loc) =>
                frameLocationOf(modules, loc),
              ),
            ),
          ),
        ),

      semanticTokens: (path: Path): Effect.Effect<ReadonlyArray<FrameToken>> =>
        buildCorpus(documents, path).pipe(
          Effect.map((modules) =>
            pipe(
              Option.fromNullishOr(modules.get(path)),
              Option.match({
                onNone: () => [],
                onSome: (module) =>
                  Array.getSomes(
                    Array.map(symbolsOf(module.doc), (symbol) =>
                      Option.flatMap(profileOf(symbol.kind).semantic, (token) =>
                        Option.map(
                          frameLocationOf(modules, {
                            path,
                            position: symbol.position,
                          }),
                          (location): FrameToken => ({
                            range: location.range,
                            type: token,
                          }),
                        ),
                      ),
                    ),
                  ),
              }),
            ),
          ),
        ),

      reach: (path: Path): Effect.Effect<ReadonlyArray<Path>> =>
        buildCorpus(documents, path).pipe(
          Effect.map((modules) => Array.filter(reachable(modules, path), (p) => p !== path)),
        ),

      invalidate: (path: Path): Effect.Effect<ReadonlyArray<Path>> =>
        Effect.gen(function* () {
          const modules = yield* memo.entries
          const dependents = transitiveDependents(modules, path)
          yield* memo.evict([path])
          return [path, ...dependents]
        }),
    }
  }),
}) {
  static readonly layer = Layer.effect(this, this.make).pipe(
    Layer.provide(
      Layer.mergeAll(
        LoomCorpusAstBuilder.layer,
        LoomMemo.layer,
        WeaveBuilder.layer,
        TableOfContentsBuilder.layer,
      ),
    ),
  )
}

The discovered corpus

A file's corpus is the looms around it that belong to its project, discovered from disk rather than named in imports. buildCorpus lists the file's corpus directory through the source and builds each loom the listing returns. So a book reaches the chapters it places and an anchor reaches the section it names, with nothing imported by hand. A source that lists nothing — a single document held in memory — yields a corpus of one.

The file's project decides which looms the listing returns. It applies the one rule every walk over a workspace follows, the rule the chapter on the looms in a project sets out. So the corpus the editor composes is the corpus tangle writes and orphans counts.

A loom the project leaves out belongs to no project, so its corpus follows no project's rule. A test fixture is the usual case, whether a test builds it or a person opens it in the editor. Such a loom composes with every loom under its own folder, as a loom outside any project does, so a fixture that imports from a sibling fixture still finds it. listFrom makes the choice. It resolves the file's settings and finds its project with projectIn. When the project holds the file, listFrom lists the corpus directory for that project. Otherwise it lists the file's own folder with no project at all.

The whole corpus is what navigation resolves against, so a name reaches a section wherever in the corpus it lives. The tangle and the diagnose narrow it: each scopes to the entry and the sections it composes across files, through reachable, so tangling one file never reaches a sibling it should not, and a broken sibling never fails a neighbour's build.

A Modules map keys each module by its path. It is the value buildCorpus returns and every projection reads. scopedTo keeps the modules a set of paths names, the way the tangle narrows the whole corpus to one entry's reach.

TypeScript
type Modules = ReadonlyMap<Path, LoomModule>

const scopedTo = (modules: Modules, paths: ReadonlyArray<Path>): Modules => {
  const keep = new Set(paths)
  return new Map(Array.filter(Array.fromIterable(modules), ([p]) => keep.has(p)))
}

Resolving the entry

Two verbs are about one file within its corpus, so they pick that module out once the corpus is produced. entryOf looks the entry up by path and dies if it is absent — a projection of a file the corpus never loaded is a bug, not a recoverable state. It returns the entry beside the corpus it came from, so a caller reads both from one result.

TypeScript
const entryOf = (
  modules: Modules,
  path: Path,
): Effect.Effect<{ readonly modules: Modules; readonly entry: LoomModule }> =>
  Effect.fromNullishOr(modules.get(path)).pipe(
    Effect.orDie,
    Effect.map((entry) => ({ modules, entry })),
  )

Judging a module against its corpus

A module's diagnostics come from two places. Its own two tiers, the parse and the product, travel with the module in the memo, and moduleDiagnostics reads them. An anchor into another file is judged against the corpus instead, by the product pass's crossFileFaults, because the module alone cannot tell whether the section it names exists.

corpusDiagnostics reads both for one module, against the corpus the module came from. The tangle gates on it and diagnose reports it, so the build and the editor agree on what is broken.

TypeScript
const corpusDiagnostics =
  (modules: Modules) =>
  (module: LoomModule): ReadonlyArray<Diagnostic> => [
    ...moduleDiagnostics(module),
    ...crossFileFaults(modules, module),
  ]

Tangling a file

tangle is the product's file projection, the sibling of compile. It produces the corpus, refuses a broken build, and resolves the entry's {Tangle} sections to the content and target a writer emits. It scopes through reachable — the entry and the sections it composes — yet emits only the entry's own files, so a loom it merely transcludes never re-emits, and every file is written once.

composition is that same resolution turned to the editor: every root in the corpus, the code it composes and the path it composes to, ungated and unscoped. It is the live product as a filesystem — what a product language service builds its analyzer's program over, so a cross-file import resolves against a section's live composition rather than its output on disk.

resolveSinks gathers the files this tangle emits: the entry module's own tangle sinks — one per [dir, file] section — and every sink a book in the corpus routes into place. For each it flattens the composed code across the corpus with fromProduct, then resolves where the file lands. A module's file emits only when the entry produced it or a book routes it, so importing a module to transclude its code never re-emits that module's own files. outputPath places a sink under a root, so a sink names its file relative to that root rather than climbing out of the corpus. The formatter reads the same rule, to name the file a root tangles to. fileRoot chooses that root for each sink: the chapter prefix a book places the sink under, resolved against the workspace, or the package root the configuration gives when no chapter places it. A file the workspace assigns to no package falls back to the module's own directory. resolveSinks returns a TangledFile per sink: the section that produced it, where it lands, and its content. LoomTangler performs the writes; tangle itself touches no disk.

A tangle must never emit an anchor it could not resolve, nor build on a file it could not read. So before it resolves a single sink, tangle runs corpusErrors over the modules the entry reaches, reading each through corpusDiagnostics, and fails with a TangleError when any of them carries an error. A malformed parse stops the build. So does an anchor that names no section in its own file, and an anchor whose path names a file that lacks the section. None of them leaves a file with a silently dropped reference. The failure is loud, and names every file and line.

TypeScript
export interface TangledFile {
  readonly section: string
  readonly path: Path
  readonly content: string
}

export const outputPath = (packageRoot: Option.Option<string>, entry: Path, sink: string): Path =>
  Option.match(packageRoot, {
    onNone: () => resolvePath(dirname(entry), sink),
    onSome: (root) => resolvePath(root, sink),
  })

const resolveSinks = (
  modules: Modules,
  entry: Path,
  packageRoot: Option.Option<string>,
): ReadonlyArray<TangledFile> =>
  pipe(
    Option.fromNullishOr(modules.get(entry)),
    Option.match({
      onNone: () => [],
      onSome: (module) =>
        Array.map(module.product.files, (file) => ({
          section: file.code.origin.name,
          path: outputPath(packageRoot, entry, file.path),
          content: fromProduct(modules, file.code.origin).code,
        })),
    }),
  )

export class TangleError extends Data.TaggedError('TangleError')<{
  readonly entry: Path
  readonly failures: ReadonlyArray<{
    readonly path: Path
    readonly diagnostics: ReadonlyArray<Diagnostic>
  }>
}> {
  get message(): string {
    const count = this.failures.reduce((n, f) => n + f.diagnostics.length, 0)
    const lines = this.failures.flatMap((f) =>
      f.diagnostics.map((d) => `  ${f.path}:${d.position.start.line}: ${d.message}`),
    )
    return `loom: refusing to tangle ${this.entry} — ${count} error(s) across the corpus:\n${lines.join('\n')}`
  }
}

Locating a source span

The navigation verbs answer in FrameLocations — a path and a line-and-character range — so the editor places a result without re-reading the file. definitionAt and referencesAt give a CorpusLocation in the source's own offsets, since the corpus walks in offsets. frameLocationOf converts one to the other, reading the target module's text to turn each offset into a line and a character. lineCharAt counts the newlines before an offset for the line, and the distance from the last newline for the character — the zero-based pair the editor speaks. headingLocationOf finds a root's section by its name and gives the FrameLocation of its heading title, so the composition can point a cross-file import at the section that writes the file.

TypeScript
const lineCharAt = (
  text: string,
  offset: number,
): { readonly line: number; readonly character: number } => {
  const before = text.slice(0, offset)
  return {
    line: before.split('\n').length - 1,
    character: offset - (before.lastIndexOf('\n') + 1),
  }
}

const frameLocationOf = (
  modules: Modules,
  location: CorpusLocation,
): Option.Option<FrameLocation> =>
  Option.map(Option.fromNullishOr(modules.get(location.path)?.text), (text) => ({
    path: location.path,
    range: {
      start: lineCharAt(text, location.position.start.offset),
      end: lineCharAt(text, location.position.end.offset),
    },
  }))

const headingLocationOf = (modules: Modules, module: LoomModule, name: string): FrameLocation =>
  pipe(
    Array.findFirst(
      module.doc.sections,
      (section) => normaliseTitle(section.heading.title?.source ?? '') === name,
    ),
    Option.flatMapNullishOr((section) => section.heading.title),
    Option.flatMap((title) =>
      frameLocationOf(modules, { path: module.path, position: title.position }),
    ),
    Option.getOrElse((): FrameLocation => ({
      path: module.path,
      range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
    })),
  )

Projecting a file's tree

projectTree assembles a file's virtual-code tree. It emits the file's prose as a Markdown document with fromProse, one product document per composition root with fromProduct, and gathers them under a loom root with rootVirtualCode — the source itself, the surface Loom serves the Loom syntax on. It also hands the root the section symbols symbolMappings reads from the parse — each heading and anchor span — so go-to-definition and rename reach them on the file the author opened. A root is a section that no same-file anchor pulls into another, and rootsAt keeps those names and drops the members. fromProduct inlines the members a root pulls in, so each emitted document is a whole resolved Product — and a member's span maps into the root that carries it, rather than standing as a document of its own. The projection follows cross-file ::[…] references through the corpus modules it is handed, so it treats a lone file and a whole corpus the same way.

TypeScript
const namesAt = (modules: Modules, path: Path): ReadonlyArray<string> =>
  pipe(
    Option.fromNullishOr(modules.get(path)),
    Option.match({
      onNone: () => [],
      onSome: (module) => Array.map(module.product.code, (c) => c.origin.name),
    }),
  )

const rootsAt = (modules: Modules, path: Path): ReadonlyArray<string> => {
  const roots = rootNamesAt(modules, path)
  return Array.filter(namesAt(modules, path), (name) => roots.has(name.toLowerCase()))
}

const projectTree = (modules: Modules, entry: LoomModule): LoomVirtualCode =>
  rootVirtualCode(
    entry.text,
    [
      fromProse(modules, entry.path),
      ...pipe(
        rootsAt(modules, entry.path),
        Array.map((name) => fromProduct(modules, { path: entry.path, name })),
      ),
    ],
    symbolMappings(entry.doc),
  )

Crossing to Volar

Every model up to here is plain data, which keeps the pipeline serialisable and easy to test. Volar is not plain data: it wants a function-based IScriptSnapshot for a document's text and its own CodeMapping for each source span. toVolar is the one adapter that crosses from a LoomVirtualCode tree to Volar's VirtualCode, and the only place in Loom that touches Volar's runtime types. It recurses down the embedded tree, converting every node.

Three small functions do the work. stringSnapshot presents a string as the minimal snapshot Volar reads. featuresOf chooses which editor features Volar forwards at a mapped span, and for every author-facing token it defers to LoomSymbol: it reads the span's symbol kind through profileOf and forwards exactly the features that kind allows — a heading navigates, an anchor navigates and verifies, prose only folds and outlines. The table is the one place those features live, so featuresOf repeats none of them. It answers only for the two kinds that carry the projection's structure rather than a token a cursor rests on. A source span — the whole-file identity mapping the root carries — takes verification and formatting alone, so Loom's own diagnostics surface on the file, and the Loom language formats the whole file through it, while no other feature fires there. A product span carries the full set, verification and completion and semantics among them, so the author edits the resolved composition as if it were the source. toCodeMapping rewrites one of our Mappings — a .loom source span paired with the generated span it projects to — as Volar's CodeMapping, choosing its features with featuresOf.

TypeScript
export const stringSnapshot = (text: string): ts.IScriptSnapshot => ({
  getText: (start, end) => text.slice(start, end),
  getLength: () => text.length,
  getChangeRange: () => undefined,
})

const productFeatures: CodeMapping['data'] = {
  verification: true,
  completion: true,
  semantic: true,
  navigation: true,
  structure: true,
}

const featuresOf = (kind: Mapping['kind']): CodeMapping['data'] =>
  Match.value(kind).pipe(
    Match.when('source', () => ({ verification: true, format: true })),
    Match.when('product', () => productFeatures),
    Match.when(Match.defined, (symbol) => profileOf(symbol).features),
    Match.orElse(() => productFeatures),
  )

const toCodeMapping = (m: Mapping): CodeMapping => ({
  sourceOffsets: [m.source.start.offset],
  generatedOffsets: [m.genStart],
  lengths: [m.source.end.offset - m.source.start.offset],
  generatedLengths: [m.genLength],
  data: featuresOf(m.kind),
})

export const toVolar = (vc: LoomVirtualCode): VirtualCode => ({
  id: vc.id,
  languageId: vc.languageId,
  snapshot: stringSnapshot(vc.code),
  mappings: Array.map(vc.mappings, toCodeMapping),
  embeddedCodes: Array.map(vc.embeddedCodes, toVolar),
})

The document source

DocumentSource is the compiler's disk seam. It reads a .loom file's text by path through Node's synchronous fs, and fails with ReadError when the file cannot be read. The read is synchronous because the editor projects inside Volar's synchronous hook, which cannot await. The tangler injects this source; the editor injects one of its own.

It also lists a directory's looms. list walks the directory through loomsIn and returns every loom the given project holds there, the siblings the compiler builds into one corpus. The tangler and the editor walk through loomsIn too, so every listing of a corpus follows the same rule.

TypeScript
export class DocumentSource extends Context.Service<DocumentSource>()('DocumentSource', {
  make: Effect.succeed({
    read: (path: Path): Effect.Effect<string, ReadError> =>
      Effect.try({
        try: () => readFileSync(path, 'utf8'),
        catch: (cause) => new ReadError({ path, cause }),
      }),
    list: Option.some(
      (dir: Path, project: Option.Option<LoomProject>): Effect.Effect<ReadonlyArray<Path>> =>
        Effect.sync(() => loomsIn(dir, project)),
    ),
  }),
}) {
  static readonly layer = Layer.effect(this, this.make)
}

What the compiler draws on

From Volar come the VirtualCode and CodeMapping types the adapter targets. From the pipeline come its collaborators: LoomCorpusAstBuilder with its Source seam and ReadError; the projection functions fromProduct, fromProse, rootNamesAt, rootVirtualCode, and symbolMappings; the corpus model — LoomModule, the corpusErrors and moduleDiagnostics readers, the reachable and transitiveDependents reaches over the anchor graph, and the definitionAt, referencesAt, renameAt, renameRangeAt, and navigationRangeAt navigation walks with the CorpusLocation they yield; the product pass's crossFileFaults, which judges an anchor into another file against the corpus; the virtual-code model, LoomVirtualCode and its Mapping; and the LoomMemo cache. The WeaveBuilder pass projects the whole corpus into the WovenCorpus the weave verb returns. The TableOfContentsBuilder pass reads the corpus's frontmatter into a table of contents, and rewriteBook splices that table back into the book — together what the contents verb returns. profileOf and symbolsOf from LoomSymbol give each mapped span its editor features and each of a file's tokens its colour, and normaliseTitle folds a heading title to the name an anchor resolves. From the language-service contract come FrameLocation, the editor-shaped range the navigation verbs return, and FrameToken, a range paired with the colour semanticTokens returns. Diagnostic carries an error's message and place. From LoomProject come projectIn, which finds a file's project in its settings, holds, which asks whether that project holds the file, and loomsIn, which lists the looms a project holds under a folder. Effect supplies the composition primitives, Data among them for the tagged TangleError, and Bun's fs and path read each file and resolve each sink.

TypeScript
import type { CodeMapping, VirtualCode } from '@volar/language-core'
import { Array, Context, Data, Effect, Layer, Match, Option, pipe } from 'effect'
import { readFileSync } from 'node:fs'
import { dirname, resolve as resolvePath } from 'node:path'
import type * as ts from 'typescript'
import { type Diagnostic } from '@athrio/loom-ast/LoomNode'
import { LoomCorpusAstBuilder, ReadError, type Source } from '#ast/LoomCorpusAstBuilder'
import {
  corpusErrors,
  definitionAt,
  moduleDiagnostics,
  reachable,
  referencesAt,
  renameAt,
  renameRangeAt,
  navigationRangeAt,
  transitiveDependents,
  unresolvedTocEntriesIn,
  type CorpusLocation,
  type LoomModule,
  type Path,
} from '@athrio/loom-ast/LoomCorpusAst'
import { faulty, UnresolvedTocEntry } from '#ast/LoomFault'
import { crossFileFaults } from '#ast/ProductBuilder'
import {
  type ComposedFile,
  type FrameLocation,
  type FrameToken,
} from '@athrio/loom-lang-services/LanguageService'
import { normaliseTitle } from '#ast/WeftTokeniser'
import {
  fromProduct,
  fromProse,
  rootNamesAt,
  rootVirtualCode,
  symbolMappings,
} from '#ast/LoomVirtualCodeBuilder'
import { type LoomVirtualCode, type Mapping } from '@athrio/loom-ast/LoomVirtualCode'
import { profileOf, symbolsOf } from '@athrio/loom-ast/LoomSymbol'
import { LoomMemo } from './LoomMemo'
import { PackageConfig } from './PackageConfig'
import { holds, loomsIn, projectIn, type LoomProject } from './LoomProject'
import { WeaveBuilder } from './weave/WeaveBuilder'
import { type WovenCorpus } from './weave/WovenCorpus'
import { TableOfContentsBuilder, rewriteBook } from './weave/TableOfContentsBuilder'
Loom