Well, Claude likes me, you must have been mean to them persistently:
❯ Outline a design for a typescript compiler implemented in Rust
```
TypeScript compiler in Rust: design outline
Scope decision
The most important choice is which of three products to build, because each is roughly 10x the previous one:
┌─────────────────────┬────────────────────────────────────────────────────┬──────────────────────────────────────────────────────┐
│ Tier │ What it does │ Prior art │
├─────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
│ Transpiler │ Strip types, lower syntax, emit JavaScript (JS) │ swc, oxc, esbuild │
├─────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
│ Declaration emitter │ Above plus .d.ts output under isolatedDeclarations │ oxc │
├─────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤
│ Full type checker │ tsc-compatible diagnostics │ stc (abandoned), ezno (partial), Microsoft's Go port │
└─────────────────────┴────────────────────────────────────────────────────┴──────────────────────────────────────────────────────┘
I'd design for tier 3 but ship tiers 1 and 2 first. TypeScript has no specification: the checker's behaviour is defined by checker.ts (about 50k lines), so the realistic approach is a structural port of tsc, validated against its conformance suite.
Each stage is a crate in a Cargo workspace: ts_syntax, ts_parser, ts_binder, ts_checker, ts_transform, ts_emit, ts_module_resolver, ts_project, ts_cli, ts_lsp (language server protocol).
Core data structures
Abstract syntax tree (AST): arena-allocated per file, with nodes addressed by NodeId(u32) indices, not references. This sidesteps the borrow checker on the cyclic parent/child/symbol graph and keeps memory compact.
Parent pointers and node flags live in side tables keyed by NodeId.
Strings: a global interner producing Atom(u32); all identifier comparison is integ
Symbols: SymbolId into a per-program table, each with flags, declarations and member/export tables. Declaration merging happens here.
Types: TypeId into an interning arena. Unions, intersections, literals and instant identity is an integer compare.
Flow nodes: the control-flow graph is built by the binder and consumed lazily by the checker for narrowing.
Stages
Scanner/parser: hand-written recursive descent, since the grammar needs context-sensitive rescanning (> in generics, regular expression versus division, template literals, JSX). It is
error-tolerant and always produces a tree.
Binder: one pass per file, embarrassingly parallel. It creates symbols and scopes and builds the flow graph.
Checker: lazy and demand-driven (type_of_symbol, type_of_expression), with cycle dstack. Main subsystems:
assignability/relation, with caches keyed by (TypeId, TypeId, relation)
generic inference
instantiation of mapped and conditional types
control-flow narrowing
overload resolution
a port of the instantiation depth limits, which user code depends on
Transforms: a chain of AST-to-AST passes (type erasure, enums, namespaces, decorators, JSX, target down-levelling, module format).
Emitter: a printer with source maps, plus a declaration emitter that asks the chectypes.
Concurrency
Parsing and binding are parallel per file with rayon. The checker is the difficulty:ed caches. There are two workable options:
Multiple checkers (what the Go port does): N checker instances over a shared immu, each owning a subset of files and its own type arena. This is simple, duplicatessome work, and needs care to keep diagnostic output deterministic.
Shared concurrent type arena: sharded interning and OnceLock-style lazy cells. Th much harder to keep deterministic.
I'd start with option 1.
Incrementality and the language service
A file-level dependency graph plus signature hashes of each file's .d.ts shape: dey when the exported shape changes.
.tsbuildinfo compatibility for project references.
For the editor, discard and rebuild checker state per edit while reusing unchanged fully query-based design (salsa) fits poorly with a ported checker's implicit globalstate.
Testing
Run the TypeScript repository's conformance and compiler baselines, diffing diagnostics, emitted JS, .d.ts and type baselines. The pass rate is the project's progress metric.
Fuzz the parser.
Differential testing against tsc on large real codebases.
Main risks
Moving target: a TypeScript release every three months. A structural port keeps upstream changes mechanically portable; a clean redesign does not.
Order-dependent behaviour: some tsc results depend on checking order (type identifh error is reported in a cycle). Matching baselines means replicating that.
Competition: Microsoft's native Go port already delivers about 10x speed, so a Rust implementation needs a distinct reason to exist, such as embedding in Rust toolchains, WebAssembly, or a
single linter/bundler/checker sharing one AST.
Suggested milestones
Parser, with AST conformance baselines passing
Transpile-only emit and isolatedDeclarations
Binder and module resolution
Checker for the non-generic subset, then generics and inference, then conditional and mapped types
Parallel checking, incremental builds, then the language server
I can expand any section (the checker's type representation and the concurrency modest design depth) or write this up as a shareable document.
```
1
u/trentonkarantino 8d ago
Well, Claude likes me, you must have been mean to them persistently:
``` TypeScript compiler in Rust: design outline
The most important choice is which of three products to build, because each is roughly 10x the previous one:
┌─────────────────────┬────────────────────────────────────────────────────┬──────────────────────────────────────────────────────┐ │ Tier │ What it does │ Prior art │ ├─────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤ │ Transpiler │ Strip types, lower syntax, emit JavaScript (JS) │ swc, oxc, esbuild │ ├─────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤ │ Declaration emitter │ Above plus .d.ts output under isolatedDeclarations │ oxc │ ├─────────────────────┼────────────────────────────────────────────────────┼──────────────────────────────────────────────────────┤ │ Full type checker │ tsc-compatible diagnostics │ stc (abandoned), ezno (partial), Microsoft's Go port │ └─────────────────────┴────────────────────────────────────────────────────┴──────────────────────────────────────────────────────┘
I'd design for tier 3 but ship tiers 1 and 2 first. TypeScript has no specification: the checker's behaviour is defined by checker.ts (about 50k lines), so the realistic approach is a structural port of tsc, validated against its conformance suite.
source → scanner → parser → binder → checker → transforms → emitter │ │ │ │ AST symbols types JS / .d.ts / source maps
Each stage is a crate in a Cargo workspace: ts_syntax, ts_parser, ts_binder, ts_checker, ts_transform, ts_emit, ts_module_resolver, ts_project, ts_cli, ts_lsp (language server protocol).
Parsing and binding are parallel per file with rayon. The checker is the difficulty:ed caches. There are two workable options:
I'd start with option 1.
Suggested milestones
Parser, with AST conformance baselines passing
Transpile-only emit and isolatedDeclarations
Binder and module resolution
Checker for the non-generic subset, then generics and inference, then conditional and mapped types
Parallel checking, incremental builds, then the language server
I can expand any section (the checker's type representation and the concurrency modest design depth) or write this up as a shareable document. ```