diff --git a/README.md b/README.md index 900ebc0..fea306d 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,61 @@ # tslua -Go reimplementation of [TypeScriptToLua](https://github.com/TypeScriptToLua/TypeScriptToLua) using [typescript-go](https://github.com/microsoft/typescript-go) internals. +TypeScript-to-Lua transpiler built on [typescript-go](https://github.com/microsoft/typescript-go). Single binary, no Node runtime required. -Work in progress. Passes a large portion of TSTL's test suite but not yet a drop-in replacement. +Built on the architecture and test suite of [TypeScriptToLua](https://github.com/TypeScriptToLua/TypeScriptToLua). For general TypeScript-to-Lua usage and caveats, see the [TSTL docs](https://typescripttolua.github.io/). Targets LuaJIT and Lua 5.0-5.5. -Targets: LuaJIT, Lua 5.0–5.5, universal. +[Docs](https://realcoldfry.github.io/tslua/) · [Playground](https://realcoldfry.github.io/tslua/playground/) · [CLI reference](https://realcoldfry.github.io/tslua/cli/overview/) -tslua ports from TSTL's architecture, transforms, and test suite. +## Install -## What's missing +```bash +npm i @tslua/cli +``` -tslua doesn't yet cover everything TSTL does. Notable gaps: +## Try it -- **Plugins** - TSTL's `luaPlugins` hook system isn't ported. A Go binary can't load JS transformer plugins, so this needs a different approach. In practice, many TSTL plugins handle things like enum remapping, array proxy workarounds, and lualib patching. A native Go pipeline can address these differently since it transpiles lualib through the same path as user code. We're still figuring out what a plugin story looks like here. -- **Build modes** - `--buildMode library` not implemented. -- **Diagnostics** - not all TSTL diagnostics are implemented, and some differ due to using a different type checker. +```ts +// tslua eval -e +const items = [10, 20, 30]; +for (const x of items) { print(x * 2) } -## Background reading +// output: +// items = {10, 20, 30} +// for ____, x in ipairs(items) do +// print(x * 2) +// end +``` + +## Why tslua + +- **Native Go binary.** Uses typescript-go's type checker and AST directly via `go:linkname` shims, no IPC or JS runtime in the loop. +- **TSTL-compatible.** Ports TSTL's transforms and lualib faithfully. Reads the same `tsconfig.json` options and produces compatible output. +- **TS 7 ready.** Built on the compiler that TypeScript is migrating to. +- **Fast.** ~6-18ms incremental rebuilds in watch mode. [Benchmarks](https://realcoldfry.github.io/tslua/performance/) +- **Alternative class styles.** `tstl` (default, TSTL-compatible), `inline`, `luabind`, `middleclass`. + +## Compatibility + +Two verification approaches, both running TSTL's own tests: + +- **[Jest harness](https://realcoldfry.github.io/tslua/testing/overview/#jest-harness).** TSTL's Jest suite runs unmodified, but with tslua's transpiler swapped in via a Unix socket server. **6071 / 6179 tests pass (98.3%).** +- **[Migrated Go tests](https://realcoldfry.github.io/tslua/testing/overview/#migrated-go-tests).** A migration system extracts TSTL's Jest specs and code-generates them into native Go tests. **5656 / 5903 cases migrated (95.8%)** across 70 of 71 spec files, with **100% behavioral pass rate** on migrated cases. The 247 unmigrated cases use TSTL assertion methods (`getMainLuaCodeChunk`, `getLuaExecutionResult`, etc.) not yet supported by the migration script. + +## What's not done yet + +- **Plugins.** TSTL's `luaPlugins` hook system. A Go binary can't load JS transformer plugins; the plugin story needs a different shape. +- **Build modes.** `--buildMode library` not implemented. +- **Diagnostics.** Not all TSTL diagnostics are ported, and some differ due to typescript-go's type checker. + +## Building from source + +Requires Go 1.24+, Node 20+, and [just](https://github.com/casey/just). + +```bash +git clone https://github.com/RealColdFry/tslua +cd tslua +just setup +just build +./tslua eval -e 'print("hello")' +``` -- [Progress on TypeScript 7 (December 2025)](https://devblogs.microsoft.com/typescript/progress-on-typescript-7-december-2025/) — TS7 won't support the existing JS API; third-party tools (including TSTL plugins) face a migration -- [typescript-go: API design direction](https://github.com/microsoft/typescript-go/discussions/455) — IPC-based programmatic API over msgpack, no stable Go API planned -- [typescript-go: Public Go API for embedding?](https://github.com/microsoft/typescript-go/discussions/481) — whether typescript-go will expose stable Go packages for in-process use -- [typescript-go: Transformer plugin / compiler API](https://github.com/microsoft/typescript-go/issues/516) — ecosystem asking what happens to compiler plugins (TSTL, typia, Angular) under TS7 -- [typescript-go: API patterns for editor extensions](https://github.com/microsoft/typescript-go/issues/2824) — concrete IPC API design replacing TS Server plugins (Vue case study) diff --git a/npm/tslua/README.md b/npm/tslua/README.md index 5a03764..9ec3134 100644 --- a/npm/tslua/README.md +++ b/npm/tslua/README.md @@ -1,11 +1,8 @@ # tslua -TypeScript-to-Lua transpiler built on [typescript-go](https://github.com/microsoft/typescript-go). +TypeScript-to-Lua transpiler built on [typescript-go](https://github.com/microsoft/typescript-go). Single binary, no Node runtime required. -**Supported targets:** LuaJIT and Lua 5.0-5.5. - -> [!WARNING] -> This is an early development build. Use at your own risk. +Built on the architecture and test suite of [TypeScriptToLua](https://github.com/TypeScriptToLua/TypeScriptToLua). For general TypeScript-to-Lua usage and caveats, see the [TSTL docs](https://typescripttolua.github.io/). Targets LuaJIT and Lua 5.0-5.5. ## Install @@ -13,16 +10,9 @@ TypeScript-to-Lua transpiler built on [typescript-go](https://github.com/microso npm install @tslua/cli ``` -## Usage - -```bash -# Transpile a project -npx tslua -p tsconfig.json - -# Transpile inline code -npx tslua eval -e 'const x: number = 1; print(x)' -``` - -## Binary downloads +## Links -Pre-built binaries are also available on [GitHub Releases](https://github.com/RealColdFry/tslua/releases). +- [Docs](https://realcoldfry.github.io/tslua/) +- [Playground](https://realcoldfry.github.io/tslua/playground/) +- [CLI reference](https://realcoldfry.github.io/tslua/cli/overview/) +- [GitHub](https://github.com/RealColdFry/tslua) diff --git a/scripts/migrate/README.md b/scripts/migrate/README.md new file mode 100644 index 0000000..704359d --- /dev/null +++ b/scripts/migrate/README.md @@ -0,0 +1,34 @@ +# TSTL Test Migration + +Extracts test cases from TSTL's Jest spec files and code-generates them into native Go tests under `internal/tstltest/`. + +See the [testing docs](https://realcoldfry.github.io/tslua/testing/overview/#migrated-go-tests) for how this fits into tslua's overall test strategy. + +## How it works + +Each TSTL spec file is run in a sandboxed VM with mock `util.testExpression` / `util.testFunction` / `util.testModule` builders. Instead of executing the tests, these builders capture test structure (TypeScript source, options, expected values) and hand it off to the Go code generator. + +## Pipeline + +| File | Role | +| -------------- | ----------------------------------------------------------------------- | +| `cli.ts` | Entry point, spec discovery, cache pre-warming, `-c` check mode | +| `extract.ts` | Sandboxed spec execution, test case capture | +| `builder.ts` | Mock builder that records `.setOptions()`, `.ignoreDiagnostics()`, etc. | +| `evaluate.ts` | JS baking - transpiles TS to JS and runs it to get expected values | +| `tstl-ref.ts` | Runs code through TSTL to capture reference Lua for codegen comparison | +| `codegen.ts` | Emits Go test files with batch test cases | +| `constants.ts` | Overrides, skips, and target mappings | +| `types.ts` | `TestCase` type definition | +| `serialize.ts` | Go literal serialization | +| `migrate.ts` | Orchestrates extract + evaluate + codegen for a single spec | + +## Usage + +```bash +just migrate expressions # migrate a specific TSTL spec +just migrate-all # regenerate all migrated test files + +# check migration coverage without generating files +node --require tsx/cjs scripts/migrate/cli.ts -c -a +``` diff --git a/scripts/migrate/cli.ts b/scripts/migrate/cli.ts index 87bd952..baf6f2e 100644 --- a/scripts/migrate/cli.ts +++ b/scripts/migrate/cli.ts @@ -73,12 +73,12 @@ function runCheck(specPaths: string[]): void { cases = []; extractionErrors = [{ name: "(crash)", error: e.message ?? String(e) }]; } - // Count "other" assertion cases as extraction failures — they can't be migrated + // Count "other" assertion cases as extraction failures, they can't be migrated for (const c of cases) { if (c.assertion === "other") { extractionErrors.push({ name: c.name, - error: `uses .${c.otherReason ?? "unknown"}() — not migratable`, + error: `uses .${c.otherReason ?? "unknown"}(), not migratable`, }); } } @@ -118,8 +118,10 @@ function runCheck(specPaths: string[]): void { // Sort by count descending const sorted = [...byCapability.entries()].toSorted((a, b) => b[1].count - a[1].count); + const totalFound = totalCases + totalErrors; + const pct = totalFound > 0 ? ((totalCases / totalFound) * 100).toFixed(1) : "0"; console.error( - `\n${totalSpecs} specs scanned, ${totalCases} cases extracted, ${totalErrors} extraction failures in ${specsWithErrors} specs\n`, + `\n${totalSpecs} specs scanned, ${totalCases} / ${totalFound} cases migrated (${pct}%), ${totalErrors} extraction failures in ${specsWithErrors} specs\n`, ); if (sorted.length === 0) { diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 796638f..8a00be4 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -42,9 +42,22 @@ export default defineConfig({ }, { label: "Configuration", + items: [{ label: "tsconfig.json", slug: "config/tsconfig" }], + }, + { + label: "Customization", items: [ - { label: "tsconfig.json", slug: "config/tsconfig" }, { label: "Class Styles", slug: "config/class-style" }, + { label: "Emit Mode", slug: "config/emit-mode" }, + { label: "Export as Global", slug: "config/export-as-global" }, + ], + }, + { + label: "Development", + items: [ + { label: "Testing", slug: "testing/overview" }, + { label: "Performance", slug: "performance" }, + { label: "Background", slug: "background" }, ], }, ], diff --git a/website/src/content/docs/background.md b/website/src/content/docs/background.md new file mode 100644 index 0000000..642ca25 --- /dev/null +++ b/website/src/content/docs/background.md @@ -0,0 +1,28 @@ +--- +title: Background +description: Context on TypeScript 7, typescript-go, and the future of TypeScript-to-Lua tooling. +--- + +tslua exists because TypeScript is being rewritten in Go. This page collects the key discussions and decisions shaping that transition, particularly for tools that depend on TypeScript's compiler API. + +## Approaches to TS7 integration + +Tools that depend on TypeScript's compiler API have a few options for the TS7 transition: + +**IPC API consumer.** Wait for typescript-go's planned msgpack-based IPC API, then call it from any language (JS, Go, Rust, etc.) as a subprocess. This is the officially supported path. The API surface is still being designed ([discussion #455](https://github.com/microsoft/typescript-go/discussions/455)), and it's unclear how much of the type checker and AST will be exposed; editor tooling is the priority, not compiler plugins. A transpiler like TSTL needs deep access to types, symbols, and the full AST, which may or may not be available over IPC. + +**Fork typescript-go.** Fork the Go codebase and add transpilation directly. Full control, but a large maintenance surface to keep in sync with upstream. + +**Direct linking via shims.** Link against typescript-go's internals without forking, using `go:linkname` to access unexported APIs. This is what tslua does. + +### How tslua uses typescript-go + +tslua takes the third approach. A [`gen_shims`](https://github.com/RealColdFry/tslua/tree/master/tools/gen_shims) tool (originally from [tsgolint](https://github.com/oxc-project/tsgolint), now maintained in-tree) generates `go:linkname` shims that expose typescript-go's type checker, AST, and program APIs as importable Go packages. tslua gets full access to the same data structures that typescript-go uses internally, at the cost of tracking upstream changes as typescript-go evolves. There is no IPC overhead or subprocess; tslua is a single binary with the type checker compiled in. + +## Reading list + +- [Progress on TypeScript 7 (December 2025)](https://devblogs.microsoft.com/typescript/progress-on-typescript-7-december-2025/). TS7 won't support the existing JS API; third-party tools (including TSTL plugins) face a migration. +- [typescript-go: API design direction](https://github.com/microsoft/typescript-go/discussions/455). IPC-based programmatic API over msgpack, no stable Go API planned. +- [typescript-go: Public Go API for embedding?](https://github.com/microsoft/typescript-go/discussions/481). Whether typescript-go will expose stable Go packages for in-process use. +- [typescript-go: Transformer plugin / compiler API](https://github.com/microsoft/typescript-go/issues/516). Ecosystem asking what happens to compiler plugins (TSTL, typia, Angular) under TS7. +- [typescript-go: API patterns for editor extensions](https://github.com/microsoft/typescript-go/issues/2824). Concrete IPC API design replacing TS Server plugins (Vue case study). diff --git a/website/src/content/docs/config/class-style.md b/website/src/content/docs/config/class-style.md index 1dbdc13..5b571fd 100644 --- a/website/src/content/docs/config/class-style.md +++ b/website/src/content/docs/config/class-style.md @@ -5,6 +5,8 @@ description: Configure how TypeScript classes are emitted to Lua. TypeScript classes can be emitted using different Lua object system conventions. By default, tslua emits TSTL-compatible prototype chains. The `classStyle` option lets you target alternative object systems. +Available styles: [`tstl`](#tstl-default) (default), [`luabind`](#luabind), [`middleclass`](#middleclass), [`inline`](#inline). + ## Quick start Set `classStyle` in your tsconfig.json under the `tstl` key: diff --git a/website/src/content/docs/config/emit-mode.md b/website/src/content/docs/config/emit-mode.md new file mode 100644 index 0000000..dfd28b7 --- /dev/null +++ b/website/src/content/docs/config/emit-mode.md @@ -0,0 +1,32 @@ +--- +title: Emit Mode +description: TSTL-compatible vs optimized Lua output. +--- + +tslua has two emit modes, controlled by the `emitMode` option in tsconfig.json or the `--emitMode` CLI flag. + +```json +{ + "tstl": { + "emitMode": "optimized" + } +} +``` + +## `"tstl"` (default) + +Matches TSTL's Lua output as closely as possible. Use this when you need byte-for-byte compatibility with TSTL, or when comparing output between the two transpilers. + +## `"optimized"` + +Emits cleaner Lua where tslua can prove the result is semantically equivalent. Every optimization preserves identical runtime behavior; the Lua evaluates to the same result as the default mode. + +This mode is a work in progress. Current optimizations: + +| Area | Default (`tstl`) | Optimized | Notes | +| ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- | +| `tostring()` in concat | Wraps all non-string operands including numeric vars | Skips wrapping for numeric types | Lua `..` handles numbers natively | +| C-style for loops | `while` loop with manual init/increment | `for i = start, limit` when pattern matches | Simpler, faster Lua | +| Map/Set for-of | Allocates intermediate tables | Zero-garbage stateless iteration via [custom lualib helpers](https://github.com/RealColdFry/tslua/blob/master/internal/lualib/patches.lua) | Less GC pressure | + +More optimizations will be added over time. If you find a case where optimized mode changes runtime behavior, please [open an issue](https://github.com/RealColdFry/tslua/issues). diff --git a/website/src/content/docs/config/export-as-global.md b/website/src/content/docs/config/export-as-global.md new file mode 100644 index 0000000..e87beb2 --- /dev/null +++ b/website/src/content/docs/config/export-as-global.md @@ -0,0 +1,70 @@ +--- +title: Export as Global +description: Strip module wrappers and emit exports as bare Lua globals. +--- + +By default, tslua wraps each module's exports in a `____exports` table and returns it, matching standard Lua module conventions. The `exportAsGlobal` option strips this wrapper and emits exported declarations as bare globals instead. + +This is useful for embedded Lua environments where scripts run in a global scope rather than as `require()`-able modules. + +## Usage + +Set `exportAsGlobal` in tsconfig.json: + +```json +{ + "tstl": { + "exportAsGlobal": true + } +} +``` + +Or use the CLI flag: + +```bash +tslua -p tsconfig.json --exportAsGlobal +``` + +## Example + +Given this TypeScript: + +```typescript +export const SPEED = 200; +export const GRAVITY = 9.8; +export const PLAYER_NAME = "hero"; +``` + +**Default output** (module wrapper): + +```lua +local ____exports = {} +____exports.SPEED = 200 +____exports.GRAVITY = 9.8 +____exports.PLAYER_NAME = "hero" +return ____exports +``` + +**With `exportAsGlobal: true`**: + +```lua +SPEED = 200 +GRAVITY = 9.8 +PLAYER_NAME = "hero" +``` + +Exports become top-level declarations, accessible to the host environment without a module wrapper. + +## Selective matching + +Instead of a boolean, `exportAsGlobal` accepts a regex string to selectively apply to specific files: + +```json +{ + "tstl": { + "exportAsGlobal": "\\.script\\.ts$" + } +} +``` + +This applies export-as-global only to files matching the pattern (e.g. `game.script.ts`), while other files (e.g. `util.ts`) keep their module wrappers. Useful when some files are entry-point scripts and others are shared modules. diff --git a/website/src/content/docs/guides/installation.md b/website/src/content/docs/guides/installation.md index 62443a6..16e0812 100644 --- a/website/src/content/docs/guides/installation.md +++ b/website/src/content/docs/guides/installation.md @@ -3,35 +3,48 @@ title: Installation description: How to install and use tslua. --- +## npm + +```bash +npm i @tslua/cli +npx tslua eval -e 'print("hello")' +``` + ## From source +Requires Go 1.24+, Node 20+, and [just](https://github.com/casey/just). + ```bash git clone https://github.com/RealColdFry/tslua cd tslua +just setup just build +./tslua eval -e 'print("hello")' ``` ## Usage ```bash # Transpile a project -./tslua -p tsconfig.json +tslua -p tsconfig.json # Transpile inline code -./tslua eval -e 'const x: number = 1 + 2' +tslua eval -e 'const x: number = 1 + 2' # Print the TypeScript AST -./tslua ast -e 'const x = [1, 2, 3]' +tslua ast -e 'const x = [1, 2, 3]' ``` +See the [CLI reference](/tslua/cli/overview/) for all commands and flags. + ## tsconfig.json -tslua reads standard `tsconfig.json` files. TSTL-specific options go under the `"tstl"` key: +tslua reads standard `tsconfig.json` files. tslua-specific options go under the `"tstl"` key: ```json { "compilerOptions": { - "target": "ES2017", + "target": "ESNext", "lib": ["ESNext"], "strict": true }, @@ -40,3 +53,5 @@ tslua reads standard `tsconfig.json` files. TSTL-specific options go under the ` } } ``` + +See [tsconfig.json reference](/tslua/config/tsconfig/) for all options. diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx index da3ae84..e504d0c 100644 --- a/website/src/content/docs/index.mdx +++ b/website/src/content/docs/index.mdx @@ -3,7 +3,7 @@ title: tslua description: Fast TypeScript-to-Lua transpiler powered by typescript-go. template: splash hero: - tagline: A fast TypeScript-to-Lua transpiler, written in Go, powered by typescript-go. + tagline: A fast TypeScript-to-Lua transpiler, written in Go, powered by typescript-go. Built on the architecture and test suite of TypeScriptToLua. actions: - text: Get Started link: ./guides/installation/ @@ -12,3 +12,25 @@ hero: link: ./playground/ variant: minimal --- + +## Try it + +```bash +npm install @tslua/cli +``` + +```ts +// tslua eval -e +const items = [10, 20, 30]; +for (const x of items) { + print(x * 2); +} + +// output: +// items = {10, 20, 30} +// for ____, x in ipairs(items) do +// print(x * 2) +// end +``` + +Targets LuaJIT and Lua 5.0-5.5. Passes [98% of TSTL's test suite](/tslua/testing/overview/#jest-harness). See the [CLI reference](/tslua/cli/overview/) for all commands and flags. diff --git a/website/src/content/docs/performance.md b/website/src/content/docs/performance.md new file mode 100644 index 0000000..3e2815a --- /dev/null +++ b/website/src/content/docs/performance.md @@ -0,0 +1,72 @@ +--- +title: Performance +description: Transpile speed and runtime benchmarks. +--- + +## Transpile speed + +Measured on a game project (~180 source files, LuaJIT target) as of April 2026. Times vary by machine. + +| Scenario | Time | +| -------------------------------- | ------- | +| Initial build | ~550ms | +| Incremental rebuild (watch mode) | ~6-18ms | + +Initial build phase breakdown: + +| Phase | Time | +| ------------ | ----- | +| Parse + bind | 113ms | +| Type check | 203ms | +| Transform | 216ms | +| Print | 10ms | +| Write | 10ms | + +### Watch mode architecture + +In watch mode (`tslua -p tsconfig.json --watch`), incremental rebuilds are fast because: + +- **Incremental program update**: typescript-go's `incremental.NewProgram` diffs the changed file against its snapshot, avoiding a full reparse +- **Async diagnostics**: type-checking runs in a background goroutine after .lua files are written. Diagnostics arrive ~20-50ms later without blocking output +- **Scoped semantic check**: only the changed files are checked for import elision data, not the entire program +- **fsnotify**: file changes are detected via OS notifications, not polling + +### Phase breakdown (incremental clean edit) + +| Phase | Time | +| ------------------- | ----------- | +| Program update | ~1-3ms | +| Transform | ~1-5ms | +| Print | ~0.1-0.5ms | +| Write | ~0.2-0.8ms | +| **Build done** | **~6-18ms** | +| Diagnostics (async) | +20-50ms | + +## Runtime benchmarks + +These benchmarks compare `tstl` (default) vs `optimized` [emit mode](/tslua/config/emit-mode/) as of April 2026. Optimized emit mode is early and only covers a few patterns so far (iterator allocation, tostring elision). The numbers below reflect what's implemented today. + +```bash +just bench # run with LuaJIT (default) +just bench-lua # show transpiled Lua output +``` + +### LuaJIT + +| Benchmark | Time (tstl) | Time (opt) | Garbage (tstl) | Garbage (opt) | +| ------------- | ----------- | -------------- | -------------- | ------------- | +| array_entries | 0.238ms | 0.028ms (8.5x) | 988 KB | 128 KB (-87%) | +| map_iterate | 0.080ms | 0.016ms (5.0x) | 355 KB | 96 KB (-73%) | +| set_iterate | 0.014ms | 0.011ms (1.3x) | 65 KB | 64 KB | + +### Lua 5.1 + +| Benchmark | Time (tstl) | Time (opt) | Garbage (tstl) | Garbage (opt) | +| ------------- | ----------- | -------------- | -------------- | ------------- | +| array_entries | 2.960ms | 0.373ms (7.9x) | 2600 KB | 256 KB (-90%) | +| map_iterate | 0.974ms | 0.463ms (2.1x) | 897 KB | 193 KB (-78%) | +| set_iterate | 0.807ms | 0.389ms (2.1x) | 551 KB | 129 KB (-77%) | + +Other benchmarks (array_iterate, array_push, string_iterate, string_concat) show no significant difference between modes. Run the full suite with `just bench`. + +The wins come from iterator optimizations in `optimized` emit mode. Map/Set for-of loops use custom stateless Lua iterators that walk the internal linked list directly, avoiding per-step closure and table allocations. See [Emit Mode](/tslua/config/emit-mode/) for details. diff --git a/website/src/content/docs/testing/overview.md b/website/src/content/docs/testing/overview.md new file mode 100644 index 0000000..5d6f00f --- /dev/null +++ b/website/src/content/docs/testing/overview.md @@ -0,0 +1,150 @@ +--- +title: Testing +description: How tslua verifies compatibility with TypeScriptToLua. +--- + +tslua uses three complementary testing approaches to verify compatibility with TSTL: + +1. **[Jest harness](#jest-harness)** runs TSTL's own test suite with tslua's transpiler swapped in via a socket server. +2. **[Migrated Go tests](#migrated-go-tests)** extract TSTL's Jest specs and code-generate them into native Go tests. +3. **[Hand-written tests](#hand-written-tests)** cover tslua-specific behavior and cases too complex to migrate. + +## Jest harness + +TSTL's own Jest test suite runs unmodified, but with tslua's transpiler swapped in. This is the most authoritative compatibility check since it uses TSTL's own test infrastructure and assertions. + +### How it works + +A patch (`extern/tstl-test-util.patch`) modifies TSTL's `test/util.ts` to check for a `TSTL_GO=1` environment variable. When set, the test builder sends transpilation requests to a tslua server instead of calling TSTL's transpiler. TSTL's existing Lua WASM runtime then evaluates the output and checks assertions as normal. + +### Server and client + +`tslua server --socket PATH` starts a long-lived process listening on a Unix socket. Jest workers send requests through `tslua-client`, a minimal Go binary that connects to the socket, pipes a JSON request from stdin, and writes the response to stdout. + +**Request** (JSON): + +```json +{ + "source": "const x: number = 1; print(x);", + "luaTarget": "5.4", + "mainFileName": "main.ts", + "extraFiles": { "helper.ts": "export const y = 2;" }, + "compilerOptions": { "strict": true, "target": "ESNext" } +} +``` + +**Response** (JSON): + +```json +{ + "ok": true, + "files": { "main.lua": "x = 1\nprint(x)" }, + "diagnostics": [] +} +``` + +The server processes requests sequentially through a single worker goroutine with panic recovery. This avoids concurrency issues with the typescript-go type checker while keeping the server alive across requests. Each request has a 10-second timeout. + +```bash +just tstl-test # run full suite +``` + +```bash +just tstl-test expressions # filter by spec name +``` + +Current result: **6071 / 6179 tests pass (98.3%).** The 103 failures break down as: + +| Category | Count | Notes | +| ---------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------- | +| Codegen snapshot diffs | ~23 | Semantically equivalent output, different formatting (e.g. hex vs decimal literals, temp variable elision) | +| Plugins/transformers | ~14 | `luaPlugins` not implemented | +| Bundling/build modes | ~11 | `--buildMode library`, `luaLibImport inline`, bundling edge cases | +| Module resolution | ~8 | `noResolvePaths`, `baseUrl` resolution differences | +| Emit paths | ~4 | `getEmitPath` for outDir/rootDir/extensions | +| Diagnostics | ~2 | Snapshot diffs in diagnostic output | +| Other | ~41 | Mix of Luau-specific, language extension edge cases, async/generators on universal target, declaration file generation | + +Most failures are in unimplemented features (plugins, build modes) or codegen snapshot comparisons where the output is semantically equivalent but not byte-identical. + +## Migrated Go tests + +A [migration system](https://github.com/RealColdFry/tslua/tree/master/scripts/migrate) extracts TSTL's Jest specs and code-generates them into native Go tests under `internal/tstltest/`. This gives us fast, reproducible test runs without needing Node or TSTL installed. + +The migration script (`scripts/migrate/cli.ts`) works by running each TSTL spec file in a sandboxed VM with mock `util.testExpression` / `util.testFunction` / `util.testModule` builders. Instead of executing the tests, these builders capture the test structure (TypeScript source, options, expected values) and emit Go test files. + +For each TSTL test case, two Go tests are generated: + +- **`TestEval_*`** - transpiles the TypeScript with tslua, runs the Lua output, and checks it against the expected value (either baked from JS evaluation or specified inline). These are the behavioral correctness tests. +- **`TestCodegen_*`** - transpiles with both tslua and TSTL, then diffs the Lua output. These catch formatting and structural divergences but are not required to pass (`just testall` skips them). + +```bash +just migrate expressions # migrate a specific TSTL spec +just migrate-all # regenerate all migrated test files +node --require tsx/cjs scripts/migrate/cli.ts -c -a # check migration coverage +``` + +Current result: **5656 / 5903 cases migrated (95.8%)** across 70 of 71 spec files, with **100% behavioral pass rate** on migrated cases. + +The 1 unmigrated spec file (`find-lua-requires`) tests a TSTL-internal Lua source parser that scans emitted Lua for `require()` calls (used by TSTL's bundler). tslua doesn't need this: it tracks dependencies at the TypeScript AST level during transpilation and uses TypeScript's own module resolution, so there's no post-emission Lua scanning. The spec also uses plain Jest assertions with no `testExpression`/`testFunction`/`testModule` builders, so the migration system has nothing to capture. + +The 247 unmigrated cases within migrated specs use TSTL assertion methods (`getMainLuaCodeChunk`, `getLuaExecutionResult`, etc.) that the migration script doesn't yet support. These are captured and reported by the `-c` (check) flag. + +### Overrides + +Not every TSTL test can be migrated as-is. `scripts/migrate/constants.ts` defines several override categories for cases that need special handling: + +- **`tstlBugOverrides`** - TSTL's expected value is wrong (verified against JS runtime). The migration uses the corrected value instead. +- **`tstlBugSkips`** - skip the test entirely (e.g. runtime differences like `error(nil)` behaving differently on native Lua vs WASM). +- **`tstlBugCodegenSkips`** - skip codegen comparison only. Used when tslua's output is correct but intentionally differs from TSTL (e.g. hex literals for bitmasks, better for-loop continue handling). +- **`batchDiagnosticOverrides`** - suppress diagnostic checks. The Go test harness compiles multiple test cases in a single `Program` for performance, which can cause `declare global` conflicts that don't exist in TSTL's per-test compilation. +- **`bakeLimitationOverrides`** - the expected value can't be computed by JS baking alone (e.g. Lua-specific error message formatting). + +## Hand-written tests + +`internal/luatest/` contains hand-written integration tests for cases where: + +- The TSTL test uses an assertion pattern too complex to migrate automatically +- The Jest harness wiring is too fiddly for a specific scenario +- tslua has behavior that TSTL doesn't test (e.g. tslua-specific features like alternative class styles) + +These tests follow the same pattern as migrated tests (transpile TypeScript, run Lua, check output) but are written directly in Go. + +```bash +just test # runs transpiler + lua + luatest +just testall # runs all three packages including tstltest +``` + +## Lua runtimes + +Both migrated and hand-written eval tests run the transpiled Lua against real Lua interpreters. `just lua-setup` builds all supported runtimes from source into `.lua-runtimes/bin/`: + +| Binary | Version | Source | +| -------- | ------- | ------------------------------------- | +| `lua5.0` | 5.0.3 | lua.org tarball | +| `lua5.1` | 5.1.5 | lua.org tarball | +| `lua5.2` | 5.2.4 | lua.org tarball | +| `lua5.3` | 5.3.6 | lua.org tarball | +| `lua5.4` | 5.4.7 | lua.org tarball | +| `lua5.5` | 5.5.0 | lua.org tarball | +| `luajit` | 2.1 | GitHub mirror, pinned commit | +| `lune` | 0.10.4 | Pre-built binary from GitHub releases | + +Each `TestEval_*` case specifies a Lua target. The test harness selects the matching runtime and runs the transpiled output against it. Target-specific tests (e.g. bitwise operators on 5.3+, native continue on Luau) only run when the corresponding binary is available. + +Lune provides Luau support. tslua emits Luau-specific constructs (e.g. native `continue`) when `luaTarget` is set to `Luau`, and these are verified against Lune. + +```bash +just lua-setup # build all runtimes (cached, ~30s first time) +``` + +## Test gate + +`just testall` is the gate. All three test packages must pass: + +``` +$ just testall + internal/lua + internal/transpiler + internal/tstltest +```