How It Works

Transpose is a source-to-source compiler: it reads C# and emits JavaScript. You write application logic against a browser-side implementation of the .NET base class library, and the result runs in any browser.

Compilation pipeline

Transpose is built entirely on Roslyn — the same compiler front end the C# compiler uses. There is no separate parser and no intermediate lowering pass.

flowchart LR S["C# sources"] --> CB["Roslyn compilation<br/>+ semantic model"] CB --> D["Roslyn diagnostics"] CB --> U["Unsupported-feature scan"] CB --> E["Emitter<br/>(syntax walk)"] E --> J["JavaScript"]
1

Bind:

Roslyn parses the sources and builds a compilation whose only base-library reference is Transpose.dll, the browser-side BCL. Every type your code uses resolves against that.

2

Report Roslyn diagnostics:

Ordinary C# errors fail the build first, in the same form the C# compiler produces them. A misspelled member is CS0117, not a runtime surprise.

3

Scan for unsupported features:

Constructs that have no browser equivalent — DllImport / LibraryImport P/Invoke, inline arrays — are reported as errors rather than emitted as broken code.

4

Emit:

The emitter walks the Roslyn syntax tree, guided by the semantic model, and writes JavaScript directly.

5

Write the output:

The runtime, the bundle, the reflection metadata, resources and index.html are assembled into the output folder — or, for a library, embedded into a .NET DLL that referencing projects consume.

The output is reproducible: the same sources and the same compiler produce a byte-identical site and a byte-identical assembly.

Runtime environment

Generated code depends on a small runtime library, tps.js, which ships inside the Transpose.BCL package. It provides:

  • Type system — C# classes, structs, interfaces, enums and generics modelled in JavaScript. Types are registered with Transpose.define(...).
  • Base class library — implementations of the core .NET types: System.String, the integer and floating-point types, System.Decimal, System.Int64, System.DateTime, System.TimeSpan, System.Console, System.Text.RegularExpressions, the generic collections, LINQ, Task, and more.
  • Language helpers — reached through the TransposeR shim.
  • Reflection — the metadata that backs System.Reflection at runtime.

The generated index.html loads tps.js before your bundle. A project that supplies its own page has to do the same, in that order.

C# feature support

Transpose supports the C# language broadly, including:

  • Classes, structs and records — inheritance, polymorphism, with expressions.
  • Interfaces — including default interface members.
  • Generics — including constraints; type arguments are threaded at runtime where the emitted code needs them.
  • Lambdas, local functions and delegates — emitted as JavaScript functions.
  • LINQ — see LINQ Support.
  • async/await — see Async Support.
  • Iteratorsyield return / yield break.
  • Pattern matching — type, constant, relational, list and property patterns, switch expressions.
  • Events, operator overloading, indexers, extension methods, nullable reference types, tuples and deconstruction.
  • Attributes — both to drive code generation and as reflection metadata. See the Attribute Reference.

Differences from .NET on the desktop

Some differences follow from the browser, and some are current limitations of the compiler. Both matter when porting code.

Consequences of running in a browser

  • Garbage collection is the JavaScript engine's. There is no GC to tune and no finalizer guarantee.
  • Threading: JavaScript is single-threaded. Task and async/await work through the event loop, but System.Threading.Thread does not, and Task.Run does not move work to another thread — see Async Support.
  • File system: System.IO file access has no browser equivalent. Use localStorage, IndexedDB, or the File System Access API through Transpose.Core.
  • P/Invoke (DllImport / LibraryImport) and inline arrays are reported as compile errors.

Current compiler limitations

  • Struct value-copy semantics apply to structs declared in the project being compiled. Assigning such a struct, passing it by value, returning it, boxing it, or putting it in a collection clones the value, recursively through its struct-typed fields. A struct from a referenced library, a BCL struct (DateTime, TimeSpan, …) and a ValueTuple are copied by reference instead, so var b = a; b.Inner.V = 9; writes through to a for those. This is a deliberate trade-off: widening it would make every DateTime or tuple assignment allocate.
  • Span<T> does not accept the implicit array conversion. Span<int> s = new int[3]; emits the bare array, so s[0] = 1 fails at runtime. Use the array directly.
  • A positional pattern only resolves members for tuples and records. x is Foo(1, 2) against a type with a hand-written Deconstruct(out …) reads Item1/Item2 instead of calling Deconstruct, because a pattern test is emitted as a single expression and cannot pass out-parameters.
  • Source maps are not emitted yet — see Exceptions & Debugging.
  • Module formats (AMD / CommonJS / ES6 / UMD) are not implemented — see Output Types.
© 2026 Curiosity. All rights reserved.