Repository navigation
Borrow facts: stack promotion across statically linked modules - #609
Merged
Merged
Conversation
Libraries publish which parameters of their exported functions are only borrowed, generated by the stack promotion escape analysis and stored in the binary; programs read them to promote objects passed to those calls. Three questions are still open (default-library gain, DLL links, Linux archives). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tatic libraries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every module compiled ahead of time gets an exported char array, __tsborrow_<file>_<hash>, listing for each non-private function the parameters its body only borrows. The facts come from the stack promotion escape analysis, run trusting no vtable, since an importer may subclass any class. They are written whatever --export and --no-stack-promotion say. The pass records the text on the module; the LLVM lowering makes it the symbol after conversion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dump::readCStringSymbols reads every symbol with a prefix, and the char array it names, from a COFF or ELF object file or from the object members of a static library; import members and unreadable files give nothing. The hidden --print-borrow-facts=<file> prints them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An --emit=exe compile reads the __tsborrow_ facts of the default library's static library and of every --lib it can find (a file, or the target's <name>.lib / lib<name>.a in the current directory), and links such a library by that path, so the file read is the file linked. Stack promotion then treats an argument a declared function's library lists as borrowed. DLL links, -shared imports and --emit=obj read nothing; missing facts mean every call escapes, as before. The default library's file for a link or the JIT now comes from one function, getDefaultLibBinary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under --emit=jit the compiler reads the __tsborrow_ facts of the default library DLL it loads, from the file (PE exports, or an ELF shared object's dynamic symbols) before anything is loaded. A JIT cache entry lists that file as a dependency, so a rebuilt library misses the cache. test-borrow-facts-defaultlib checks Math.max's rest array goes on the stack under --emit=exe and --emit=jit for every model whose default library publishes facts, and skips otherwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…takes none It defines, or binds, another symbol than its name, so a fact keyed by the name could describe a different function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A fact is matched by name, and the linker may take another definition of that name. A fact is now dropped when another library or --obj of the link defines its symbol, when a -shared import exports it, or when the JIT cache compiles the module apart from modules it imports. A --lib the linker searches for itself may define anything, so then no fact is trusted. Dump::definedSymbols lists what an input defines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ame from --emit=exe linked the default library with -l and -L, so the linker took the first library of that name it found - a stray copy in the current folder before the configured one - while the facts were read from the configured file. The static library is now linked by its path; the DLL's import library is still searched for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only a static library or an object file is linked into the program, so only its facts describe the code that runs. Any other --lib counts as one the linker resolves itself, and then no fact is trusted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77
added a commit
that referenced
this pull request
Oct 20, 2026
#609 links the static default library by its full path and dropped its -L. On Linux libTypeScriptDefaultLib.a is a GNU ld script, INPUT(-lTypeScriptDefaultLibCore -lcurl), whose archive the linker looks up through the -L paths: every --emit=exe with the default library failed with "cannot find -lTypeScriptDefaultLibCore" (v0.0-pre-alpha105 release, Linux). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77
added a commit
that referenced
this pull request
Oct 20, 2026
) #609 links the static default library by its full path and dropped its -L. On Linux libTypeScriptDefaultLib.a is a GNU ld script, INPUT(-lTypeScriptDefaultLibCore -lcurl), whose archive the linker looks up through the -L paths: every --emit=exe with the default library failed with "cannot find -lTypeScriptDefaultLibCore" (v0.0-pre-alpha105 release, Linux). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack promotion so far treated every call into another module as an escape. With this change, a library records which parameters of its functions are only read, and a program linked statically against it reads those facts. An object passed to such a parameter can then go on the stack. For example,
Math.max(a, b, c)now builds its argument array on the stack, under both--emit=exeand--emit=jit.Spec:
tslang/docs/superpowers/specs/2026-10-18-borrow-facts-across-modules-design.md. Plan:tslang/docs/superpowers/plans/2026-10-18-borrow-facts-across-modules.md.Producing the facts
__tsborrow_<file>_<hash>. It is a char array with one line per function:<symbol> <borrowed parameter indexes>.--exportand--no-stack-promotionsay.@linknameor@dllnamepublishes no facts.Reading the facts
Facts are read only where the file read is the file that runs:
--emit=exe: the default library's static library (now linked by its path rather than searched for), and every--libthat resolves to a static library or object file (also linked by that path).--emit=jit: the default library's DLL, read from the file before anything is loaded. A JIT cache entry lists that DLL as a dependency.-sharedimports,--emit=obj. Missing or old facts mean every call escapes, as before.Matching
--lib, an--obj, a-sharedimport, or a module the JIT cache compiled separately. If any--libis searched for by the linker, no fact is trusted at all.@linknameor@dllnametakes no fact.Also
tslang --print-borrow-facts=<file>prints what an object file, static library or DLL publishes.docs/memory-models.mdhas a new "Calls into other modules" section.Testing
test-stack-facts-lib-*covers gc, rc, own and none.test-borrow-facts-*builds a static library and a program with--lib, checks every decision, and compares the output with an unpromoted build. Negative cases:--emit=obj@linkname--objdefining the same symbol--lib@static-sharedimport--libnaming a DLLtest-borrow-facts-defaultlibcovers the real default library. It is skipped unless that library was built with this compiler. It passed against a locally rebuilt rc default library, in both exe and JIT modes, and also checks that a strayTypeScriptDefaultLib.libin the working folder is not linked.-sharedfilter, the-sharedtest fails.-DTSLANG_TEST_NO_STACK_PROMOTION=ON.Not covered
.sopaths are written but untested; this was built and tested on Windows only.🤖 Generated with Claude Code