Changelog

Release notes for the Transpose compiler, newest first. Each entry lists the published NuGet package versions for that release.

v26.7.3204

Features

Feature

Watch mode

tps --watch rebuilds the site whenever a source file changes, in the root project or in any project it references, and serves the result over a local development server (--watch-port, default 4300). The page it serves reconnects over a websocket and reloads itself after each rebuild, so the edit-and-see loop needs no manual build or refresh. See Watch mode.

Feature

A stylesheet change skips the compiler

When every file in a batch of changes is a source of a stylesheet the last build already produced, the CSS is re-copied and the page swaps it in place instead of reloading. The running application keeps its state: open panels stay open and the current route stays put. Anything else is a real rebuild.

Feature

Incremental builds are on by default

The MSBuild SDK now passes --incremental, so an ordinary dotnet build reuses the previous build of each project: nothing is compiled when every input hashes the same, and a body-only edit keeps the cached JavaScript of every untouched type. The output is byte-identical either way. Turn it off for a project with <TransposeIncremental>false</TransposeIncremental>. A build's output is also recoverable, so a cache that goes missing only costs a full build.

Feature

The compiler as a library

Transpose.Compiler.Library compiles C# to JavaScript in-process: compile in-memory sources, build a real .csproj, or drive the same watch loop from a host that runs its own web server. It ships at the same version as Transpose.Compiler, from the same release.

Language and Compiler

Feature

Two members that would collide in JavaScript are now an error

When two members of a type resolve to the same emitted name, only the last would exist at runtime and which one survived depended on declaration order. The compiler now reports TransposeR0003 and names both members. C# overloads cannot trigger this, since they take numbered slots, so it means two members carry the same explicit [Name].

Fixed

A struct copy carries its nested struct state

Copying a struct that itself holds struct-typed fields now clones those fields too, so mutating the copy no longer writes through to the original. The object and collection initializer paths that previously skipped the copy are covered as well.

Fixed

A struct local declared without an initializer is emitted as a zeroed struct

It is no longer left undefined until first assigned.

Fixed

Nested member initializers

Home = { City = "x" }, Tags = { "a", "b" } and Scores = { ["k"] = 7 } apply to the member's current value rather than assigning a new one. The first two used to emit an array literal that overwrote a getter-only collection, and the indexed form failed to translate at all.

Fixed

A plain struct has value equality and hashing

.NET gives every value type field-wise Equals and GetHashCode, which is what makes a struct usable as a dictionary or set key. Only records synthesized them before, so a plain struct fell back to reference identity and a lookup for a key just added threw. Both are now synthesized over the struct's instance fields unless it declares its own.

Fixed

`DateTime` equality compares the instant

== and != on two separately constructed DateTime values with the same instant used to be unequal, and lifted DateTime? equality had the same problem.

Fixed

A record's `ToString()` matches C#

A hand-written ToString or PrintMembers is now used instead of being ignored, and a derived record chains to its base. The output covers public fields as well as public readable properties, excludes non-public members, prints an overridden member once rather than twice, and renders a char as its character and an enum as its member name rather than as numbers. The char and enum fix applies to plain string concatenation too.

Fixed

Record equality compares instance fields, base record first

This is what C# does, so a public field now participates and a computed property no longer does. Previously record Node(int V) { public int[] Cache => new[] { V }; } compared two equal values as unequal, because each read of Cache allocated a new array. Equals(object) delegates to the typed Equals, so a record's own Equals and GetHashCode govern ==, dictionary and set membership alike.

Fixed

Record construction

Optional positional parameters now take their defaults, a positional parameter no longer clobbers a member the record itself declared, and a record struct's implicit parameterless constructor is emitted so new S() works. with on a record with a get-only computed property no longer throws. A plain struct's parameterless constructor gained the same fix: it zeroes the value rather than running declared field initializers, matching C#.

Fixed

A hand-written `Deconstruct` no longer replaces the synthesized one

Both overloads coexist and a call site resolves the one it bound.

Fixed

`[ObjectLiteral]` on a record keeps its positional arguments

new Point(1, 2) on [ObjectLiteral] record Point(int X, int Y) emits {X: 1, Y: 2}; it used to emit an empty object and drop them.

Fixed

Instance field initializers run before the base constructor

That is C#'s order for every derived type. A synthesized or primary constructor ran them after the base call, so a base constructor observed the derived fields still at their defaults.

Fixed

A virtual or overriding auto-property has storage of its own

.NET gives each declaration its own backing field, reachable across levels only through virtual dispatch. Collapsing them into one location meant a base initializer landed on the override's value and no read dispatched. A non-virtual auto-property is unchanged.

Fixed

`base.P` reaches the base type's accessor

A property read or write through base used to run the most-derived override, which is exactly what base is meant to bypass.

Fixed

A local never shadows a type reference

A parameter, local, out var, foreach or catch variable, or query range variable with the same name as a type no longer hides it, which previously produced a call against the local and failed at runtime.

Runtime and Base Library

Feature

`Task` can be handed to a JavaScript API that wants a promise

A ToPromise() extension binds the runtime adapter the compiler already uses for await, so a C# task crosses into JavaScript as a thenable. A faulted task rejects the promise rather than dropping the exception, and the handler receives exactly the task's result.

Feature

The nullability and required-member annotations are available

SetsRequiredMembers is a compiler-required member, so a required member did not compile at all without it. AllowNull, DisallowNull, MaybeNull(When), NotNull(When/IfNotNull), DoesNotReturn(If) and MemberNotNull(When) carry no runtime behaviour but likewise had to exist for annotated code to compile.

Fixed

`string.Join` and `string.Concat` render their members through .NET's `ToString()`

A sequence of non-string values now formats the way it does on .NET.

Fixed

Fractional seconds parse as a fraction

A timestamp ending in .25 read as 25 milliseconds instead of 250, so a round trip through a date string did not return the original value.

Fixed

`object.ToString()` follows .NET rather than JavaScript

An array reports its type name (System.Int32[]) instead of its joined elements, so "" + new int[] { 1, 2 } no longer gives 1,2. A DateTime reports the culture's general date and time pattern instead of the JavaScript date string, a Type reports its display name, and a type with no ToString of its own falls back to its type name with generic arguments named plainly.

Web and Package Bindings

Improved

The JSON package is verified against the real Json.NET

Every case in the suite now runs twice, once natively against Json.NET and once as translated JavaScript, and the outputs are diffed, so a divergence has to be deliberate and is recorded. Building it out corrected several behaviours: DefaultValueHandling.Ignore drops null members; the camel-case resolver lowercases a leading run of capitals the way Json.NET does (ALLCAPS becomes allcaps); TypeNameHandling.Auto writes the type name again; a type name that does not resolve is ignored rather than fatal when the caller did not ask for type names; an empty or whitespace-only document deserializes to the default value; and System.Version round-trips as its 1.2.3.4 string. The remaining documented differences from Json.NET are listed in the JSON serialization guide.

Improved

`Transpose.Newtonsoft.Json` no longer relies on runtime code evaluation

A payload using Json.NET's lenient syntax (comments, single quotes, unquoted names, trailing commas) previously fell back to the JavaScript evaluator, which a Content-Security-Policy without 'unsafe-eval' blocks outright. It now goes through a hand-written reader, so the package works under a strict policy. The reader is also slightly stricter, matching Json.NET in rejecting undefined, a leading + on a number and a signed hexadecimal literal.

Fixed

Polymorphic payloads deserialize through a custom serialization binder

Even when the binder is implemented implicitly, which covers interface-typed arrays, an interface-typed member nested in such an array, a null polymorphic slot, and an outer type whose only constructor takes arguments.

Build and Tooling

Feature

A resource can be copied without being loaded

A tps.json resource group takes "load": false, which still writes the file into the output and still embeds it into a package, but leaves loading it to the application: a module fetched on demand, or a stylesheet swapped in at runtime. The legacy .dontload name suffix keeps working, and either spelling alone suppresses the injection. The flag travels in the package's resource manifest, so a project referencing the library extracts the file without auto-loading it either. See Resources.

Feature

Assemblies carry a minimum compiler version, and it is enforced

Every assembly the compiler produces records the compiler that built it and the oldest compiler allowed to consume it. Before compiling, the compiler checks every assembly the project binds against and fails with TPS0008 if one needs a newer compiler, naming the version required and the command that installs it. Because a package's real payload is JavaScript and the consuming compiler decides how emitted code binds to it, an older compiler fed a newer package would otherwise produce a subtly wrong bundle instead of an error. Assemblies published before the stamp existed are skipped, so older packages keep working.

Improved

Package references are reconciled across a project reference closure

Two projects in one build can no longer bind different versions of the same package.

Improved

The newest cached base library is resolved by version

A stale copy in the package cache no longer wins over a newer one.

Improved

Reading a package's embedded resources no longer loads the assembly

Loading it kept the file locked for the process's lifetime and resolved by assembly identity, which is what made the second rebuild of a multi-project app in watch mode fail to write its assembly and made a rebuilt library's resources come back stale.

Fixed

A project is no longer mistaken for the runtime library itself

Whether outputBy: ClassPath selects the runtime build is decided by the project's assembly name. The previous test looked at whether the base library appeared among the resolved references, which it never does (the compiler injects it), so any project whose Transpose.BCL package was not yet in the NuGet cache was compiled self-contained and failed on every predefined type.

v26.7.3041

Features

Feature

Opt-in incremental compilation

tps --incremental reuses the previous build of a project: a build whose inputs all hash the same does no work at all, and an edit confined to method or accessor bodies reuses the cached JavaScript of every type it did not touch, the reflection metadata and, in a metadata-only configuration, the emitted assembly. The output is byte-identical either way, and the cache lives in the project's obj/.

Feature

The base library, Web bindings and JSON support are published

Transpose.BCL, Transpose.Core, Transpose.Newtonsoft.Json and Transpose.HttpClient are now on NuGet, so a project can reference the toolchain rather than build it.

Language and Compiler

Feature

Minified output

outputFormatting selects Formatted, Minified or Both. When both variants exist the build configuration decides which is wired into the generated page: Release keeps the minified page as index.html, Debug the readable one. Authored resources are never re-minified, only Transpose-emitted JavaScript is.

Feature

The conditional-compilation symbol is `TRANSPOSE`

DEBUG (in a Debug configuration), TRACE and the framework-derived symbols are also defined implicitly, matching the .NET SDK.

Improved

Locals are block-scoped

C# locals are emitted with let so two sibling blocks can each declare the same name, a loop body captures a fresh variable per iteration, and a local no longer collides with a variable declared inside a Script.Write block.

Fixed

Struct and default values are initialized correctly

A non-primitive struct field or expression now defaults to a zeroed struct rather than null, a struct's nested struct fields are zero-initialized, and default(T) in a generic method and a static struct field both produce the zeroed value.

Fixed

Numeric and conversion correctness

Narrow integer arithmetic that promotes to long now produces a real 64-bit value, primitive-type casts were corrected, user-defined conversions declared in a referenced library or in the base library are now applied, and a conditional expression lifts a narrow branch to the expression's own type.

Fixed

Enums with a 64-bit underlying type are no longer supported

Enum members are emitted as plain JavaScript numbers, which hold integers exactly only up to 2^53, so enum E : long { X = long.MaxValue } quietly took a different ordinal and members far apart above that limit could collide onto one value. Declaring one is now a clear error instead; int, uint and narrower underlying types are unaffected, and a referenced assembly can still expose a 64-bit-backed enum.

Fixed

`==` and `!=` against `null` are always a null test

They never resolve to a call to a user-defined operator.

Fixed

String and interpolation output

Doubled braces in an interpolated string are unescaped, alignment specifiers are honoured, a null operand in a concatenation is treated as the empty string, and an interpolated string converted to FormattableString or IFormattable goes through the factory.

Fixed

Pattern matching and argument binding

Bare type patterns, boxed-enum type tests and extended property-pattern paths now resolve correctly, as do a positional argument following a named one, a single element passed to a params parameter by name, params pass-through, an omitted optional argument before a params parameter, and named or optional arguments in a call with ref or out arguments.

Fixed

Closures, iterators and `foreach`

For-loop closure capture, foreach disposal, foreach over null, extension GetEnumerator, iterator local functions, and inline variable scoping inside foreach were all corrected.

Fixed

An expression that wraps an `await` is emitted as an async wrapper

An await inside an out or ref call, an object initializer or a reordered argument list no longer breaks the bundle's syntax.

Fixed

Reflection metadata correctness

Constructor metadata carries the right attribute visibility and slot, attributes are constructed through the overload actually applied, references to delegate and method type parameters resolve, and PropertyInfo.CanRead / CanWrite report correctly.

Fixed

Attribute handling

[Namespace], [Reflectable], [Script] and [Conditional] are implemented; the two-argument [Template] form for non-expanded params is honoured, as is a [Template] on a method group; a [Convention] on the implementing type applies to interface-implementing methods; the enum name-casing modes were fixed; and [Transpose.Ready] static methods are registered to run when the page is ready. Every attribute and its status is catalogued in the attribute reference.

Fixed

`Script.Write` inlining

A call whose argument is dynamic now inlines, and an inline lambda argument is parenthesized so it is safe wherever the template places it.

Fixed

`Exception.StackTrace` resolves for an error raised by raw JavaScript

A native error thrown by library code now carries its stack into the .NET-shaped exception.

Fixed

Array values are tagged with their element type

A runtime type test against an array now reports correctly.

Fixed

`typeof` on an open generic emits the type definition

It no longer emits an application with unbound type parameters.

Fixed

Tuples are real `ValueTuple` instances

A tuple literal used to be a plain object, so ToString() printed [object Object] and Equals and GetHashCode threw. Tuples now print as (a, b) and work as dictionary and set keys. Element access, deconstruction, named elements and == are unchanged.

Fixed

`Activator.CreateInstance` accepts an `object[]` argument array

The evaluation order of named arguments also now matches C#.

Fixed

A const is read through the slot its declaring type emits

Inlining baked the value into every consumer, so updating a base package without rebuilding everything in between left the old value behind. A read now resolves against whichever build of that package is loaded. Consts are emitted as static fields.

Fixed

`[ObjectLiteral(ObjectCreateMode.Constructor)]` runs the constructor

new T(a, b, c) on such a type used to emit an empty object literal, dropping both the constructor and its arguments.

Fixed

Deconstruction into a field, property or indexer

Only locals and parameters were bound correctly; a field, a static field, this.F, obj.F, arr[i] or map[k] on the left of a deconstruction was mis-emitted or dropped outright.

Fixed

`[GlobalMethods]` members keep their names

A member whose name collided with a JavaScript function property (name, length, …) was renamed to an identifier nobody declared; members of a [GlobalMethods] binding are the ambient globals themselves and are now emitted bare.

Runtime and Base Library

Feature

New base-library APIs

Task.Yield, string.ToLowerInvariant and ToUpperInvariant, and the LINQ operators Chunk, MinBy and MaxBy.

Fixed

`Random.Next(min, max)` no longer always returns the minimum

Along with the 64-bit representation bugs behind it.

Fixed

`Guid.NewGuid()` no longer returns a constant

Each call produces a distinct value.

Fixed

`Convert.ToInt64` and `ToUInt64` round

Like .NET's other integral Convert methods they round half-to-even rather than truncating, so Convert.ToInt64(42.7) is 43. A truncating (long) cast is unaffected.

Fixed

Nullable LINQ aggregates skip nulls

Sum over a sequence containing nulls totals the non-null values instead of returning null, and Average, Min and Max return null only when there is no non-null value at all, rather than throwing. A selector is applied first, so items.Sum(x => x.MaybeNull) drops the projected nulls. The non-nullable overloads keep their own contract.

Fixed

Collection and comparison behaviour

Dictionary now dispatches Keys and Values through the interface correctly, IComparer<T>.Compare resolves, and delegates combine and remove through the binary + and - operators.

Fixed

Formatting and struct operators

bool.ToString() casing and nullable-enum ToString() were corrected, as were the runtime operator methods for non-external base-library structs such as DateTimeOffset.

Web and Package Bindings

Fixed

`Transpose.Newtonsoft.Json` polymorphic and array handling

Array type arguments and ISerializationBinder dispatch were corrected, a JSON array now deserializes into an IEnumerable<T>-only target, and an [ObjectLiteral] type is returned raw instead of being deep-deserialized.

Fixed

A `Transpose.*` binding library that carries real implementation now threads its generic type arguments

A generic method in such a library emitted a body that referenced its type parameter without receiving it, failing at runtime. Libraries whose package id starts with Transpose. but which are not the base library are the affected case.

Fixed

Each binding library emits a uniquely named bundle

Transpose.Howler, Transpose.P2 and Transpose.WebGL2 carried a stale copied configuration that pointed at files they do not have, which produced a generic, collision-prone bundle name. They now emit Transpose.Howler.js, Transpose.P2.js and Transpose.WebGL2.js.

Build and Tooling

Feature

Diagnostics reach the build

Errors and warnings are printed in MSBuild's canonical format, so a compile error lands in the IDE error list and is navigable to the file and line. Every error is reported by default, ordered by file and line, with --max-errors to cap the output.

Feature

Shared projects compile

The resolver follows <Import> transitively, so a shared project's compile items are picked up.

Feature

Output-folder pruning

After a successful build, files left over from a previous build that this build did not write are removed, scoped to files the project itself authored. A failed build leaves the previous output intact.

Feature

Build progress and timing

The compiler reports per-phase and per-type progress, and --timing prints a per-phase time and allocation breakdown.

Feature

A metadata-only assembly emit

Because a Transpose assembly is only ever bound against and can never execute, the emitted assembly can carry full metadata with no method bodies. This is the default in Debug and is worth about 18% of a build; Release emits real IL, and the SDK refuses to pack a Debug build so a metadata-only assembly cannot reach a NuGet feed. DebugType is honoured.

Improved

The compiler no longer binds the wrong copy of a referenced assembly

Each reference assembly's resolved path is now logged as well.

Fixed

Resource handling in a site build

A package's resource output subdirectories are preserved on extraction, a tps.json resource that self-references the project's own compiled output is honoured, a file with no minified variant is kept in the minified page, and package CSS and binary resources are no longer fed to the JavaScript minifier.

Fixed

An SDK-driven library build emits a packable assembly

This is what lets the binding libraries publish.

Fixed

The minifier no longer emits invalid JavaScript

For a ?? operand mixed with && or ||.

Performance

Improved

A clean build is roughly 45% faster

The win comes from the compiler's runtime configuration, a cheaper feature scan and fewer redundant binds.

Improved

JavaScript emission costs 2.3x less

After caching the naming layer and rewriting its hot paths, and per-symbol allocations during emit are down.

Improved

Compilation runs in parallel

The emitted assembly is also written once rather than twice.

Improved

The published compiler is ReadyToRun-compiled per platform

That takes about a second off every invocation. dotnet tool install resolves the right build for your machine, and the release pipeline verifies the property cannot be lost silently.

v26.7.2730

Features

Feature

Transpose is the new C# to JavaScript compiler, built on Roslyn

The translator is a clean-room rewrite that binds with Roslyn and emits JavaScript directly from the C# syntax tree, so browser code now gets the same type checking, generics and error messages as the rest of a .NET solution. The legacy translation pipeline is gone.

Feature

Projects build with a plain `dotnet build`

A project references the Transpose.Build.Target MSBuild SDK, which runs the tps compiler once per project. There is no global-tool compiler to install and no compilation server to keep running. The output is a runnable site: the tps.js runtime, the JavaScript bundle, the reflection metadata, your resources and a generated index.html.

Feature

The base library is compiled by Transpose itself

Transpose.BCL is built into the tps.js runtime and a Transpose.dll reference assembly that carries the runtime as an embedded resource, giving a core library with zero assembly references that doubles as the only base-library reference a project needs.

Feature

The rename from h5 is complete

The runtime global in emitted JavaScript is Transpose (language helpers are reached through TransposeR), the runtime bundle and config file are tps.js and tps.json, the compiler command is tps, the codegen attributes live in the Transpose namespace, and the Web bindings in Transpose.Core. Package ids are Transpose.*, with the base library published as Transpose.BCL (its assembly stays Transpose). A step-by-step porting guide is available in the migration guide.

Language and Compiler

Feature

Broad C# language support

Classes, structs, records and record structs; interfaces with both implicit and explicit dispatch; generics with runtime type-argument threading, including new T(); pattern matching, tuples and switch expressions, including C# 11 list patterns and property patterns; iterators; local functions; events and multicast delegates; user-defined operators and conversions; goto and labels, including across an await; block-scoped using declarations; and Index / Range on arrays.

Feature

Recent C# versions are available

C# 12 primary constructors and collection expressions, the C# 14 field keyword, params collections, ref returns, and multidimensional arrays. LangVersion defaults to latest.

Feature

`async` and `await`

An async method is emitted as a native JavaScript async function and its result is returned as a runtime Task, so awaiting a JavaScript promise from C# and handing a task to JavaScript both work. ValueTask is treated like Task.

Feature

LINQ in method and query syntax

Query expressions compile to the same operator chain, including join, group by, let and into continuations.

Feature

.NET numeric semantics

32-bit integer arithmetic wraps as it does on .NET rather than silently turning into a float, lifted nullable operators propagate null, and the conversions to and from the exact 64-bit and decimal types are emitted rather than erased.

Feature

Struct value-copy semantics

Assigning, passing or returning a struct declared in the project being compiled clones the value rather than aliasing it.

Feature

Reflection metadata

Types and members carry metadata that backs System.Reflection at runtime, emitted either to a separate .meta.js file or inline in the bundle.

Feature

The code-generation attributes are honoured

[External] (including at assembly level), [Name], [Template] with {this} / positional / named substitution and out and ref write-back, [Script], [ObjectLiteral], [Enum] in all nine emit modes, [Convention], [GlobalMethods], [GlobalTarget], [ExpandParams] and [IgnoreGeneric], plus the .NET [Flags], [Conditional] and [ModuleInitializer]. See the attribute reference.

Feature

Raw JavaScript interop

Script.Write injects its code verbatim, with positional substitution of the surrounding C# arguments.

Improved

Browser-incompatible constructs are reported as errors

A P/Invoke declaration or an inline array now fails the build with a clear diagnostic instead of emitting code that cannot run in a browser.

Improved

Emitted names follow a stable scheme

Overloads take numbered slots, constructors past the first are named factories, and inherited or explicit [Name] values, object-method overrides and interface member names resolve in a fixed order, so the emitted surface is predictable.

Runtime and Base Library

Feature

The .NET base class library, implemented for the browser

Transpose.BCL provides System.String and the numeric types, the generic collections (List<T>, Dictionary<TKey, TValue>, HashSet<T>, Queue<T>, Stack<T>, …), LINQ, System.Text and composite formatting, regular expressions, Console, Task, Activator and the reflection surface, alongside DateTime, TimeSpan, Guid and Random.

Feature

`long`, `ulong` and `decimal` are exact

They are implemented as runtime types rather than JavaScript numbers, so a 64-bit identifier or a monetary amount survives arithmetic and a round trip without losing precision. Note that they are therefore objects at the interop boundary, not numbers.

Feature

Value equality for structs

== and != on a non-primitive struct such as TimeSpan or DateTimeOffset compare by value, and TimeSpan arithmetic goes through the runtime's own operators.

Feature

`foreach` accepts an extension `GetEnumerator`

Enumerator resolution handles the shapes the base library and user code produce.

Web and Package Bindings

Feature

`Transpose.Core` binds the browser

Strongly typed definitions for the DOM (Transpose.Core.dom) and the ES5/ES6 standard library (Transpose.Core.es5): elements and events, XMLHttpRequest, Canvas, JSON, Promise and the rest. Everything in it is external, so it adds nothing to the bundle beyond the calls you make.

Feature

Binding libraries are supported

A library marked [assembly: External] compiles to a package of pure bindings with no emitted bodies. This is how Transpose.Core above and the JavaScript-library bindings (Transpose.Newtonsoft.Json, Transpose.HttpClient and the rest) are built, and it is available for binding a library of your own.

Build and Tooling

Feature

Library and site builds

--emit-package produces a .NET assembly with the compiled JavaScript and the tps.json resources embedded, for other projects to reference; --separate-assemblies makes a consuming build extract that embedded JavaScript instead of recompiling the library's sources. A referenced project is built first and then reused.

Feature

`tps` command-line options

--reference for an assembly outside the NuGet cache, --define for a preprocessor symbol, --assembly-version, and --build-runtime with outputBy: ClassPath for building the runtime library itself. See the CLI reference.

Feature

`tps.json` drives the build

output, fileName, html and reflection control where output goes, what the generated page contains, and whether reflection metadata is emitted. See Global Configuration.