Tsonic GitHub

Architecture Overview

High-level view of the Tsonic compiler architecture.

Design Principles

1. Layered Architecture

The compiler is organized into distinct layers with clear boundaries:

CLI Layer
    |
    v
Frontend Layer (TypeScript -> IR)
    |
    v
Emitter Layer (IR -> C#)
    |
    v
Backend Layer (C# -> Binary)

Each layer:

  • Has a single responsibility
  • Knows nothing about layers above
  • Communicates via well-defined interfaces

2. Functional Core

All business logic is pure functional:

  • No mutable state
  • Functions return values, not modify parameters
  • Side effects isolated to boundaries (CLI, I/O)

3. Explicit Over Implicit

  • All dependencies passed as parameters
  • No global state or singletons
  • Configuration explicit, not magic

4. Error as Values

Errors are returned, not thrown:

const result = compile(source);
if (!result.ok) {
  console.log(result.error);
  return;
}
const { value } = result;

Component Overview

CLI (packages/cli)

Command-line interface and orchestration:

  • Argument parsing
  • Configuration loading
  • Command dispatch (project init, generate, build, run)
  • Error reporting

Entry point for all user interactions.

Frontend (packages/frontend)

TypeScript to IR transformation:

  • TypeScript program creation (via TS Compiler API)
  • Module resolution
  • Import/export validation
  • IR building
  • Dependency graph construction

Emitter (packages/emitter)

IR to C# code generation:

  • Type emission
  • Expression emission
  • Statement emission
  • Generic specialization
  • Module/namespace structure

Backend (packages/backend)

.NET compilation:

  • .csproj generation
  • Program.cs generation
  • dotnet CLI wrapper
  • Build orchestration

Runtime (runtime/)

C# runtime libraries:

  • Tsonic.Runtime: Core types and utilities (IteratorResult, etc.)

Data Flow

Compilation Pipeline

1. CLI receives: tsonic build src/App.ts

2. Config loaded:
   - tsonic.json parsed
   - CLI args merged
   - Resolved config created

3. Frontend processes:
   - TypeScript program created
   - Source files parsed
   - Imports resolved
   - IR modules built
   - Dependency graph constructed

4. Emitter generates:
   - C# files from IR modules
   - Program.cs entry point
   - tsonic.csproj project file

5. Backend compiles:
   - dotnet restore
   - dotnet publish (NativeAOT)
   - Binary copied to out/

Key Data Structures

IrModule: Represents a compiled TypeScript file

type IrModule = {
  filePath: string;
  namespace: string;
  className: string;
  imports: IrImport[];
  exports: IrExport[];
  body: IrStatement[];
};

ResolvedConfig: Merged configuration

type ResolvedConfig = {
  rootNamespace: string;
  entryPoint: string;
  sourceRoot: string;
  outputDirectory: string;
  // ...
};

Error Handling

Diagnostics

All errors are collected as diagnostics:

type Diagnostic = {
  code: string; // e.g., "TSN1001"
  severity: "error" | "warning";
  message: string;
  location?: SourceLocation;
  suggestion?: string;
};

Diagnostic Codes

Range Category
TSN1xxx Module resolution
TSN2xxx Syntax/feature support
TSN3xxx Type system
TSN4xxx Code generation
TSN5xxx Build/runtime

Error Propagation

Errors bubble up through Result types:

const parseResult = parse(source);
if (!parseResult.ok) return parseResult;

const validateResult = validate(parseResult.value);
if (!validateResult.ok) return validateResult;

// Continue with valid data

Extension Points

Custom Libraries

External .NET library bindings via --lib:

  • TypeScript declarations describe .NET API
  • Compiler resolves types
  • Generated code references library

Output Types

Multiple output configurations:

  • Executable (NativeAOT)
  • Library (DLL)
  • Console app (non-AOT)

NuGet Integration

Packages specified in config:

  • Added to .csproj
  • Restored by dotnet
  • Available in generated code