diff --git a/tslang/benchmarks/raytrace/README.md b/tslang/benchmarks/raytrace/README.md index 3ab1c781c..68a2a28b1 100644 --- a/tslang/benchmarks/raytrace/README.md +++ b/tslang/benchmarks/raytrace/README.md @@ -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` 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, diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index a9b295511..f1d128d37 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -464,6 +464,7 @@ namespace mlirgen getClassesMap().insert({namePtr, newClassPtr}); fullNameClassesMap.insert(fullNamePtr, newClassPtr); + moduleClasses.push_back(newClassPtr); } return newClassPtr; diff --git a/tslang/lib/TypeScript/MLIRGenFunctions.cpp b/tslang/lib/TypeScript/MLIRGenFunctions.cpp index 6c431b782..ced1513f0 100644 --- a/tslang/lib/TypeScript/MLIRGenFunctions.cpp +++ b/tslang/lib/TypeScript/MLIRGenFunctions.cpp @@ -1714,6 +1714,23 @@ namespace mlirgen } } } + else if (node == SyntaxKind::PrefixUnaryExpression || node == SyntaxKind::PostfixUnaryExpression) + { + auto opCode = node == SyntaxKind::PrefixUnaryExpression + ? node.as()->_operator + : node.as()->_operator; + if (opCode == SyntaxKind::PlusPlusToken || opCode == SyntaxKind::MinusMinusToken) + { + if (node == SyntaxKind::PrefixUnaryExpression) + { + addTargets(node.as()->operand); + } + else + { + addTargets(node.as()->operand); + } + } + } else if (node == SyntaxKind::ForOfStatement) { auto initializer = node.as()->initializer; @@ -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 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 ¶m : 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(); diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index eab95ec94..dd5e8f3c3 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -274,6 +274,9 @@ class MLIRGenImpl mlir::LogicalResult mlirCodeGenModule(SourceFile module, std::vector 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(); @@ -16896,6 +16899,9 @@ class MLIRGenImpl llvm::ScopedHashTable fullNameClassesMap; + // every class this module made, its imported sources' too (devirtualizeClosedMethods) + llvm::SmallVector 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; diff --git a/tslang/lib/TypeScript/MLIRGenModule.cpp b/tslang/lib/TypeScript/MLIRGenModule.cpp index b57457db6..6a7cf9b15 100644 --- a/tslang/lib/TypeScript/MLIRGenModule.cpp +++ b/tslang/lib/TypeScript/MLIRGenModule.cpp @@ -336,6 +336,7 @@ namespace mlirgen compileOptions.generateDebugInfo = storeDebugInfo; if (mlir::succeeded(mlirCodeGenModule(module, includeFiles))) { + devirtualizeClosedMethods(); return theModule; } } @@ -343,6 +344,104 @@ namespace mlirgen 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 ancestors; + llvm::SmallPtrSet seen; + llvm::SmallVector 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 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( + 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; diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index dbfe3b070..81d1dedf9 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -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 @@ -1440,14 +1440,45 @@ inline bool isMemberThis(mlir::Value value) } else { - return false; + return {}; } } auto argument = mlir::dyn_cast(value); + if (!argument || !argument.getOwner()->isEntryBlock() || !mlir::isa(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(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(); + auto propertyRefOp = loadOp ? loadOp.getReference().getDefiningOp() : mlir_ts::PropertyRefOp(); + auto argument = propertyRefOp ? mlir::dyn_cast(propertyRefOp.getObjectRef()) : mlir::BlockArgument(); return argument && argument.getArgNumber() == 0 && argument.getOwner()->isEntryBlock() && mlir::isa(argument.getOwner()->getParentOp()) && - mlir::isa(argument.getType()); + mlir::isa(argument.getType()) && !mlir::isa(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) diff --git a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp index e87d8bcb3..17aaf6c09 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceClosures.cpp @@ -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) diff --git a/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp b/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp index 03c2aa91b..4236a3cd9 100644 --- a/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp +++ b/tslang/lib/TypeScript/OwnershipInferenceDiagnostics.cpp @@ -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(user); + auto propertyRefOp = storeOp ? storeOp.getReference().getDefiningOp() + : mlir_ts::PropertyRefOp(); + return propertyRefOp && closureOfBox(propertyRefOp.getObjectRef()) && user->getLoc() == op->getLoc(); + }; + auto retainOp = mlir::dyn_cast(op); + auto retainedRead = retainOp ? retainOp->getOperand(0).getDefiningOp() : mlir_ts::LoadOp(); + auto boxFill = retainedRead && llvm::any_of(retainedRead.getReference().getUsers(), [&](mlir::Operation *user) { + auto loadOp = mlir::dyn_cast(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)) { diff --git a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp index e56862755..f0ffd622f 100644 --- a/tslang/lib/TypeScript/OwnershipSignaturePass.cpp +++ b/tslang/lib/TypeScript/OwnershipSignaturePass.cpp @@ -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(); - propertyRefOp && isMemberThis(used) && closureOfBox(propertyRefOp->getOperand(0))) + propertyRefOp && isBorrowableCapture(used) && closureOfBox(propertyRefOp->getOperand(0))) { return false; } diff --git a/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp b/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp index f8f0b205d..1d14f3e5d 100644 --- a/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp +++ b/tslang/lib/TypeScriptExceptionPass/Win32ExceptionPass.cpp @@ -23,6 +23,10 @@ struct CatchRegion LandingPadInst *landingPad; llvm::SmallVector calls; + // throws the inliner brought into a catch, left out of `calls`: the catchret placed ahead of + // the region's end takes them out of the funclet - unless the region ends in a `resume` (a + // catch-all entered as a cleanup is), which has no catchret, and they need the bundle after all + llvm::SmallVector skippedThrows; CatchPadInst *catchPad; CleanupPadInst *cleanupPad; InvokeInst *unwindInfoOp; @@ -307,6 +311,7 @@ struct Win32ExceptionPassCode // funclet and does still need the bundle; leave that path alone. if (catchRegion->isCatch()) { + catchRegion->skippedThrows.push_back(CI); continue; } } @@ -437,6 +442,11 @@ struct Win32ExceptionPassCode } #endif + if (isa_and_nonnull(catchRegion.end)) + { + catchRegion.calls.append(catchRegion.skippedThrows); + } + // set funcset llvm::SmallVector newCalls; for (auto callBase : catchRegion.calls) diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 5c9b6a2b4..e3e8f0ebe 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -615,6 +615,7 @@ tslang_add_test(NAME test-compile-00-object-literal-contextual-lambda COMMAND te tslang_add_test(NAME test-compile-own-00-object-literal-contextual-lambda COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-compile-00-interface-field-getter-owned COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_field_getter_owned.ts") tslang_add_test(NAME test-compile-00-conditional-record-literal-temporary COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_record_literal_temporary.ts") +tslang_add_test(NAME test-compile-00-class-method-devirtualized COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_method_devirtualized.ts") tslang_add_test(NAME test-compile-00-call-arg-param-field-global COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00call_arg_param_field_global.ts") tslang_add_test(NAME test-compile-00-class-accessor-untyped-setter COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_untyped_setter.ts") tslang_add_test(NAME test-compile-00-class-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") @@ -1256,6 +1257,7 @@ tslang_add_test(NAME test-jit-00-object-literal-contextual-lambda COMMAND test-r tslang_add_test(NAME test-jit-own-00-object-literal-contextual-lambda COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/tests/00object_literal_contextual_lambda.ts") tslang_add_test(NAME test-jit-00-interface-field-getter-owned COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00interface_field_getter_owned.ts") tslang_add_test(NAME test-jit-00-conditional-record-literal-temporary COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00conditional_record_literal_temporary.ts") +tslang_add_test(NAME test-jit-00-class-method-devirtualized COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_method_devirtualized.ts") tslang_add_test(NAME test-jit-00-call-arg-param-field-global COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00call_arg_param_field_global.ts") tslang_add_test(NAME test-jit-00-class-accessor-untyped-setter COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_untyped_setter.ts") tslang_add_test(NAME test-jit-00-class-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") @@ -2272,6 +2274,7 @@ set(TSLANG_CORPUS 00object_literal_contextual_lambda.ts 00interface_field_getter_owned.ts 00conditional_record_literal_temporary.ts + 00class_method_devirtualized.ts 00call_arg_param_field_global.ts 00class_accessor_untyped_setter.ts 00class_accessor.ts @@ -4093,7 +4096,8 @@ foreach(own_test own_fresh_string own_fresh_array own_return_new own_try_local o own_callback_shapes own_callback_extended own_callback_interface own_generic_specializations own_callback_cast own_callback_interface_slot own_equal_strings_into_any own_any_copies own_array_copies own_string_pushed_twice own_rest_takes own_shares_elements - own_rest_keeps_declared own_interface_getter_let own_callback_assigns_capture) + own_rest_keeps_declared own_interface_getter_let own_callback_assigns_capture + own_param_capture_copies) tslang_add_test(NAME test-jit-own-${own_test} COMMAND test-runner -jit -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") tslang_add_test(NAME test-compile-own-${own_test} COMMAND test-runner -mm=own "${PROJECT_SOURCE_DIR}/test/tester/own/${own_test}.ts") # The promise: no counting at all in what own emits. @@ -4132,7 +4136,8 @@ foreach(keeps_mm rc gc none) own_captured_string_callback own_callback_and_call own_callback_captured own_literal_closures own_literal_variable_closures own_temporary_field_take own_union_narrowed_calls own_named_field_take own_let_field_take own_literal_captures own_literal_object_captures own_literal_interface_captures own_literal_let_captures own_interface_generic_method own_back_reference own_field_function_result own_callback_shapes own_callback_extended own_callback_interface own_generic_specializations own_callback_cast own_callback_interface_slot own_equal_strings_into_any - own_rest_keeps_declared own_interface_getter_let own_callback_assigns_capture) + own_rest_keeps_declared own_interface_getter_let own_callback_assigns_capture + own_param_capture_copies) tslang_add_test(NAME test-jit-${keeps_mm}-${keeps_test} COMMAND test-runner -jit -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts") tslang_add_test(NAME test-compile-${keeps_mm}-${keeps_test} COMMAND test-runner -mm=${keeps_mm} "${PROJECT_SOURCE_DIR}/test/tester/own/${keeps_test}.ts") endforeach() @@ -4205,6 +4210,13 @@ add_test(NAME test-own-interface-getter-let-released "-DFILE=${PROJECT_SOURCE_DIR}/test/tester/own/own_interface_getter_let.ts" -P "${CMAKE_CURRENT_SOURCE_DIR}/own-interface-getter-let.cmake") +# A call through a vtable to a method no class overrides is made direct; one with an override stays +# virtual. The program runs the same either way, so the IR is read. +add_test(NAME test-class-method-devirtualized-ir + COMMAND ${CMAKE_COMMAND} -DTSLANG=$ + "-DFILE=${PROJECT_SOURCE_DIR}/test/tester/tests/00class_method_devirtualized.ts" + -P "${CMAKE_CURRENT_SOURCE_DIR}/class-method-devirtualized.cmake") + # A rest parameter its function says it keeps (`@keeps`) is released once emptied under own, as an # inferred keep's is (spec 72); no run can see the leak, so the IR is read. add_test(NAME test-own-rest-keeps-declared-released diff --git a/tslang/test/tester/class-method-devirtualized.cmake b/tslang/test/tester/class-method-devirtualized.cmake new file mode 100644 index 000000000..64b622086 --- /dev/null +++ b/tslang/test/tester/class-method-devirtualized.cmake @@ -0,0 +1,38 @@ +# A call through a class's vtable to a method no class overrides is made direct +# (MLIRGenImpl::devirtualizeClosedMethods); one an override exists for stays virtual +# (tests/00class_method_devirtualized.ts). The IR is read (--emit=mlir): `nameOf` calls `Shape.name` +# directly, and `areaOf` still calls `Shape.area` through the vtable. +cmake_minimum_required(VERSION 3.17.3) +execute_process(COMMAND "${TSLANG}" --emit=mlir --no-default-lib "${FILE}" + OUTPUT_VARIABLE out ERROR_VARIABLE err RESULT_VARIABLE result) +# the IR is printed to stderr +string(APPEND out "${err}") +if(NOT result EQUAL 0) + message(FATAL_ERROR "tslang failed (${result}):\n${out}") +endif() + +# the body of a function, from its `ts.Func` to the next one +function(body_of name var) + string(FIND "${out}" "ts.Func @${name} " begin) + if(begin EQUAL -1) + message(FATAL_ERROR "no function `${name}` in:\n${out}") + endif() + string(SUBSTRING "${out}" ${begin} -1 body) + string(SUBSTRING "${body}" 1 -1 rest) + string(FIND "${rest}" "ts.Func @" end) + if(NOT end EQUAL -1) + math(EXPR end "${end} + 1") + string(SUBSTRING "${body}" 0 ${end} body) + endif() + set(${var} "${body}" PARENT_SCOPE) +endfunction() + +body_of(nameOf name_body) +if(name_body MATCHES "ThisVirtualSymbolRef" OR NOT name_body MATCHES "\"ts[.]ThisSymbolRef\"[(]%[0-9]+[)] <{identifier = @Shape[.]name}>") + message(FATAL_ERROR "`nameOf` does not call `Shape.name` directly:\n${name_body}") +endif() + +body_of(areaOf area_body) +if(NOT area_body MATCHES "ThisVirtualSymbolRef") + message(FATAL_ERROR "`areaOf` does not call `Shape.area` through the vtable:\n${area_body}") +endif() diff --git a/tslang/test/tester/own/own_param_capture_copies.ts b/tslang/test/tester/own/own_param_capture_copies.ts new file mode 100644 index 000000000..7bb8773d8 --- /dev/null +++ b/tslang/test/tester/own/own_param_capture_copies.ts @@ -0,0 +1,94 @@ +// A parameter nothing assigns is captured by copy, not moved into a heap cell: the ray tracer's +// `addLight` shape, a callback over the method's parameters given to `reduce`, called with values +// read out of fields. A parameter the body or a closure assigns stays in a cell, so every reader +// sees the assignment. Each call churns, so a parameter freed too early reads garbage; every model +// runs it. +class Item { + constructor(public n: number) {} +} + +interface Thing { + n: number; +} + +class Light implements Thing { + constructor(public n: number) {} +} + +interface Scene { + lights: Light[]; + thing: Thing; +} + +function churn() { + const junk: Item[] = []; + for (let i = 0; i < 64; i++) junk.push(new Item(999)); +} + +class Tracer { + k = 2; + + // `this`, an interface, a class and a number, all captured by a closure that does not escape + natural(thing: Thing, item: Item, scene: Scene, scale: number) { + const add = (sum: number, light: Light) => { + churn(); + return sum + light.n * thing.n + item.n * scale + scene.lights.length + this.k; + }; + return scene.lights.reduce(add, 0); + } + + // a closure in a closure over the same parameter + nested(item: Item, xs: number[]) { + const outer = (x: number) => { + const inner = (y: number) => y + item.n; + return inner(x) * 2; + }; + let s = 0; + for (const x of xs) s += outer(x); + return s; + } + + trace(scene: Scene, item: Item) { + // the arguments are read out of fields: the callee borrows them + return this.natural(scene.thing, item, scene, 3) + this.natural(scene.lights[0], item, scene, 1); + } +} + +// assigned by the closure: the frame sees it +function assignedInClosure(n: number, xs: number[]) { + const bump = (x: number) => { + n = n + x; + }; + for (const x of xs) bump(x); + return n; +} + +// assigned by the body after the closure is made: the closure sees it +function assignedAfter(n: number) { + const read = () => n; + n++; + const first = read(); + --n; + n += 10; + return first * 100 + read(); +} + +function main() { + const scene: Scene = { lights: [new Light(1), new Light(2), new Light(3)], thing: new Light(5) }; + const tracer = new Tracer(); + const item = new Item(7); + + // natural(thing 5) = (1+2+3)*5 + 3*(7*3 + 3 + 2) = 30 + 78 = 108 + // natural(thing 1) = (1+2+3)*1 + 3*(7*1 + 3 + 2) = 6 + 36 = 42 + const traced = tracer.trace(scene, item); + assert(traced == 150, "captured parameters read after churn"); + assert(item.n == 7 && scene.thing.n == 5, "the caller's values are still there"); + + // (1+7)*2 + (2+7)*2 + (3+7)*2 = 54 + assert(tracer.nested(item, [1, 2, 3]) == 54, "a closure in a closure"); + + assert(assignedInClosure(1, [1, 2, 3]) == 7, "assigned by the closure"); + assert(assignedAfter(4) == 514, "assigned by the body"); + + print("done."); +} diff --git a/tslang/test/tester/tests/00class_method_devirtualized.ts b/tslang/test/tester/tests/00class_method_devirtualized.ts new file mode 100644 index 000000000..5610e916c --- /dev/null +++ b/tslang/test/tester/tests/00class_method_devirtualized.ts @@ -0,0 +1,148 @@ +// A call through a class's vtable to a method no class overrides is made a direct call; one that a +// class overrides - declared before or after the call, any number of classes down - stays virtual. + +// nothing extends it: its calls are direct +class Plain { + constructor(public n: number) {} + + value() { + return this.n * 2; + } + + twice() { + return this.value() + this.value(); + } +} + +// `area` is overridden by a class declared after the code that calls it; `name` is not +class Shape { + area() { + return 0; + } + + name() { + return "shape"; + } +} + +function areaOf(s: Shape) { + return s.area(); +} + +function nameOf(s: Shape) { + return s.name(); +} + +class Square extends Shape { + constructor(public side: number) { + super(); + } + + area() { + return this.side * this.side; + } +} + +// `level` is overridden two classes down only +class A { + level() { + return 1; + } + + kind() { + return "a"; + } +} + +class B extends A {} + +class C extends B { + level() { + return 3; + } +} + +function levelOf(a: A) { + return a.level(); +} + +function levelOfB(b: B) { + return b.level(); +} + +abstract class Animal { + abstract sound(): string; + + speak() { + return this.sound() + "!"; + } +} + +class Dog extends Animal { + sound() { + return "woof"; + } +} + +// a specialization of a generic class, overridden by a class extending it +class Box { + constructor(public v: T) {} + + read() { + return this.v; + } +} + +class TenBox extends Box { + read() { + return this.v * 10; + } +} + +function readBox(b: Box) { + return b.read(); +} + +// overridden by a class declared inside a function +class Outer { + m() { + return 1; + } +} + +const makeInner = () => { + class Inner extends Outer { + m() { + return 7; + } + } + + return new Inner(); +}; + +function callM(o: Outer) { + return o.m(); +} + +function main() { + assert(readBox(new TenBox(2)) == 20 && readBox(new Box(3)) == 3, "a generic class's override"); + assert(callM(makeInner()) == 7 && callM(new Outer()) == 1, "an override in a function's class"); + + const p = new Plain(5); + assert(p.value() == 10, "a method nothing overrides"); + assert(p.twice() == 20, "called on `this`"); + + assert(areaOf(new Square(3)) == 9, "an override declared after the call"); + assert(areaOf(new Shape()) == 0, "the base's own method"); + assert(nameOf(new Square(3)) == "shape", "an inherited method"); + + assert(levelOf(new C()) == 3, "overridden two classes down, through the base"); + assert(levelOfB(new C()) == 3, "overridden two classes down, through the middle class"); + assert(levelOf(new B()) == 1, "the middle class has the base's"); + assert(new C().kind() == "a", "an inherited method of a chain"); + + const animal: Animal = new Dog(); + assert(animal.speak() == "woof!", "an abstract method"); + + print("done."); +}