Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

06 — Snippets, commands, and a task provider

Article 04 introduced the EmitterRegistry and the grammar emitter — the first consumer of the LanguageIR contract. Article 05 wrote the manifest emitter: the one that assembles package.json from fragments. This article writes the two emitters whose fragments the manifest emitter assembles first: the snippet emitter that projects IRSnippet[] into a VSCode .code-snippets JSON file, and the executor emitter that projects IRExecutor[] into a tuple of artifacts — contributes.commands entries, contributes.taskDefinitions entries, and a generated TaskProvider class stub that binds command invocation to shell execution.

These two emitters sit at the border where IR declarations stop being text in a spec.ts fixture and start being things a user types in a VSCode palette. A @Snippet becomes an expandable prefix that fires on tab; an @Executor becomes a titled command id that appears in Ctrl-Shift-P. Both look simple from the outside — a few JSON entries written to disk. Both earn their article because the coordination with the manifest emitter is the design-load-bearing piece: neither snippet nor executor emitter writes package.json; each writes its own contrib fragment to a staging object, and the manifest emitter runs last to assemble. That split is what keeps the SRP lens honest when article 07 adds a fourth emitter and article 08 adds a fifth.

The design-series companion is 04 — SOLID in the monorepo patterns: that article argued for one emitter per output kind, no emitter reaching outside its own kind. This article cashes the argument out for two concrete cases, plus the exception — the manifest emitter — that proves the rule.

REQ-IDEDSL-EXECUTOR-ROUND-TRIP — the Requirement

REQ-IDEDSL-EXECUTOR-ROUND-TRIPFor every @Snippet and every @Executor declaration in a spec.ts source file, the build shall produce VSCode-runtime-usable artifacts such that: (a) the snippet emitter writes one ${outDir}/snippets/${languageId}.code-snippets JSON file per language, preserving each IRSnippet.trigger as the entry key, IRSnippet.body as the prefix-less body array, and IRSnippet.description as the description field; (b) the executor emitter contributes, for each IRExecutor, one contributes.commands entry with a deterministically-derived command id of the form ${extensionId}.${executorId} and title copied from the IR; (c) the executor emitter contributes, for each IRExecutor, one contributes.taskDefinitions entry typed ide-dsl-${languageId} with a required executor property of type string; (d) the executor emitter writes a generated ${outDir}/src/task-provider.ts file exporting a TaskProvider class implementing vscode.TaskProvider whose provideTasks() returns one vscode.Task per IRExecutor and whose resolveTask() dispatches via cp.spawn to the shell command in IRExecutor.command; (e) none of the above emitters writes package.json directly — each contributes a manifest fragment that the article-12 manifest emitter reads last.

Rationale: the value proposition of the Ide.Dsl workflow is that a language author writes one spec.ts source of truth and the extension that ships can run a compliance --strict task from the command palette without the author ever opening a package.json. If the snippet and executor emitters each write their own slice of package.json independently, the last writer wins and the other writes are silently overwritten; debugging that is the kind of build-time race condition that kills a generator project. The fragment-then-assemble model keeps each emitter responsible for exactly one output kind, keeps package.json written by exactly one emitter, and keeps ordering an explicit EmitterRegistry.runAll() concern rather than a filesystem-timing concern. The TaskProvider stub is generated rather than hand-written because the binding from IRExecutor.id to vscode.TaskDefinition is mechanical; writing it by hand would duplicate the IR shape a third time, violating the DRY argument of the companion design article.

Fit criteria: a spec.ts fixture containing @Snippet({ trigger: 'hello', body: ['hello ${1:name}', '$0'], description: 'greeting' }) and @Executor({ id: 'compliance-strict', command: 'compliance --strict', title: 'Compliance — strict' }) shall produce, on build, (i) a snippets/example.code-snippets file whose hello entry has prefix: 'hello', body: ['hello ${1:name}', '$0'], description: 'greeting'; (ii) a contributes.commands entry with command: 'ide-dsl-example.compliance-strict' and title: 'Compliance — strict'; (iii) a contributes.taskDefinitions entry of type ide-dsl-example; (iv) a src/task-provider.ts file that type-checks against @types/vscode and whose resolveTask calls cp.spawn('compliance --strict', ...) for the compliance-strict executor.

Verification: Test. Refines REQ-IDEDSL-DECORATORS-STANDARD and REQ-IDEDSL-EMITTER-REGISTRY-ORDERED.

Two notes on the requirement before the article unfolds. First, the command id derivation is deterministic on purpose — ${extensionId}.${executorId} is a single rule, with no per-executor escape hatch. A language author who wants a command id that does not match their executor id has to rename the executor; the generator refuses to be configurable on this point. Determinism here is what lets the task provider recover the executor by string-matching the suffix, which is what lets the resolveTask() body stay one case analysis instead of a registry lookup.

Second, the ide-dsl-${languageId} type prefix on contributes.taskDefinitions exists to keep task types disjoint across Ide.Dsl-generated extensions installed side-by-side. VSCode matches tasks by their type string at runtime; two extensions declaring the same task type would collide in the task picker. Namespacing by language id is the cheapest way to make the extension installable next to itself with a different spec.ts and not ship a bug report on day one.

FEAT-IDEDSL-06 — the satisfying Feature

// packages/ide-dsl/requirements/features/snippets-and-executors.ts
import { Feature, Priority, Satisfies, type ACResult } from '@frenchexdev/requirements';
import { ReqIdeDslExecutorRoundTripRequirement } from '../requirements/req-idedsl-executor-round-trip.js';

@Satisfies(ReqIdeDslExecutorRoundTripRequirement)
export abstract class SnippetsAndExecutorsFeature extends Feature {
  readonly id = 'FEAT-IDEDSL-06';
  readonly title = 'SnippetEmitter and ExecutorEmitter project @Snippet and @Executor into VSCode-runtime artifacts';
  readonly priority = Priority.Critical;

  // ── Snippets ──
  abstract snippetEmitterWritesOneCodeSnippetsFilePerLanguage(): ACResult;
  abstract snippetEmitterPreservesTriggerBodyAndDescription(): ACResult;
  abstract snippetRoundTripIrToJsonToIrIsIdentity(): ACResult;

  // ── Commands ──
  abstract executorEmitterDerivesCommandIdDeterministically(): ACResult;
  abstract executorEmitterContributesOneCommandPerExecutor(): ACResult;

  // ── Task definitions and provider ──
  abstract executorEmitterContributesOneTaskDefinitionPerExecutor(): ACResult;
  abstract executorEmitterNamespacesTaskTypeByLanguageId(): ACResult;
  abstract generatedTaskProviderTypeChecksAgainstVscodeTypes(): ACResult;
  abstract generatedTaskProviderResolveTaskDispatchesViaCpSpawn(): ACResult;

  // ── Coordination with the manifest emitter ──
  abstract neitherEmitterWritesPackageJsonDirectly(): ACResult;
  abstract bothEmittersContributeFragmentsConsumedByManifestEmitter(): ACResult;
}

Eleven ACs in four clusters. The Snippets cluster nails the .code-snippets shape and the IR round-trip. The Commands cluster nails command id derivation. The Task definitions cluster nails the task type namespacing and the generated provider stub. The Coordination cluster — the two ACs at the bottom — is the one that proves neither emitter steps on the manifest emitter's toes; without those, the SOLID argument in the companion design article never lands.

The snippet emitter

The snippet emitter is the shortest of the seven. It reads ir.snippets, it writes one JSON file, it is done. The shape of a VSCode .code-snippets file has been stable since the TextMate-snippets-inspired format was folded into VSCode in 2016 — it is one of the few VSCode APIs older than the LSP server-side extension points — and the entries are a flat object keyed by a human-readable snippet name, each containing prefix, body, and optional description. The VSCode snippets grammar documents the tab-stop ${1:placeholder} and final-cursor $0 syntax; the emitter does not interpret any of it, it just copies the body lines through.

// packages/ide-dsl/src/emitters/snippet-emitter.ts
import type { Emitter, FileSystem } from '../emitter.js';
import type { LanguageIR, IRSnippet } from '../ir.js';

interface VscodeSnippetEntry {
  readonly prefix: string;
  readonly body: readonly string[];
  readonly description?: string;
}

type VscodeSnippetsFile = Record<string, VscodeSnippetEntry>;

export class SnippetEmitter implements Emitter {
  readonly kind = 'snippets' as const;

  async emit(ir: LanguageIR, fs: FileSystem, outDir: string): Promise<void> {
    const entries: VscodeSnippetsFile = {};
    for (const snippet of ir.snippets) {
      entries[snippet.trigger] = {
        prefix: snippet.trigger,
        body: snippet.body,
        ...(snippet.description !== undefined && { description: snippet.description }),
      };
    }
    const path = `${outDir}/snippets/${ir.languageId}.code-snippets`;
    await fs.mkdir(`${outDir}/snippets`, { recursive: true });
    await fs.writeFile(path, JSON.stringify(entries, null, 2) + '\n');
  }
}

Three observations about what this emitter does not do. It does not validate the body — the IR smart constructors of article 03 already ran ajv on it, and re-running that validation here would be the DRY violation the design article warns against. It does not sort the keys — insertion order in a JSON object is stable in V8 since 2015 and the IR preserved the source order from the spec.ts file; sorting would destroy that source affinity and make the snippet file harder to read next to the @Snippet declarations. It does not write to package.json — the .code-snippets file is registered with the extension through contributes.snippets, which is a manifest fragment the emitter returns via the staging object (see below), not a field it writes itself.

The manifest fragment the snippet emitter contributes is minimal:

// returned to the staging object, consumed by the manifest emitter
{
  contributes: {
    snippets: [
      { language: ir.languageId, path: `./snippets/${ir.languageId}.code-snippets` },
    ],
  },
}

One entry, pointing the manifest at the file just written. The manifest emitter of article 05 merges this fragment into contributes by kind; if article 07 adds a second language with a second snippet file, two entries end up in the array. The snippet emitter never sees the other language; the manifest emitter never inspects the snippet file contents. Each stays in its lane.

The executor emitter

The executor emitter is the longest of the seven, and the only one that writes a generated .ts source file. It produces three outputs per invocation: one manifest fragment for contributes.commands, one manifest fragment for contributes.taskDefinitions, and one src/task-provider.ts file. The vscode.TaskProvider API has been stable since VSCode 1.32 (February 2019); the shape has not changed across the 1.80s and 1.90s. Task providers are how custom task runners — npm, gulp, maven, and now Ide.Dsl — register tasks discoverable through the command palette's "Run Task" entry, dispatched through a resolveTask() that the host calls lazily when the user actually picks one.

// packages/ide-dsl/src/emitters/executor-emitter.ts
import type { Emitter, FileSystem } from '../emitter.js';
import type { LanguageIR, IRExecutor } from '../ir.js';

export class ExecutorEmitter implements Emitter {
  readonly kind = 'executors' as const;

  async emit(ir: LanguageIR, fs: FileSystem, outDir: string): Promise<void> {
    const source = this.renderTaskProvider(ir);
    await fs.mkdir(`${outDir}/src`, { recursive: true });
    await fs.writeFile(`${outDir}/src/task-provider.ts`, source);
    // The manifest fragments — commands + taskDefinitions — are
    // returned through the staging object read by the manifest emitter.
  }

  private renderTaskProvider(ir: LanguageIR): string {
    const extensionId = `ide-dsl-${ir.languageId}`;
    const taskType = extensionId;
    const cases = ir.executors.map((e) => this.renderCase(e, taskType)).join('\n');
    return [
      "import * as vscode from 'vscode';",
      "import * as cp from 'child_process';",
      '',
      `export const TASK_TYPE = '${taskType}';`,
      '',
      'export class TaskProvider implements vscode.TaskProvider {',
      '  provideTasks(): vscode.Task[] {',
      '    return [',
      ir.executors.map((e) => `      this.taskFor('${e.id}', '${e.title}'),`).join('\n'),
      '    ];',
      '  }',
      '',
      '  resolveTask(task: vscode.Task): vscode.Task | undefined {',
      "    const executor = task.definition['executor'];",
      '    switch (executor) {',
      cases,
      '      default: return undefined;',
      '    }',
      '  }',
      '',
      '  private taskFor(executor: string, title: string): vscode.Task {',
      '    return new vscode.Task(',
      `      { type: TASK_TYPE, executor },`,
      '      vscode.TaskScope.Workspace,',
      '      title,',
      '      TASK_TYPE,',
      '      new vscode.ShellExecution(this.commandFor(executor)),',
      '    );',
      '  }',
      '',
      '  private commandFor(executor: string): string {',
      '    switch (executor) {',
      ir.executors.map((e) => `      case '${e.id}': return ${JSON.stringify(e.command)};`).join('\n'),
      "      default: throw new Error(`unknown executor: ${executor}`);",
      '    }',
      '  }',
      '}',
      '',
    ].join('\n');
  }

  private renderCase(e: IRExecutor, taskType: string): string {
    return `      case '${e.id}': return this.taskFor('${e.id}', '${e.title}');`;
  }
}

The generated file is plain TypeScript — no template engine, no AST rewriter. String concatenation suffices because the inputs are already branded at IR level: an ExecutorId is kebab-case-constrained by its smart constructor, so the single-quoted case label is free of injection hazard; IRExecutor.command is passed through JSON.stringify which escapes the shell fragment safely into a TypeScript string literal. Article 11 on "generated code you actually want to read" will come back to this — the argument is that a string-concatenation generator whose output is committed and diff-reviewed is dramatically easier to maintain than an AST-based one whose output is hidden behind a visitor pattern.

The two manifest fragments returned to the staging object are:

// contributes.commands fragment
{
  contributes: {
    commands: ir.executors.map((e) => ({
      command: `ide-dsl-${ir.languageId}.${e.id}`,
      title: e.title,
    })),
  },
}

// contributes.taskDefinitions fragment
{
  contributes: {
    taskDefinitions: [
      {
        type: `ide-dsl-${ir.languageId}`,
        required: ['executor'],
        properties: { executor: { type: 'string' } },
      },
    ],
  },
}

One commands entry per executor; one taskDefinitions entry per language — the executor discriminator inside the task definition is what maps multiple executors onto a single task type, which is the shape VSCode's task picker expects.

Figure 1 — the executor lifecycle

The lifecycle of a single @Executor declaration, from spec.ts author-time to VSCode runtime, is worth seeing as a sequence diagram. The same IRExecutor shape passes through four actors — the extractor, the executor emitter, the manifest emitter, and the VSCode task system — each of which reads a different slice of the same contract.

Diagram
Figure 1 — The @Executor lifecycle, from spec.ts declaration to VSCode palette invocation and shell execution.

The diagram is labelled by actor rather than by file. Actor 3 (ExecutorEmitter) does three things: it writes src/task-provider.ts, and it returns two fragments to the staging object. Actor 4 (ManifestEmitter) is the only writer of package.json. Actor 5 (VSCode host) reads package.json at extension-load time, then dispatches runtime invocations to the generated TaskProvider class from actor 3's file output. The whole round trip keeps each emitter responsible for one output kind.

Figure 2 — the two emitter classes

The second figure shows the two emitter classes as a class diagram, with their kind discriminants, their emit signature inherited from the Emitter port of article 04, and the files each writes. This is the static view; figure 1 was the dynamic view.

Diagram
Figure 2 — SnippetEmitter and ExecutorEmitter against the Emitter port from article 04. Each class owns one output kind; neither writes package.json.

The dashed arrows are not Java-style "depends on" — they are a data-flow hint. SnippetEmitter and ExecutorEmitter do not hold a reference to ManifestEmitter; they write to a staging object that ManifestEmitter reads. The EmitterRegistry.runAll() of article 04 is what brings them into contact in registered order — manifest last — and the staging object is the one piece of shared mutable state the registry tolerates.

How the manifest emitter consumes these outputs

The coordination model needs one more paragraph of attention because it is the most asked-about decision in the series. Three shapes were considered. The first was the naive shape — each emitter writes its own slice of package.json directly, last-writer-wins, with no staging. That shape was rejected the first time two emitters wrote the same top-level field; debugging it took an afternoon and a git-bisect.

The second shape was the orchestrator shape — the EmitterRegistry.runAll() itself accumulates fragments, with emitters returning { files: {...}, manifestFragment: {...} } instead of writing through the FileSystem port. That shape was rejected on SRP grounds: it makes the registry responsible for two concerns, file writing and fragment accumulation, where before it was responsible only for running emitters in order.

The third shape — the one the article argues for — is the staging-object shape. The FileSystem port from article 04 gains no new method; instead, a second port, ManifestStaging, is passed alongside it to emitters that contribute to package.json. The snippet emitter and the executor emitter both implement a narrower interface that receives both ports; the grammar emitter from article 04, which writes a .tmLanguage.json and contributes nothing to the manifest, implements only the narrower Emitter interface. The registry treats them uniformly by introspecting which ports each emitter accepts — a cheap runtime check at registration time — or, more cleanly still in TypeScript, by declaring two sibling interfaces and letting the type system sort them.

// packages/ide-dsl/src/manifest-staging.ts
export interface ManifestFragment {
  readonly contributes?: Partial<Record<string, readonly unknown[]>>;
}

export interface ManifestStaging {
  contribute(fragment: ManifestFragment): void;
  assemble(): Record<string, unknown>;
}

The ManifestEmitter of article 05 holds the ManifestStaging instance; its emit() calls assemble() to read the accumulated fragments, merges in the static base fields (name, version, engines), and writes package.json. The EmitterRegistry.runAll() runs emitters in registered order with the manifest emitter last; registration order is enforced by a test that fails if the manifest emitter appears anywhere other than the tail of the registry. That test is the one AC — bothEmittersContributeFragmentsConsumedByManifestEmitter — that makes the coordination story testable end-to-end rather than by reading the code.

Tests

// packages/ide-dsl/test/unit/emitters/snippets-and-executors.test.ts
import { expect } from 'vitest';
import { FeatureTest, Verifies } from '@frenchexdev/requirements/test';
import { SnippetEmitter } from '../../../src/emitters/snippet-emitter.js';
import { ExecutorEmitter } from '../../../src/emitters/executor-emitter.js';
import { SnippetsAndExecutorsFeature } from '../../../requirements/features/snippets-and-executors.js';
import { makeInMemoryFs, makeFixtureIR } from '../helpers.js';

@FeatureTest(SnippetsAndExecutorsFeature)
export class SnippetsAndExecutorsFeatureTest {
  @Verifies('snippetRoundTripIrToJsonToIrIsIdentity')
  async snippetsSurviveTheJsonRoundTrip() {
    const ir = makeFixtureIR({
      languageId: 'example',
      snippets: [
        { trigger: 'hello', body: ['hello ${1:name}', '$0'], description: 'greeting' },
      ],
    });
    const fs = makeInMemoryFs();
    await new SnippetEmitter().emit(ir, fs, '/out');
    const written = await fs.readFile('/out/snippets/example.code-snippets');
    const parsed = JSON.parse(written);
    expect(parsed).toEqual({
      hello: { prefix: 'hello', body: ['hello ${1:name}', '$0'], description: 'greeting' },
    });
  }

  @Verifies('executorEmitterDerivesCommandIdDeterministically')
  async executorCommandIdIsDeterministic() {
    const ir = makeFixtureIR({
      languageId: 'example',
      executors: [
        { id: 'compliance-strict', command: 'compliance --strict', title: 'Compliance — strict' },
      ],
    });
    const fs = makeInMemoryFs();
    const staging = makeStaging();
    await new ExecutorEmitter(staging).emit(ir, fs, '/out');
    const commands = staging.assemble().contributes.commands;
    expect(commands).toEqual([
      { command: 'ide-dsl-example.compliance-strict', title: 'Compliance — strict' },
    ]);
  }

  @Verifies('generatedTaskProviderTypeChecksAgainstVscodeTypes')
  async generatedTaskProviderCompiles() {
    const ir = makeFixtureIR({
      languageId: 'example',
      executors: [
        { id: 'compliance-strict', command: 'compliance --strict', title: 'Compliance — strict' },
      ],
    });
    const fs = makeInMemoryFs();
    await new ExecutorEmitter(makeStaging()).emit(ir, fs, '/out');
    const source = await fs.readFile('/out/src/task-provider.ts');
    const diagnostics = await typecheckInMemory(source, { libs: ['vscode'] });
    expect(diagnostics).toEqual([]);
  }

  @Verifies('neitherEmitterWritesPackageJsonDirectly')
  async noEmitterWritesPackageJson() {
    const ir = makeFixtureIR({ languageId: 'example' });
    const fs = makeInMemoryFs();
    await new SnippetEmitter().emit(ir, fs, '/out');
    await new ExecutorEmitter(makeStaging()).emit(ir, fs, '/out');
    expect(await fs.exists('/out/package.json')).toBe(false);
  }
}

The typecheckInMemory helper is the same one article 04 used for the grammar emitter's golden snapshot — it runs a transient ts.createProgram with @types/vscode on the compiler path and returns the diagnostic array. The test passes iff the generated source compiles; it is the cheapest end-to-end gate the series has on "the generated code is actually usable", short of firing up a VSCode Extension Host and clicking the command.

SOLID lens

Single responsibility. The snippet emitter writes one kind of file and returns one kind of fragment. The executor emitter writes one generated .ts source and returns two kinds of fragments (commands and taskDefinitions) — which is still one output kind in the coordination model, because both fragments describe the same @Executor declaration from two angles. Neither emitter knows that package.json exists; neither emitter reads the other's output. The responsibility carved at the class boundary matches the responsibility carved at the decorator boundary in article 01.

Open/closed. Adding a new kind of executor — say, a hypothetical @AsyncExecutor for long-running jobs that surface a progress notification — is an IR-level extension: add an IRAsyncExecutor type, add a branded AsyncExecutorId, extend the extractor to recognize the decorator, and the ExecutorEmitter gains a second provideTasks branch without losing the first. The snippet emitter does not change. The manifest emitter does not change. The EmitterRegistry does not change. Closed against the existing decorators; open to the new ones.

Liskov substitution. Both classes implement the Emitter port of article 04. The registry's runAll() loops over emitters calling emit(ir, fs, outDir) without case analysis; swapping SnippetEmitter for a differently-implemented snippet emitter that produces the same .code-snippets file would be invisible to the registry and to every test except the byte-level golden snapshot.

DRY lens

The prefix / body / description shape of a .code-snippets entry lives in exactly one place: the IRSnippet type in article 03. The snippet emitter does not copy it, does not redefine it, does not mirror it. The VSCode file format happens to use the same field names — prefix, body, description — which is a coincidence worth noting but not one the emitter depends on; if VSCode renamed prefix to trigger tomorrow, the snippet emitter would gain one line of translation, the IR would stay fixed, and every downstream consumer that reads the IR (the LSP server, the traceability report, the golden snapshot tests) would stay fixed too. That is the payoff of the DRY-on-the-IR argument from 05 — DRY and the IR as contract: the IR is the one model, and every projection is a thin strategy on top of it.

The executor emitter makes the same bet twice. The contributes.commands entries, the contributes.taskDefinitions entries, the TaskProvider.provideTasks return array, and the TaskProvider.commandFor switch all read from ir.executors directly. There is no intermediate "executor model" object, no "compiled executors" cache. If the IR grows a new field, the emitter grows at most one branch. If the VSCode API changes, the emitter changes; nothing else does.

The design argument for this article lives at 04 — SOLID in the monorepo patterns. The EmitterRegistry port that both emitters implement was introduced in 11 — EmitterRegistry + grammar emitter. The ManifestEmitter that consumes the fragments was written in 12 — Manifest emitter. The LanguageIR contract that feeds both emitters was nailed down in 10 — LanguageIR as contract. The @Snippet and @Executor decorators whose declarations this article projects were written in 08 — Bootstrapping Ide.Dsl.

Proposal (write-in-public). The TaskProvider-based dispatch is the canonical VSCode pattern, but it is not the only one. A terminal-based shape — vscode.window.createTerminal({ name: title }).sendText(command) — is one line shorter, streams stdout into a terminal the user already knows how to read, and skips the contributes.taskDefinitions entry entirely. The cost is that task output is not structured as a vscode.Task (no exit-code wiring, no problem-matcher integration, no headless-runner compatibility). For a compliance --strict executor whose interesting output is a traceability report, the problem-matcher integration is free value — exit code 1 becomes a VSCode problem marker — and the TaskProvider shape is clearly right. For a hypothetical serve --watch executor whose interesting output is a long-running log, the terminal shape might be the better fit. I am inclined to ship TaskProvider now and consider a @TerminalExecutor variant later; feedback welcome.

Next build article: 14 — the LSP stub emitter and the handler registration surface — which will show how @LspFeature declarations become a generated packages/ide-dsl/lsp/src/handlers.ts registry whose shape is closer to a dispatch table than to an emitter output.

⬇ Download