Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/c-cpp-header-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ C is a 90%-mechanical translation; C++ is not. Realistic phasing:
| Phase | Scope | Approach | Effort |
| --- | --- | --- | --- |
| C++ 0 | `extern "C"` blocks in C++ headers | Falls out of Option A for free (clang parses the header as C++, importer takes only C-linkage decls) | included in A |
| C++ 1 | Free functions, non-template classes with methods (no virtuals crossing the boundary), overloads | Bind by **mangled name**: clang provides Itanium/MSVC mangled symbols (`clang::MangleContext`); emit `declare` per overload with the mangled name attached (needs a small compiler addition: a `@linkname("?foo@@YAHH@Z")` attribute on declarations so the emitted `FuncOp` symbol differs from the TS-visible name). Methods become functions taking `this: Reference<S>` first. Constructors/destructors bound explicitly; caller manages object memory (`GC_malloc_atomic` for PODs / explicit new-delete pair bindings). | 2–4 weeks after A |
| C++ 1 | Free functions, non-template classes with methods (no virtuals crossing the boundary), overloads | Bind by **mangled name**: clang provides Itanium/MSVC mangled symbols (`clang::MangleContext`); emit `declare` per overload with the mangled name attached (`@linkname("?foo@@YAHH@Z")`, an alias of `@dllname`, makes the emitted `FuncOp` symbol differ from the TS-visible name). Methods become functions taking `this: Reference<S>` first. Constructors/destructors bound explicitly; caller manages object memory (`GC_malloc_atomic` for PODs / explicit new-delete pair bindings). | 2–4 weeks after A |
| C++ 2 | Templates, virtual dispatch, exceptions, STL types | **Do not bind directly.** Generate a flattened `extern "C"` wrapper `.cpp` (tsbindgen emits both the wrapper and its `.d.ts`), compiled with the system C++ compiler and linked via existing `-obj=`. Explicit template instantiations only; exceptions caught at the wrapper boundary and converted to error codes (C++ exceptions must never unwind into tslang frames — different personality functions, especially in JIT where Win64 unwind info is custom, see `docs/jit-gc-static-roots.md` context). | 3–6 weeks, per-need |

The C++ ABI reality (mangling differences, MSVC vs Itanium, RTTI, std::string
Expand Down
817 changes: 817 additions & 0 deletions docs/superpowers/plans/2026-09-25-linkname.md

Large diffs are not rendered by default.

3,701 changes: 3,701 additions & 0 deletions docs/superpowers/plans/2026-09-26-tsbindgen.md

Large diffs are not rendered by default.

328 changes: 328 additions & 0 deletions docs/superpowers/specs/2026-09-25-tsbindgen-linkname-design.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions tslang/include/TypeScript/Defines.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
#define DLL_EXPORT "dllexport"
#define DLL_IMPORT "dllimport"
#define DLL_NAME "dllname"
#define LINK_NAME "linkname"

#if __LP64__
#define TRAMPOLINE_SIZE 48
Expand Down
14 changes: 4 additions & 10 deletions tslang/include/TypeScript/VSCodeTemplate/Files.h
Original file line number Diff line number Diff line change
Expand Up @@ -291,15 +291,12 @@ const auto LAUNCH_JSON_DATA_LINUX = R"raw(

#define CMAKE_FOLDER_PATH "cmake"

const auto CMAKE_LISTS_TXT_DATA = R"raw(cmake_minimum_required(VERSION 3.20)
const auto CMAKE_LISTS_TXT_DATA = R"raw(cmake_minimum_required(VERSION 3.21)

# Make CMake find ts-language modules
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

project(<<PROJECT>> CXX)

# Enable TS-language
enable_language(TSLANG)
project(<<PROJECT>> CXX TSLANG)

# Include folders
include_directories(${CMAKE_TSLANG_DIR}/defaultlib)
Expand Down Expand Up @@ -331,10 +328,7 @@ endif()
# program is compiled for one model and linked against another model's default lib.
set(CMAKE_TSLANG_FLAGS "${CMAKE_TSLANG_FLAGS} -mm=${TSLANG_MEMORY_MODEL}") # global

if(WIN32)
else()
set(CMAKE_TSLANG_FLAGS "${CMAKE_TSLANG_FLAGS} -relocation-model=pic") # global
endif()
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# .ts files compile with TSLANG command; .cpp with the C++ compiler.
add_executable(${PROJECT_NAME}
Expand Down Expand Up @@ -363,7 +357,7 @@ const auto CMAKE_PRESETS_JSON_DATA = R"raw({
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 20,
"minor": 21,
"patch": 0
},
"configurePresets": [
Expand Down
29 changes: 24 additions & 5 deletions tslang/lib/TypeScript/LowerToAffineLoops.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2707,14 +2707,33 @@ void TypeScriptToAffineLoweringTSFuncPass::runOnFunction()
LLVM_DEBUG(llvm::dbgs() << "\n!! BEFORE FUNC DUMP: \n" << function << "\n";);

// We only lower the main function as we expect that all other functions have been inlined.
// The JIT calls `main` through a thunk with the C entry point's signature (see jit.cpp), which
// can only adapt the shapes a C `main` has: `argc` as an integer or a number, `argv` as a
// `string[]` or as the C `char **` itself, a `Ref<string>`, and an exit code that is an integer
// or a number.
if (tsContext.compileOptions.isJit && function.getName() == MAIN_ENTRY_NAME)
{
auto voidType = mlir_ts::VoidType::get(function.getContext());
// Verify that the given main has no inputs and results.
if (function.getNumArguments() ||
llvm::any_of(function.getFunctionType().getResults(), [&](mlir::Type type) { return type != voidType; }))
auto isArgc = [](mlir::Type type) { return isa<mlir::IntegerType>(type) || isa<mlir_ts::NumberType>(type); };
auto isArgv = [](mlir::Type type) {
if (auto refType = dyn_cast<mlir_ts::RefType>(type))
{
return isa<mlir_ts::StringType>(refType.getElementType());
}

auto arrayType = dyn_cast<mlir_ts::ArrayType>(type);
return arrayType && isa<mlir_ts::StringType>(arrayType.getElementType());
};
auto isExitCode = [&](mlir::Type type) { return isa<mlir_ts::VoidType>(type) || isArgc(type); };

auto inputs = function.getFunctionType().getInputs();
auto results = function.getFunctionType().getResults();
auto validInputs = inputs.size() <= 2
&& (inputs.size() < 1 || isArgc(inputs[0]))
&& (inputs.size() < 2 || isArgv(inputs[1]));
auto validResults = results.size() <= 1 && llvm::all_of(results, isExitCode);
if (!validInputs || !validResults)
{
function.emitError("expected 'main' to have 0 inputs and 0 results");
function.emitError("expected 'main' to be 'main(argc?: i32 | number, argv?: string[] | Ref<string>): void | i32 | number'");
return signalPassFailure();
}
}
Expand Down
105 changes: 105 additions & 0 deletions tslang/lib/TypeScript/LowerToLLVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7529,6 +7529,111 @@ void TypeScriptToLLVMLoweringPass::runOnOperation()
SymbolTable::setSymbolName(globalOp, dllName);
}

// @dllname/@linkname on a function: renamed here rather than in ExportFixPass, where LLVM's
// setName silently makes a taken name unique ("strlen.1") and the call binds nothing.
// Several TS declarations may bind one C symbol; the ones with the same TS type collapse into
// one op. The type is compared as the TS type, not the LLVM one: string and Opaque both lower
// to ptr, but the calls are still TS-typed here, and redirecting one to a callee of another
// signature would fail the verifier.
SmallVector<mlir_ts::FuncOp> renamedFuncs;
// every name a rename moves a function away from, and where it goes: binding one of these
// would bind whichever function holds the name when the loop gets there, so it is an error
// whatever the order of the two declarations
llvm::DenseMap<mlir::StringAttr, mlir::StringAttr> vacatedNames;
m.walk([&](mlir_ts::FuncOp funcOp) {
auto dllName = funcOp->getAttrOfType<mlir::StringAttr>(DLL_NAME);
if (!dllName)
{
return;
}

// naming the symbol the function already has is a no-op; dropped up front so the loop
// below never meets a still-attributed op under the target name
if (dllName == funcOp.getSymNameAttr())
{
funcOp->removeAttr(DLL_NAME);
return;
}

renamedFuncs.push_back(funcOp);
vacatedNames[funcOp.getSymNameAttr()] = dllName;
});

for (auto funcOp : renamedFuncs)
{
auto dllName = funcOp->getAttrOfType<mlir::StringAttr>(DLL_NAME);
funcOp->removeAttr(DLL_NAME);

auto vacated = vacatedNames.find(dllName);
if (vacated != vacatedNames.end())
{
funcOp.emitError("'") << funcOp.getSymName() << "' binds symbol '" << dllName.getValue()
<< "', which @dllname/@linkname renames to '" << vacated->second.getValue() << "'";
signalPassFailure();
return;
}

auto existingSymbol = SymbolTable::lookupSymbolIn(m, dllName);
if (existingSymbol)
{
auto existingFunc = dyn_cast<mlir_ts::FuncOp>(existingSymbol);
if (!existingFunc)
{
funcOp.emitError("'") << funcOp.getSymName() << "' binds symbol '" << dllName.getValue()
<< "', which is not a function";
signalPassFailure();
return;
}

if (existingFunc.getFunctionType() != funcOp.getFunctionType())
{
funcOp.emitError("'") << funcOp.getSymName() << "' binds symbol '" << dllName.getValue()
<< "' with type " << funcOp.getFunctionType() << ", but '"
<< existingFunc.getSymName() << "' declares it with type "
<< existingFunc.getFunctionType();
signalPassFailure();
return;
}

if (!funcOp.isExternal() && !existingFunc.isExternal())
{
funcOp.emitError("'") << funcOp.getSymName() << "' and '" << existingFunc.getSymName()
<< "' both define symbol '" << dllName.getValue() << "'";
signalPassFailure();
return;
}

if (failed(SymbolTable::replaceAllSymbolUses(funcOp, dllName, m)))
{
funcOp.emitError("can't rename function to @dllname '") << dllName.getValue() << "'";
signalPassFailure();
return;
}

// keep whichever of the two has the body
if (funcOp.isExternal())
{
funcOp.erase();
}
else
{
existingFunc.erase();
SymbolTable::setSymbolName(funcOp, dllName);
}

continue;
}

if (failed(SymbolTable::replaceAllSymbolUses(funcOp, dllName, m)))
{
funcOp.emitError("can't rename function to @dllname '") << dllName.getValue() << "'";
signalPassFailure();
return;
}

SymbolTable::setSymbolName(funcOp, dllName);
}

// The first thing to define is the conversion target. This will define the
// final target for this lowering. For this lowering, we are only targeting
// the LLVM dialect.
Expand Down
12 changes: 11 additions & 1 deletion tslang/lib/TypeScript/MLIRGenFunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,11 @@ namespace mlirgen
auto suppressExportForGenericInstantiation =
functionLikeDeclarationBaseAST->typeParameters.size() > 0 && !genContext.typeParamsWithArgs.empty();

if (mlir::failed(checkLinkNameDecorators(location, functionLikeDeclarationBaseAST, genContext)))
{
return std::make_tuple(funcOp, funcProto, mlir::failure(), false);
}

SmallVector<mlir::NamedAttribute> attrs;
auto dllExport = processFunctionAttributes(location, fullName, functionLikeDeclarationBaseAST, attrs, funcProtoGenContext,
suppressExportForGenericInstantiation);
Expand Down Expand Up @@ -919,7 +924,12 @@ namespace mlirgen
isPublic |=
((functionLikeDeclarationBaseAST->internalFlags & InternalFlags::DllExport) == InternalFlags::DllExport)
|| ((functionLikeDeclarationBaseAST->internalFlags & InternalFlags::IsPublic) == InternalFlags::IsPublic)
|| funcProto->getName() == MAIN_ENTRY_NAME;
|| funcProto->getName() == MAIN_ENTRY_NAME
// @dllname/@linkname: the function defines a symbol that something else binds by that
// name - a `declare` in this module, or another object. Private, SymbolDCE may erase it
// before TypeScriptToLLVMLoweringPass gives it the name (--opt inlines its callers
// first), and the declaration then binds nothing.
|| funcOp->hasAttr(DLL_NAME);

// if explicit public/protected - set public visibility
if (hasModifier(functionLikeDeclarationBaseAST, SyntaxKind::PublicKeyword)
Expand Down
45 changes: 44 additions & 1 deletion tslang/lib/TypeScript/MLIRGenImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2667,7 +2667,10 @@ class MLIRGenImpl
auto dllExport = !suppressExport
&& (getExportModifier(functionLikeDeclarationBaseAST)
|| ((functionLikeDeclarationBaseAST->internalFlags & InternalFlags::DllExport) == InternalFlags::DllExport));
if (dllExport)
// a declaration has no body to export: `export declare function` only makes it visible
// to importers, which the return value decides (__decls), while the attribute made the
// exe re-export the C function the declaration binds
if (dllExport && functionLikeDeclarationBaseAST->body)
{
attrs.push_back({mlir::StringAttr::get(builder.getContext(), "export"), mlir::UnitAttr::get(builder.getContext())});
}
Expand Down Expand Up @@ -11596,11 +11599,51 @@ class MLIRGenImpl
if (expr == SyntaxKind::Identifier)
{
auto name = MLIRHelper::getName(expr.as<Node>(), stringAllocator);
// @linkname is @dllname under the name that fits a static link; every reader
// of DLL_NAME takes both
if (name == LINK_NAME)
{
name = DLL_NAME;
}

functor(name, args);
}
}
}

// Two @dllname/@linkname decorators naming different symbols leave it ambiguous which one
// the declaration binds, so that is an error rather than last-one-wins. An error only
// reaches the user from a statement that fails (outputDiagnostics), so callers must
// return this result.
mlir::LogicalResult checkLinkNameDecorators(mlir::Location location, Node node, const GenContext &genContext)
{
StringRef first;
StringRef second;
iterateDecorators(node, genContext, [&](StringRef name, SmallVector<StringRef> args) {
if (name != DLL_NAME || args.empty())
{
return;
}

if (first.empty())
{
first = args.front();
}
else if (second.empty() && args.front() != first)
{
second = args.front();
}
});

if (!second.empty())
{
emitError(location) << "conflicting @dllname/@linkname: '" << first << "' vs '" << second << "'";
return mlir::failure();
}

return mlir::success();
}

bool isAddedToExport(mlir::Type type)
{
if (stage != Stages::SourceGeneration)
Expand Down
5 changes: 5 additions & 0 deletions tslang/lib/TypeScript/MLIRGenVariables.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,11 @@ namespace mlirgen
varClass.invariant = true;
}
});

if (mlir::failed(checkLinkNameDecorators(loc(variableDeclarationListAST), variableDeclarationListAST->parent, genContext)))
{
return mlir::failure();
}
}

// An exported module-level const is a variable, as an exported `let` is. A const may be
Expand Down
2 changes: 2 additions & 0 deletions tslang/lib/TypeScriptExportPass/ExportFixPass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ struct ExportFixPassCode
LLVM_DEBUG(llvm::dbgs() << "\nEXPORT Function: " << F.getName());
LLVM_DEBUG(llvm::dbgs() << "\nEXPORT Dump Before: ...\n" << F << "\n";);

// TypeScriptToLLVMLoweringPass renames @dllname/@linkname functions in MLIR, where a
// taken name can be merged or reported; this only catches an attribute added after it
if (F.hasFnAttribute(DLL_NAME))
{
auto dllName = F.getFnAttribute(DLL_NAME).getValueAsString().str();
Expand Down
Loading
Loading