Compiler milestone

How the VexaScript compiler self-hosts in native C++

Commit 58703ec2 completed the first native self-hosting run on July 19, 2026. A VexaScript compiler generated as C++ loaded the real cli/cli.ts project and generated the next C++ compiler.

What the test runs

The native executable does not call Node.js to compile the project. It loads the TypeScript files, runs the compiler, builds the module graph, and writes a new C++ translation unit.

First run Value
Commit 58703ec2
Generation time without optimization 171 seconds
Generated C++ file about 6.6 MB
Change size 29 files, 1,401 additions, 596 deletions
Entrypoint cli/cli.ts

This test covers much more code than a native sample. It combines recursive types, declarations, modules, asynchronous files, collections, and a large amount of generated output.

Why the first run appeared to hang

The tokenizer calls charCodeAt for each character. C++ selected an old std::string overload, which converted the full UTF-16 source to UTF-8 and back for every character.

One character lookup should take constant time. Instead, each lookup processed the whole file, so tokenizing a file of N characters took about N squared work. A direct UTF-16 overload fixed the problem.

This bug shows why a runtime type change must update all related helper functions. Leaving one old overload can produce valid but very slow C++.

Asynchronous TypeScript checks

The CLI uses tsc --noEmit to validate TypeScript projects. Waiting for that process on the main runtime thread would block timers and Promises. The native adapter runs the process on a worker and posts the result back to the event loop.

The worker receives copied command data. It does not keep raw pointers to Oilpan-managed objects. The same rule is used for blocking FFI calls.

Errors found in later compiler generations

The first generated compiler could run even when it emitted incorrect types for the next compiler.

Problem Fix
map callbacks were emitted as returning void Keep the declared callback result type
Missing values entered AnalysisType[] Normalize them at unionType
Recursive generic maps lost their element type Add explicit AnalysisType annotations
Missing values became invalid WeakMap keys Reject missing operands before cache access
Optional collection receivers lost optional behavior Use the shared optional-call code

Adding a null check at each crash site was not enough. The compiler now fixes the producer of the bad value and also checks important type-system entry points.

What self-hosting proves

For .ts and .tsx files, TypeScript remains responsible for the full semantic check. VexaScript then reuses its parsed and analyzed data to generate C++. .vx files use the VexaScript checker.

Later commits made repeated native compiler output byte-for-byte stable. The test is useful because a wrong type or unsafe pointer may survive in a normal program but fail when the generated program must compile itself again.