Repository navigation
Conversation
…truct data An embedder can have a function whose `new f()` must evaluate to a primitive. bun:ffi `CString` is one: `new CString(ptr)` is a string. Such a function cannot be a constructor: every algorithm that constructs its argument trusts [[Construct]] to return an object. Array.from writes into the result with put_by_val_direct, and Interpreter::executeConstruct casts it with asObject. `new f()` and `super()` on an InternalFunction do not read construct data. setUpCall, linkFor and virtualFor jump to the InternalFunction trampoline, which loads m_functionForConstruct from the cell. Everything else asks getConstructData first. So a new type-info flag, ReportsNoConstructData, makes InternalFunction::getConstructData report CallData::Type::None while m_functionForConstruct stays installed. isConstructor() is false for such a function. Array.of and Array.from make a plain array, and a species lookup, Reflect.construct, bind, Proxy and `extends` throw a TypeError. `new f()` keeps its result on every tier. No function sets the flag here. $vm.createInternalFunctionWithoutConstructData() makes one for the test. * JSTests/stress/internal-function-new-without-construct-data.js: Added. * Source/JavaScriptCore/runtime/InternalFunction.cpp: (JSC::InternalFunction::getConstructData): * Source/JavaScriptCore/runtime/JSTypeInfo.h: (JSC::TypeInfo::reportsNoConstructData const): * Source/JavaScriptCore/tools/JSDollarVM.cpp: (JSC::JSDollarVM::finishCreation):
|
Preview build of 42d31a6: |
|
@robobun wake up!! |
|
Status:
One decision is needed from a maintainer. After the bun change,
If those forms must keep their result, this change is the wrong tool, because Edited: the first version of this comment said that all of these forms fail an assertion on a debug build. The literal |
…t a type-info flag The flag ReportsNoConstructData added a branch to InternalFunction::getConstructData, which every native constructor runs, and it used one of the four free type-info bits. The method table says the same with neither cost. Everything that reads the construct data of an InternalFunction goes through the method table, so a subclass that declares `using JSCell::getConstructData;` reports no construct data. m_functionForConstruct stays installed for `new f()`. The constructor of InternalFunction asserted that no subclass changes that slot, because compiler threads call it. The assertion now accepts JSCell::getConstructData, which returns an empty CallData and reads nothing. * JSTests/stress/internal-function-new-without-construct-data.js: * Source/JavaScriptCore/runtime/InternalFunction.cpp: (JSC::InternalFunction::InternalFunction): (JSC::InternalFunction::getConstructData): * Source/JavaScriptCore/runtime/JSCellInlines.h: (JSC::JSCell::isConstructorWithConcurrency): * Source/JavaScriptCore/runtime/JSTypeInfo.h: (JSC::TypeInfo::reportsNoConstructData const): Deleted. * Source/JavaScriptCore/tools/JSDollarVM.cpp:
new handler and report no construct datanew handler and report no construct data
In that build a subclass of InternalFunction may take JSCell::getConstructData. No class in bun does it at this commit.
|
Closing: #757 decided this the other way. A host constructor may return a primitive now. The bun side, oven-sh/bun#44226, is closed too. Its last comment has the numbers on main: 10 of the 12 aborts are gone. The 2 that are left are |
Draft. The bun side is oven-sh/bun#44226, pinned to the preview build here.
Problem
CStringmust return a string primitive fromnew CString(ptr)(bun:ffi: use the engine-native FFI when available bun#35246). As an InternalFunction it is a constructor to every engine algorithm, and those trust[[Construct]]to return an object.Array.from.call(CString, [1, 2, 3])aborts a release build:RELEASE_ASSERT(baseValue.isObject())inllint_slow_path_put_by_val_direct.Fix
InternalFunctionmay putJSCell::getConstructDatain its method table (using JSCell::getConstructData;). It then reports no construct data, andm_functionForConstructstays installed.InternalFunctionasserted that no subclass changes that slot. The assertion now accepts this one function.new f()andsuper()read no construct data on an InternalFunction:setUpCall,linkForandvirtualForjump to a trampoline that loadsm_functionForConstruct. Every other construct asks the method table first.JSTests/stress/internal-function-new-without-construct-data.js, debug ASANjsc, 7 tier configurations.Background
isConstructor(),construct(),Reflect.construct,bind,Proxy,Array.of,Array.from,extendsand species read construct data through the method table.JSCell::getConstructDatareturns an emptyCallDataand reads nothing.getConstructDatafor every native constructor, and one of 4 free type-info bits.Downsides
InternalFunction::getConstructDatahas the same 13 instructions before and after, and the release text of bun has the same size (see Notes).super()of a class whose[[Prototype]]is such a function still runs the handler. The handler must checknew.target.Notes
Why not an
isObjectcheck inInterpreter::executeConstruct. It runs after the handler has read the length as a pointer, andArray.fromis a JS builtin that never gets there.The test helper.
$vm.createInternalFunctionWithoutConstructData()makes a function of a class that takesJSCell::getConstructData. Its call handler returns"call:<argument count>"and its construct handler returns"new:<argument count>", or throws a TypeError whennew.targetis not the function.JSDollarVMis compiled into the library only when assertions are on, so a release build of bun does not have it.Tier configurations run (local build of this branch,
jsc, Debug with ASAN, Linux x64), all with--validateExceptionChecks=true: default with FTL,--useLLInt=false,--useJIT=false,--useFTLJIT=false, DFG eager with--useConcurrentJIT=falseand graph validation, FTL eager with--useConcurrentJIT=falseand graph validation,--forceOSRExitToLLInt=true.Without the change to the assertion a build with assertions stops when the helper makes its function:
ASSERTION FAILED: methodTable()->getConstructData == InternalFunction::info()->methodTable.getConstructData.Existing tests run on the same build, unchanged, all pass:
is-constructor.js,is-constructor-should-be-correct-in-ai.js,internal-function-construct.js,internal-function-call.js,reflect-construct.js,proxy-construct.js,array-of-non-array-constructor.js,array-species-functions.js,array-from-with-iterable.js,class-subclassing-function.js.Release builds, measured (Linux x64).
nm -Sandobjdump -don the ltojscoff20ce774and of42d31a6b:InternalFunction::getConstructDatais 41 bytes and the same 13 instructions in both. A release build of bun has 80,692,188 bytes of text with each of the two packages, and noJSDollarVMsymbol. Thejscshell has$vmin every build, so its lto text grows from 47,724,559 to 47,726,911 bytes.The flag version, measured. gdb single-stepped one call of
InternalFunction::getConstructDatain the ltojscoff20ce774and of the first commit of this PR: 13 to 18 instructions for a function that is a constructor, 41 to 61 bytes.What this does not cover. A native function made with
JSFunction::createdoes not read its construct data from the method table (getConstructDataInlineinJSObjectInlines.hgoes toJSFunction::getConstructDatafor aJSFunctionTypecell). The functions of oven-sh/bun#34575 are of that kind.