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
857 changes: 857 additions & 0 deletions tslang/docs/superpowers/plans/2026-09-28-own-phase-0.md

Large diffs are not rendered by default.

479 changes: 479 additions & 0 deletions tslang/docs/superpowers/specs/2026-09-24-own-memory-model-design.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions tslang/include/TypeScript/DataStructs.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ struct CompileOptions
{
return memoryModel == MemoryModelRC;
}

// Whether the ownership operations are live and lower to code - retains and releases under
// `rc`, releases that destroy under `own`. Where only the count itself matters, the
// question is isRefCounted().
bool tracksOwnership() const
{
return memoryModel == MemoryModelRC || memoryModel == MemoryModelOwn;
}
};

#endif // TYPESCRIPT_DATASTRUCT_H_
2 changes: 1 addition & 1 deletion tslang/include/TypeScript/LowerToLLVM/CastLogicHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,7 @@ class CastLogicHelper
// pointer's block header is undefined behaviour. An iterator's final
// `{ value: undefined, done: true }` is built exactly this way, and the caller
// retains the result before it looks at `done`.
if (compileOptions.isRefCounted())
if (compileOptions.tracksOwnership())
{
return rewriter.create<LLVM::ZeroOp>(loc, llvmRtArrayStructType).getResult();
}
Expand Down
4 changes: 2 additions & 2 deletions tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ class LLVMCodeHelper : public LLVMCodeHelperBase
// can be bound to a local that takes a reference, so the same header word has to sit in
// front of the data, marked immortal - exactly as it does for a string literal. Without
// it a retain reads, and a release writes, the word before a read-only global.
auto withHeader = compileOptions.isRefCounted();
auto withHeader = compileOptions.tracksOwnership();
auto headerSize = getHeapBlockHeaderSize();

// Create the global at the entry of the module.
Expand Down Expand Up @@ -722,7 +722,7 @@ class LLVMCodeHelper : public LLVMCodeHelperBase
// field (a generator's state object is built exactly this way), and walking an
// undef pointer to reach its header is undefined behaviour, which the optimizer
// is entitled to - and does - fold the whole caller away for.
auto unspecifiedFieldIsOwning = compileOptions.isRefCounted() &&
auto unspecifiedFieldIsOwning = compileOptions.tracksOwnership() &&
MLIRTypeHelper(rewriter.getContext(), compileOptions).ownsHeapMemory(loc, type);

mlir::Value itemValue = unspecifiedFieldIsOwning
Expand Down
10 changes: 7 additions & 3 deletions tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ class LLVMCodeHelperBase
}
}

if (compileOptions.isRefCounted())
if (compileOptions.tracksOwnership())
{
// The block starts *unowned*. Whoever first takes it - a local's declaration, a
// field or element store, a literal capturing it, a push - is what brings the count
Expand All @@ -398,9 +398,13 @@ class LLVMCodeHelperBase
// HEAP_BLOCK_IMMORTAL, so the block leaks instead of being freed out from under a
// live reference.
//
// Under `-mm=own` there is no count, and the same 0 only says "mortal": the free path
// reads the word to skip immortal blocks, and a native `malloc` block's header is
// otherwise whatever the allocator left there (the zeroing above is wasm only).
//
// Written after the zeroing above, which covers the header along with the payload.
// Only under `-mm=rc` -- under `gc` nothing reads the word, and a store per
// allocation on the hot path is not worth paying for dead code.
// Only under `-mm=rc` and `-mm=own` -- under `gc` nothing reads the word, and a store
// per allocation on the hot path is not worth paying for dead code.
rewriter.create<LLVM::StoreOp>(
loc, rewriter.create<LLVM::ConstantOp>(loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, 0)),
blockPtr);
Expand Down
25 changes: 22 additions & 3 deletions tslang/include/TypeScript/LowerToLLVM/OwnershipRoutineLogic.h
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ class OwnershipRoutineLogic
void emitReleaseArrayElements(mlir::Type elementType, mlir::Value dataPtr, mlir::Value startIndex,
mlir::Value count)
{
if (!compileOptions.isRefCounted())
if (!compileOptions.tracksOwnership())
{
return;
}
Expand Down Expand Up @@ -588,8 +588,27 @@ class OwnershipRoutineLogic
ValueRange{payloadPtr});
}

// Is this block mortal - that is, not HEAP_BLOCK_IMMORTAL? Inline rather than a helper: it
// is one load and one compare, and it is the whole of what a release costs under own.
mlir::Value emitIsMortal(mlir::Value payloadPtr)
{
TypeConverterHelper tch(typeConverter);
TypeHelper th(rewriter);
LLVMCodeHelperBase ch(op, rewriter, typeConverter, compileOptions);
auto loc = op->getLoc();

auto llvmIndexType = tch.convertType(th.getIndexType());
auto blockPtr = ch.getBlockPtrFromPayloadPtr(loc, payloadPtr, llvmIndexType);
auto header = rewriter.create<LLVM::LoadOp>(loc, llvmIndexType, blockPtr);
auto immortal = rewriter.create<LLVM::ConstantOp>(
loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, HEAP_BLOCK_IMMORTAL));
return rewriter.create<LLVM::ICmpOp>(loc, LLVM::ICmpPredicate::ne, header, immortal);
}

// Runs `thenBody` -- the destroy half: release what the value owns, then free it -- only
// when `payloadPtr` is non-null and the reference being dropped was the last one.
// when `payloadPtr` is non-null and the reference being dropped was the last one. Under
// own there is no count: the one owner is giving the block up, so it goes unless it is
// immortal (a literal, a `typeof` tag).
void emitIfLastReference(mlir::Value payloadPtr, llvm::function_ref<void()> thenBody)
{
TypeHelper th(rewriter);
Expand All @@ -605,7 +624,7 @@ class OwnershipRoutineLogic
rewriter.create<LLVM::BrOp>(loc, ValueRange{}, continuationBlock);

rewriter.setInsertionPointToEnd(decBlock);
auto wasLast = emitDecRef(payloadPtr);
auto wasLast = compileOptions.memoryModel == MemoryModelOwn ? emitIsMortal(payloadPtr) : emitDecRef(payloadPtr);
rewriter.create<LLVM::CondBrOp>(loc, wasLast, thenBlock, continuationBlock);

rewriter.setInsertionPointToEnd(currentBlock);
Expand Down
4 changes: 4 additions & 0 deletions tslang/include/TypeScript/Passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ std::unique_ptr<mlir::Pass> createRelocateConstantPass();
/// and in every memory model - the ownership ops survive to there regardless of model.
std::unique_ptr<mlir::Pass> createOwnershipVerifierPass();

/// Ownership inference for -mm=own: erases the retains it can prove are a fresh value's single
/// acquisition and reports every other one. Runs at the affine level under own only.
std::unique_ptr<mlir::Pass> createOwnershipInferencePass();

/// Lets a call take over the reference its callee returned instead of retaining a second one.
/// Runs on the whole module, after MLIRGen, because deciding which callees retain their result
/// needs every function to be present - see docs/reference-counting-evaluation.md section 9.27.
Expand Down
8 changes: 7 additions & 1 deletion tslang/include/TypeScript/TypeScriptCompiler/Defines.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ enum MemoryModel
// covered - leaks rather than being swept up. See sections 9.6 and 9.28.
MemoryModelRC,
// No reclamation at all.
MemoryModelNone
MemoryModelNone,
// Single ownership, inferred at compile time: every mortal block has exactly one owner,
// which destroys it. No count is kept; a program whose ownership cannot be proven is a
// compile error. See docs/superpowers/specs/2026-09-24-own-memory-model-design.md.
MemoryModelOwn
};

// The spelling used both by the `-mm=` flag and by the shared-library marker symbol, so the
Expand All @@ -43,6 +47,8 @@ inline const char *memoryModelName(enum MemoryModel model)
return "rc";
case MemoryModelNone:
return "none";
case MemoryModelOwn:
return "own";
default:
return "gc";
}
Expand Down
1 change: 1 addition & 0 deletions tslang/lib/TypeScript/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ add_mlir_dialect_library(MLIRTypeScript
LowerToLLVM.cpp
RelocateConstantPass.cpp
OwnershipVerifierPass.cpp
OwnershipInferencePass.cpp
OwnedReturnConsumptionPass.cpp
GCPass.cpp
AsyncTargetWidthPass.cpp
Expand Down
60 changes: 50 additions & 10 deletions tslang/lib/TypeScript/LowerToLLVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,12 @@ class TypeDescriptorOpLowering : public TsLlvmPattern<mlir_ts::TypeDescriptorOp>
// symbol has to exist before the global is built
OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
auto releaseRoutineName = orl.getOrCreateReleaseRoutine(descriptorType);
auto retainRoutineName = orl.getOrCreateRetainRoutine(descriptorType);
// Under own nothing retains: the descriptor's retain slot is read only from inside
// retain routines (retainViaDescriptor), none of which can run, and building one would
// bring `__tslang_inc_ref` into a model that promises it is never referenced.
auto retainRoutineName = tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn
? std::string()
: orl.getOrCreateRetainRoutine(descriptorType);

rewriter.replaceOp(op, ch.getOrCreateTypeDescriptorName(descriptorType, name,
TypeOfOpHelper::typeKindFromName(name),
Expand All @@ -420,6 +425,13 @@ class RetainOpLowering : public TsLlvmPattern<mlir_ts::RetainOp>
LogicalResult matchAndRewrite(mlir_ts::RetainOp op, Adaptor transformed,
ConversionPatternRewriter &rewriter) const final
{
// Under own, ownership inference erased every retain it could prove, and reported the
// rest; one that reaches here was neither, and erasing it would ship a double free.
if (tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn)
{
return op.emitError("ownership inference left a retain behind");
}

if (tsLlvmContext->compileOptions.isRefCounted())
{
OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
Expand All @@ -439,7 +451,7 @@ class ReleaseOpLowering : public TsLlvmPattern<mlir_ts::ReleaseOp>
LogicalResult matchAndRewrite(mlir_ts::ReleaseOp op, Adaptor transformed,
ConversionPatternRewriter &rewriter) const final
{
if (tsLlvmContext->compileOptions.isRefCounted())
if (tsLlvmContext->compileOptions.tracksOwnership())
{
OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
orl.emitReleaseValue(op.getReference().getType(), transformed.getReference());
Expand All @@ -460,6 +472,13 @@ class RetainSlotOpLowering : public TsLlvmPattern<mlir_ts::RetainSlotOp>
LogicalResult matchAndRewrite(mlir_ts::RetainSlotOp op, Adaptor transformed,
ConversionPatternRewriter &rewriter) const final
{
// Under own, ownership inference erased every retain it could prove, and reported the
// rest; one that reaches here was neither, and erasing it would ship a double free.
if (tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn)
{
return op.emitError("ownership inference left a retain behind");
}

if (tsLlvmContext->compileOptions.isRefCounted())
{
OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
Expand All @@ -479,7 +498,7 @@ class ReleaseSlotOpLowering : public TsLlvmPattern<mlir_ts::ReleaseSlotOp>
LogicalResult matchAndRewrite(mlir_ts::ReleaseSlotOp op, Adaptor transformed,
ConversionPatternRewriter &rewriter) const final
{
if (tsLlvmContext->compileOptions.isRefCounted())
if (tsLlvmContext->compileOptions.tracksOwnership())
{
OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
orl.emitReleaseSlot(cast<mlir_ts::RefType>(op.getSlot().getType()).getElementType(), transformed.getSlot());
Expand All @@ -500,6 +519,13 @@ class RetainCellOpLowering : public TsLlvmPattern<mlir_ts::RetainCellOp>
LogicalResult matchAndRewrite(mlir_ts::RetainCellOp op, Adaptor transformed,
ConversionPatternRewriter &rewriter) const final
{
// Under own, ownership inference erased every retain it could prove, and reported the
// rest; one that reaches here was neither, and erasing it would ship a double free.
if (tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn)
{
return op.emitError("ownership inference left a retain behind");
}

if (tsLlvmContext->compileOptions.isRefCounted())
{
OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
Expand All @@ -519,7 +545,7 @@ class ReleaseCellOpLowering : public TsLlvmPattern<mlir_ts::ReleaseCellOp>
LogicalResult matchAndRewrite(mlir_ts::ReleaseCellOp op, Adaptor transformed,
ConversionPatternRewriter &rewriter) const final
{
if (tsLlvmContext->compileOptions.isRefCounted())
if (tsLlvmContext->compileOptions.tracksOwnership())
{
OwnershipRoutineLogic orl(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
orl.emitReleaseCell(cast<mlir_ts::RefType>(op.getSlot().getType()).getElementType(),
Expand Down Expand Up @@ -648,7 +674,7 @@ class SetLengthOfOpLowering : public TsLlvmPattern<mlir_ts::SetLengthOfOp>
//
// Only where an element owns something, and only under -mm=rc: nothing reads an
// unwritten slot in the other models, and the memset is not free.
if (tsLlvmContext->compileOptions.isRefCounted())
if (tsLlvmContext->compileOptions.tracksOwnership())
{
MLIRTypeHelper mth(rewriter.getContext(), tsLlvmContext->compileOptions);
if (mth.ownsHeapMemory(loc, elementType))
Expand Down Expand Up @@ -1484,7 +1510,7 @@ class UndefOpLowering : public TsLlvmPattern<mlir_ts::UndefOp>
// reaching an undef pointer's block header is undefined behaviour. An iterator's final
// `{ value: undefined, done: true }` is built exactly this way, and the caller retains
// the result before it looks at `done`.
if (tsLlvmContext->compileOptions.isRefCounted())
if (tsLlvmContext->compileOptions.tracksOwnership())
{
MLIRTypeHelper mth(rewriter.getContext(), tsLlvmContext->compileOptions);
if (mth.ownsHeapMemory(op.getLoc(), op.getType()))
Expand Down Expand Up @@ -2406,6 +2432,7 @@ struct VariableOpLowering : public TsLlvmPattern<mlir_ts::VariableOp>
// is born unowned because a receiver is about to take it (§9.24), a cell is born
// owned: the frame is its first owner, and the frame's scope exit is what gives that
// reference back. A box is the one heap variable with no frame owner, and says so.
// Under own the frame owns the cell outright; there is no count to start at one.
if (tsLlvmContext->compileOptions.isRefCounted() && !varOp->hasAttr(CAPTURE_BOX_ATTR_NAME))
{
OwnershipRoutineLogic orl(varOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
Expand Down Expand Up @@ -2447,7 +2474,7 @@ struct VariableOpLowering : public TsLlvmPattern<mlir_ts::VariableOp>

auto value = transformed.getInitializer();
auto isUnwrittenCell = isCaptured && !varOp->hasAttr(CAPTURE_BOX_ATTR_NAME);
if (!value && tsLlvmContext->compileOptions.isRefCounted() &&
if (!value && tsLlvmContext->compileOptions.tracksOwnership() &&
(varOp->hasAttr(OWNED_LOCAL_ATTR_NAME) || isUnwrittenCell))
{
// An owned local with no initializer here is one whose storage was hoisted out in
Expand Down Expand Up @@ -2484,9 +2511,19 @@ struct VariableOpLowering : public TsLlvmPattern<mlir_ts::VariableOp>
// (`isOwningSlot`). So the first value has to be taken too, unless the frame has
// already taken it - which is what an owned local's mark says, and what a captured
// parameter's storage is precisely missing, the argument being the caller's.
if (isCaptured && tsLlvmContext->compileOptions.isRefCounted() &&
if (isCaptured && tsLlvmContext->compileOptions.tracksOwnership() &&
!varOp->hasAttr(CAPTURE_BOX_ATTR_NAME) && !varOp->hasAttr(OWNED_LOCAL_ATTR_NAME))
{
// The value is someone else's - a parameter's is the caller's - and a cell that
// destroyed it would free it under them. Own has no count to take here: closures
// are phase 5.
if (tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn &&
MLIRTypeHelper(rewriter.getContext(), tsLlvmContext->compileOptions)
.ownsHeapMemory(location, referenceType.getElementType()))
{
return varOp.emitError("a captured variable cannot own its value under -mm=own yet");
}

OwnershipRoutineLogic orl(varOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
orl.emitRetainSlot(referenceType.getElementType(), allocated);
}
Expand Down Expand Up @@ -3427,7 +3464,7 @@ struct DeleteOpLowering : public TsLlvmPattern<mlir_ts::DeleteOp>
// Under reference counting `delete` drops a reference rather than freeing outright:
// the object goes only if this was the last one, and what it owns is released with it.
// That also keeps `delete` off an immortal block, which a bare free would not.
if (tsLlvmContext->compileOptions.isRefCounted())
if (tsLlvmContext->compileOptions.tracksOwnership())
{
OwnershipRoutineLogic orl(deleteOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
orl.emitReleaseValue(deleteOp.getReference().getType(), transformed.getReference());
Expand Down Expand Up @@ -5691,7 +5728,10 @@ struct NewInterfaceOpLowering : public TsLlvmPattern<mlir_ts::NewInterfaceOp>
// generated first: the descriptor's initializer takes their addresses
OwnershipRoutineLogic orl(newInterfaceOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
auto releaseRoutineName = orl.getOrCreateReleaseRoutine(thisType);
auto retainRoutineName = orl.getOrCreateRetainRoutine(thisType);
// under own nothing retains: see TypeDescriptorOpLowering
auto retainRoutineName = tsLlvmContext->compileOptions.memoryModel == MemoryModelOwn
? std::string()
: orl.getOrCreateRetainRoutine(thisType);

LLVMCodeHelper ch(newInterfaceOp, rewriter, getTypeConverter(), tsLlvmContext->compileOptions);
return ch.getOrCreateTypeDescriptorName(thisType, name, TypeOfOpHelper::typeKindFromName(name),
Expand Down
4 changes: 2 additions & 2 deletions tslang/lib/TypeScript/MLIRGenExpressions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ namespace mlirgen
// Same two cases as a local's declaration: a value that already carries a
// reference hands it over, anything else is retained. Either way the slot
// is the owner from here, and the awaiting scope gives that reference back.
if (compileOptions.isRefCounted() && mth.ownsHeapMemory(location, resultType))
if (compileOptions.tracksOwnership() && mth.ownsHeapMemory(location, resultType))
{
if (producesOwnedReference(value))
{
Expand Down Expand Up @@ -1191,7 +1191,7 @@ namespace mlirgen
// wherever the allocator has since put that memory. Whether that faults depends on what
// was allocated in between, which is why `00class_static.ts` needed two `print` calls
// between the delete and the end of the function to show it.
if (compileOptions.isRefCounted())
if (compileOptions.tracksOwnership())
{
// `delete new C()` and `delete c`, where `c` is a `const` the compiler kept as a
// value rather than storage: the reference is one nobody has claimed, and §9.30
Expand Down
Loading
Loading