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
29 changes: 29 additions & 0 deletions tslang/benchmarks/raytrace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,35 @@ pwsh tslang/benchmarks/raytrace/bench.ps1
- Rust: `opt-level=3`, `lto="fat"`, `codegen-units=1`, `panic="abort"`
- C++: `clang++ -std=c++20 -O3`, and `cl /std:c++20 /O2 /EHsc /MT`

## Results 2026-10-31: parameters captured by copy, direct method calls

Release, Windows 11 x64, the same machine; main 1cd17c33 against the same tree with each change
added in turn. Median of 7 runs after a warm-up; every build prints the same checksum. Neither
benchmark file changed.

- Parameters by copy: a closure used to move every parameter it captured into a heap cell, shared
by the frame and the closure's box, so `getNaturalColor` made six cells on every call for
`addLight`. Now a parameter that nothing assigns (neither the body nor a closure in it) is
captured by copy, as `this` and a `const` already were. Under `-mm=own`, a closure that does not
escape borrows the copy, as it borrowed the cell. Under own, a generator's parameters, callbacks
and `Shared<T>` handles stay in cells. Under none, the optimized `RayTracer.getNaturalColor` no
longer allocates on the heap: the closure's box is on its stack.
- Direct method calls: a call through a class's vtable to a method that no class in the module
overrides is made a direct call, as a call on a `@final` class's value already was, so LLVM can
inline it. A class that code outside the module may extend (exported, declared or imported, a
C++ binding, or a base of one of these) keeps its methods virtual. That takes in `RayTracer`'s
methods: variant F below, now without the source edit.

| Build | main | parameters by copy | and direct method calls | vs Rust idiomatic (55 ms) |
|---|---|---|---|---|
| tslang gc | 266 ms | 97 ms | 79 ms | 1.4x |
| tslang rc | 171 ms | 133 ms | 120 ms | 2.2x |
| tslang none | 157 ms | 96 ms | 79 ms | 1.4x |
| tslang own | 143 ms | 104 ms | 86 ms | 1.6x |

Of the three costs measured below, A's cells and F's virtual calls are now gone without editing
the source; B, `Intersection | null`, measured 16-17 ms under none and own, and remains.

## Results 2026-10-24: `@value`

Release, Windows 11 x64, the same machine and toolchains as below; branch `value-types` (98ec37ff,
Expand Down
1 change: 1 addition & 0 deletions tslang/lib/TypeScript/MLIRGenClasses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ namespace mlirgen

getClassesMap().insert({namePtr, newClassPtr});
fullNameClassesMap.insert(fullNamePtr, newClassPtr);
moduleClasses.push_back(newClassPtr);
}

return newClassPtr;
Expand Down
54 changes: 54 additions & 0 deletions tslang/lib/TypeScript/MLIRGenFunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1714,6 +1714,23 @@ namespace mlirgen
}
}
}
else if (node == SyntaxKind::PrefixUnaryExpression || node == SyntaxKind::PostfixUnaryExpression)
{
auto opCode = node == SyntaxKind::PrefixUnaryExpression
? node.as<ts::PrefixUnaryExpression>()->_operator
: node.as<ts::PostfixUnaryExpression>()->_operator;
if (opCode == SyntaxKind::PlusPlusToken || opCode == SyntaxKind::MinusMinusToken)
{
if (node == SyntaxKind::PrefixUnaryExpression)
{
addTargets(node.as<ts::PrefixUnaryExpression>()->operand);
}
else
{
addTargets(node.as<ts::PostfixUnaryExpression>()->operand);
}
}
}
else if (node == SyntaxKind::ForOfStatement)
{
auto initializer = node.as<ts::ForOfStatement>()->initializer;
Expand Down Expand Up @@ -1978,6 +1995,43 @@ namespace mlirgen
return mlir::failure();
}

// A parameter nothing assigns - not this body, not a closure in it - holds the caller's value
// for the whole call, so a closure takes a copy of it, as it does of `this` and of a `const`,
// instead of moving it into a heap cell the frame and the box share: a closure made per call
// (`reduce((col, light) => ... pos ...)`) cost one cell per parameter on every call.
// -mm=own keeps a generator's parameters in cells, whose state keeps or borrows them by its
// own rules (spec 38) - the generator is the wrapper buildGeneratorWrapperDeclaration made,
// whose body holds the `next` method - and two kinds anywhere: a callback, whose reach own
// follows through the cell (spec 54), and a Shared<T> handle, whose cell holds a count of its
// own (spec 23.2)
auto own = compileOptions.memoryModel == MemoryModelOwn;
auto generatorWrapper = false;
if (own && functionLikeDeclarationBaseAST->body)
{
ts::VisitorAST nextVisitor([&](ts::Node node) {
generatorWrapper = generatorWrapper ||
(node->internalFlags & InternalFlags::GeneratorNext) == InternalFlags::GeneratorNext;
});
nextVisitor.visit(functionLikeDeclarationBaseAST->body);
}

if (functionLikeDeclarationBaseAST->body && !generatorWrapper)
{
llvm::StringSet<> assignedNames;
llvm::StringSet<> lengthNames;
collectAssignedNames(functionLikeDeclarationBaseAST->body, assignedNames, &lengthNames);
for (const auto &param : funcProto->getParams())
{
auto keptInCell = own && (mth.isAnyFunctionType(param->getType()) ||
MLIRTypeHelper::isSharedHandleType(param->getType()));
if (!keptInCell && !param->getBindingPattern() && param->getName() != THIS_NAME &&
!assignedNames.contains(param->getName()) && !lengthNames.contains(param->getName()))
{
param->setCaptureByValue();
}
}
}

if (failed(mlirGenFunctionParamsBindings(firstIndex, funcProto, arguments, funcGenContext)))
{
return mlir::failure();
Expand Down
6 changes: 6 additions & 0 deletions tslang/lib/TypeScript/MLIRGenImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ class MLIRGenImpl
mlir::LogicalResult mlirCodeGenModule(SourceFile module, std::vector<SourceFile> includeFiles = {},
bool validate = true, bool isMain = true);

// calls through a vtable to methods no class overrides made direct, once the module is generated
void devirtualizeClosedMethods();

bool registerNamespace(llvm::StringRef namePtr, bool isFunctionNamespace = false);

mlir::LogicalResult exitNamespace();
Expand Down Expand Up @@ -16896,6 +16899,9 @@ class MLIRGenImpl

llvm::ScopedHashTable<StringRef, ClassInfo::TypePtr> fullNameClassesMap;

// every class this module made, its imported sources' too (devirtualizeClosedMethods)
llvm::SmallVector<ClassInfo::TypePtr> moduleClasses;

// the vtables of classes for interfaces they don't declare that casts ask for
// (interfaceVTableNameForClass), from the discovery pass on: see castAnyToInterface
llvm::StringSet<> classInterfaceVTablesCast;
Expand Down
99 changes: 99 additions & 0 deletions tslang/lib/TypeScript/MLIRGenModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -336,13 +336,112 @@ namespace mlirgen
compileOptions.generateDebugInfo = storeDebugInfo;
if (mlir::succeeded(mlirCodeGenModule(module, includeFiles)))
{
devirtualizeClosedMethods();
return theModule;
}
}

return nullptr;
}

// A call through a class's vtable to a method that no class overrides reaches that method
// alone, so it is made a direct call, as one on a @final receiver is (ClassMethodAccess): LLVM
// can inline it. Every class the module made is known by now, the imported sources' too; a
// class code outside the module may extend - exported, declared or imported, a C++ binding,
// or one of its subclasses so - keeps its methods virtual.
void MLIRGenImpl::devirtualizeClosedMethods()
{
auto seenOutside = [](const ClassInfo::TypePtr &classInfo) {
return classInfo->isExport || classInfo->isPublic || classInfo->isDeclaration || classInfo->isImport ||
classInfo->isDynamicImport || classInfo->isCppVTable;
};

llvm::StringSet<> candidates;
llvm::StringSet<> kept;
for (auto &classInfo : moduleClasses)
{
// the classes it extends, directly or not
llvm::SmallVector<ClassInfo::TypePtr> ancestors;
llvm::SmallPtrSet<ClassInfo *, 8> seen;
llvm::SmallVector<ClassInfo::TypePtr> pending(classInfo->baseClasses.begin(), classInfo->baseClasses.end());
while (!pending.empty())
{
auto base = pending.pop_back_val();
if (base && seen.insert(base.get()).second)
{
ancestors.push_back(base);
pending.append(base->baseClasses.begin(), base->baseClasses.end());
}
}

auto outside = seenOutside(classInfo);
for (auto &method : classInfo->methods)
{
if (method.isStatic)
{
continue;
}

// an abstract method has no body to call
if (outside || method.isAbstract)
{
kept.insert(method.funcName);
}
else
{
candidates.insert(method.funcName);
}

// a method of the same name in a base class, which this one overrides
for (auto &ancestor : ancestors)
{
for (auto &baseMethod : ancestor->methods)
{
if (!baseMethod.isStatic && baseMethod.name == method.name && baseMethod.funcName != method.funcName)
{
kept.insert(baseMethod.funcName);
}
}
}
}

// a class seen from outside may be extended there, overriding any method it has
if (outside)
{
for (auto &ancestor : ancestors)
{
for (auto &baseMethod : ancestor->methods)
{
kept.insert(baseMethod.funcName);
}
}
}
}

llvm::SmallVector<mlir_ts::ThisVirtualSymbolRefOp> virtualRefs;
theModule.walk([&](mlir_ts::ThisVirtualSymbolRefOp virtualRefOp) {
auto symbol = virtualRefOp.getIdentifier();
if (candidates.contains(symbol) && !kept.contains(symbol))
{
virtualRefs.push_back(virtualRefOp);
}
});

for (auto virtualRefOp : virtualRefs)
{
mlir::OpBuilder refBuilder(virtualRefOp);
auto directRefOp = refBuilder.create<mlir_ts::ThisSymbolRefOp>(
virtualRefOp.getLoc(), virtualRefOp.getType(), virtualRefOp.getThisVal(), virtualRefOp.getIdentifierAttr());
virtualRefOp.getResult().replaceAllUsesWith(directRefOp.getResult());
auto vtable = virtualRefOp.getVtable();
virtualRefOp.erase();
if (auto *vtableOp = vtable.getDefiningOp(); vtableOp && vtableOp->use_empty() && mlir::isMemoryEffectFree(vtableOp))
{
vtableOp->erase();
}
}
}

mlir::LogicalResult MLIRGenImpl::mlirGenCodeGenInit(SourceFile module)
{
sourceFile = module;
Expand Down
41 changes: 36 additions & 5 deletions tslang/lib/TypeScript/OwnershipFacts.h
Original file line number Diff line number Diff line change
Expand Up @@ -1420,9 +1420,9 @@ inline mlir_ts::CreateBoundFunctionOp closureOfBox(mlir::Value box)
return {};
}

// `this` of the class member a function is: its first parameter, of a class type, or a read of the
// parameter's slot, seen through views (spec 70)
inline bool isMemberThis(mlir::Value value)
// The function's own parameter a value is: the argument itself, or a read of the parameter's slot,
// seen through views; null for anything else
inline mlir::BlockArgument parameterRead(mlir::Value value)
{
value = rootOf(value);
// the parameter's slot: `ts.Param` in MLIRGen, `ts.Variable` initialized from it by the time the
Expand All @@ -1440,14 +1440,45 @@ inline bool isMemberThis(mlir::Value value)
}
else
{
return false;
return {};
}
}

auto argument = mlir::dyn_cast<mlir::BlockArgument>(value);
if (!argument || !argument.getOwner()->isEntryBlock() || !mlir::isa<mlir_ts::FuncOp>(argument.getOwner()->getParentOp()))
{
return {};
}

return argument;
}

// `this` of the class member a function is: its first parameter, of a class type, or a read of the
// parameter's slot, seen through views (spec 70)
inline bool isMemberThis(mlir::Value value)
{
auto argument = parameterRead(value);
return argument && argument.getArgNumber() == 0 && mlir::isa<mlir_ts::ClassType>(argument.getType());
}

// A copy a closure's own box holds, read out of it (the box is the closure body's first argument): the
// enclosing frame's, which the box keeps for as long as the closure runs
inline bool isOwnCaptureRead(mlir::Value value)
{
value = rootOf(value);
auto loadOp = value.getDefiningOp<mlir_ts::LoadOp>();
auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp<mlir_ts::PropertyRefOp>() : mlir_ts::PropertyRefOp();
auto argument = propertyRefOp ? mlir::dyn_cast<mlir::BlockArgument>(propertyRefOp.getObjectRef()) : mlir::BlockArgument();
return argument && argument.getArgNumber() == 0 && argument.getOwner()->isEntryBlock() &&
mlir::isa<mlir_ts::FuncOp>(argument.getOwner()->getParentOp()) &&
mlir::isa<mlir_ts::ClassType>(argument.getType());
mlir::isa<mlir_ts::RefType>(argument.getType()) && !mlir::isa<mlir_ts::RefType>(value.getType());
}

// A value a closure may capture by copy and borrow while it does not escape: the function's parameter
// (MLIRGen captures one nothing assigns by copy, `this` too - spec 70), or a copy its own box holds
inline bool isBorrowableCapture(mlir::Value value)
{
return parameterRead(value) || isOwnCaptureRead(value);
}

// A field of the member's `this`: of its object, or of a base class's storage in it (spec 57)
Expand Down
8 changes: 4 additions & 4 deletions tslang/lib/TypeScript/OwnershipInferenceClosures.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,16 @@ namespace own_inference
// ...and only when every copy has an owner of its own: the box frees itself alone, so a
// value made for it (a callback that captures, `a.filter((x) => x > n)`) would be
// nobody's
// ...or when it holds the member's `this` (a copy, spec 70), the caller's, as a closure
// that does not escape borrowed the cell it held before
// ...or when it holds the member's `this` (a copy, spec 70) or another parameter's copy,
// the caller's, as a closure that does not escape borrowed the cell it held before
auto noneFresh = llvm::none_of(closure->fills, [&](auto &fill) {
auto value = fill.second.getValue();
return !holdsNoBlock(value) && isFresh(rootOf(value));
});
auto holdsThis = llvm::any_of(closure->fills, [&](auto &fill) { return isMemberThis(fill.second.getValue()); });
auto holdsParam = llvm::any_of(closure->fills, [&](auto &fill) { return isBorrowableCapture(fill.second.getValue()); });
closure->borrowsCopies =
!closure->escape && noneFresh &&
(holdsThis || llvm::any_of(closure->boxUses, [](mlir::Operation *use) {
(holdsParam || llvm::any_of(closure->boxUses, [](mlir::Operation *use) {
return use->hasAttr(OWN_RESULT_BOUNDED_ATTR_NAME);
}));
if (!closure->borrowsCopies)
Expand Down
23 changes: 23 additions & 0 deletions tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,29 @@ namespace own_inference
// (spec 15), and noteLostFacts says why they cannot
if (auto param = borrowedParam(root); param >= 0)
{
// the retain of a parameter's copy for a closure's box (MLIRGen's mlirGenRetainCaptured
// reads the slot again for the box's field), the box of a closure that escapes - a
// parameter nothing assigns is captured by copy, and its cell used to be refused the same
// way (decideEscaping)
auto isBoxFill = [&](mlir::Operation *user) {
auto storeOp = mlir::dyn_cast<mlir_ts::StoreOp>(user);
auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp<mlir_ts::PropertyRefOp>()
: mlir_ts::PropertyRefOp();
return propertyRefOp && closureOfBox(propertyRefOp.getObjectRef()) && user->getLoc() == op->getLoc();
};
auto retainOp = mlir::dyn_cast<mlir_ts::RetainOp>(op);
auto retainedRead = retainOp ? retainOp->getOperand(0).getDefiningOp<mlir_ts::LoadOp>() : mlir_ts::LoadOp();
auto boxFill = retainedRead && llvm::any_of(retainedRead.getReference().getUsers(), [&](mlir::Operation *user) {
auto loadOp = mlir::dyn_cast<mlir_ts::LoadOp>(user);
return loadOp && llvm::any_of(loadOp->getUsers(), isBoxFill);
});
if (boxFill)
{
op->emitError("a closure that escapes owns what it captures, but -mm=own cannot move this variable "
"into it: its value is the caller's");
return true;
}

auto diag = op->emitError("'") << (name == "this value" ? paramName(param) : name);
if (isReturned(root))
{
Expand Down
10 changes: 6 additions & 4 deletions tslang/lib/TypeScript/OwnershipSignaturePass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -877,11 +877,13 @@ namespace own_signature
return false;
}

// `this` in a closure's box (captured by copy, spec 70): the object is the caller's, which
// no box can own. A closure that does not escape borrows it, as it did the cell; one stored
// into a field of `this` is a back-reference; any other one is inference's error
// `this` in a closure's box (captured by copy, spec 70), any other parameter nothing
// assigns, which a closure captures by copy too, or a copy the enclosing closure's box
// holds: the object is the caller's, which no box can own. A closure that does not escape
// borrows it, as it did the cell; one stored into a field of `this` is a back-reference;
// any other one is inference's error
if (auto propertyRefOp = storeOp.getReference().getDefiningOp<mlir_ts::PropertyRefOp>();
propertyRefOp && isMemberThis(used) && closureOfBox(propertyRefOp->getOperand(0)))
propertyRefOp && isBorrowableCapture(used) && closureOfBox(propertyRefOp->getOperand(0)))
{
return false;
}
Expand Down
Loading
Loading