Skip to content

JIT cache: each .ts file is compiled into an object kept in __jit - #405

Merged
ASDAlexander77 merged 2 commits into
mainfrom
claude/vigilant-wozniak-6dh4ue
Sep 29, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
claude/vigilant-wozniak-6dh4ue

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Follow-up to #404. Instead of including every imported .ts module into the program's own module on each run, --emit=jit now compiles the program and each .ts module it imports into an object of its own. The objects are cached and reused on later runs.

What changes

  • Per-module objects. Under --emit=jit the program and every .ts module it imports, directly or through another module, are each compiled into their own object. The objects are stored in a __jit folder beside the source.
  • Manifest. Each object is stored with a manifest of what it was compiled from:
    • the sources read: the file itself, its references, its imports and lib.d.ts;
    • the imported shared libraries;
    • the tslang build.
  • Cache reuse. While all of that is unchanged, the next run loads the objects into the LLJIT instead of compiling again. An edited module is recompiled together with its importers. Options are part of the object's name, so runs with other options keep objects of their own. Writes are atomic (temp file + rename), so concurrent runs can share a cache.
  • Import of a .ts file under the JIT. It is a declaration again, as it is when compiling; the module's own object provides the bodies. MLIRGen records the .ts files imported (dependencies first) and the libraries imported.
  • Initialization order. Each object's llvm.global_ctors are moved into one init function, and these are called in dependency order before the program starts, because the JIT runs no object's global_ctors. On ELF, COMDAT definitions (for example a class's .size, which is defined by the module and by its importers) are made weak, so ORC keeps one.
  • Import cycles. A program in an import cycle is compiled as one module with the imports' bodies included, and cached as one object. A module in a cycle can't be compiled on its own; the same holds for --emit=obj.
  • Options. --jit-cache=false keeps the previous behaviour (one module, compiled every run). --jit-cache-dir=<folder> puts all objects in one folder. The cache is not used for stdin input or --dump-object-file.
  • runJit refactor. It is split into reusable parts (process setup, library loading, JIT creation, program run), which are shared with the cached path.
  • Win64. The _CxxThrowException shim now finds the image base of the object that holds the ThrowInfo, since there can be more than one JIT'd object.

Tests

  • jit-cache/: diamond imports with initialization order, and an import cycle.
  • jit-cache.cmake: the cache is reused without being rewritten, and is recompiled after an imported module changes.

Testing

Linux, Release build with the prebuilt LLVM 22.1.8: the full ctest suite passes (3190 passed, 0 failed). Not built or tested on Windows, so the Win64 _CxxThrowException change is untested.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VPAHhg8c9NEdoYNw52AtTg


Generated by Claude Code

Under --emit=jit the program and every .ts module it imports (directly or
through another) are each compiled into an object of their own, stored in a
`__jit` folder beside the source with a manifest of what it was compiled from
(the sources read - itself, references, imports, lib.d.ts - the imported
shared libraries, and the tslang build). The next run loads the objects
into the LLJIT instead of compiling again while all of that is unchanged;
an edited module is recompiled together with its importers. Options are part
of the object's name, so runs with other options keep objects of their own.

- An import pointing to a .ts file is a declaration again under the JIT, as
  it is compiled; the module's object provides the bodies. MLIRGen records
  the .ts files imported (dependencies first) and the libraries imported.
- Each object's llvm.global_ctors are moved into one init function, called in
  dependency order before the program starts (the JIT runs no object's
  global_ctors). On ELF, COMDAT definitions (a class's `.size`, defined by the
  module and its importers) are made weak so ORC keeps one.
- A program in an import cycle is compiled as one module with the imports'
  bodies included (a module in a cycle can't be compiled on its own, the
  same holds for --emit=obj) and cached as one object.
- Writes are atomic (temp file + rename), so concurrent runs share a cache.
- --jit-cache=false keeps the previous behaviour (one module, every run);
  --jit-cache-dir=<folder> puts all objects in one folder. Not used for
  stdin input or --dump-object-file.
- runJit is split into reusable parts (process setup, library loading, JIT
  creation, program run) shared with the cached path; on Win64 the
  _CxxThrowException shim now finds the image base of the object the
  ThrowInfo is in, since there can be more than one JIT'd object.

Tests: jit-cache/ (diamond imports with initialization order, an import
cycle) and jit-cache.cmake (cache reused without rewriting, recompiled
after an imported module changes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPAHhg8c9NEdoYNw52AtTg
@ASDAlexander77
ASDAlexander77 merged commit f154f5c into main Sep 29, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the claude/vigilant-wozniak-6dh4ue branch September 29, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants