From 3ad19d9e49d5daa1095b9c0b7a88731108bb909a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 1 Sep 2026 02:23:42 +0000 Subject: [PATCH] Print every native function's source on one line #155 changed the InternalFunction and callable-object branches of Function.prototype.toString to the V8 shape: function Map() { [native code] } Host functions (NativeExecutable::toStringSlow), builtin functions written in JavaScript (FunctionExecutable::toStringSlow), bound functions and remote functions (JSFunction::toString) still printed the old three-line shape: function hasOwnProperty() { [native code] } lodash's isNative() builds a RegExp from the source of Object.prototype.hasOwnProperty and tests Map, Set, WeakMap, DataView and Promise against it. With two shapes in one engine, the RegExp did not match, lodash decided Map was not native, and MapCache and Stack fell back to ListCache, a linear scan. cloneDeep, memoize, uniq and isEqual became quadratic under Bun. json-schema-to-typescript measured cloneDeep of a 10 000 definition schema at 11 s under Bun and 76 ms under node (bcherny/json-schema-to-typescript#871). This changes the remaining four sites to the one-line shape, so all native functions print the same way, and updates the inspector's native-getter sniff and the testapi.mm expectation to match. JSTests/stress/function-toString-native-one-line.js pins the shape for each kind of function, re-runs lodash's RegExp, and walks the global object to check that no native function prints on more than one line. --- .../function-toString-native-one-line.js | 109 ++++++++++++++++++ Source/JavaScriptCore/API/tests/testapi.mm | 2 +- .../inspector/InjectedScriptSource.js | 2 +- .../runtime/FunctionExecutable.cpp | 2 +- Source/JavaScriptCore/runtime/JSFunction.cpp | 4 +- .../runtime/NativeExecutable.cpp | 2 +- 6 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 JSTests/stress/function-toString-native-one-line.js diff --git a/JSTests/stress/function-toString-native-one-line.js b/JSTests/stress/function-toString-native-one-line.js new file mode 100644 index 000000000000..c75d6da76fc4 --- /dev/null +++ b/JSTests/stress/function-toString-native-one-line.js @@ -0,0 +1,109 @@ +// Every kind of native function prints the same single-line NativeFunction +// source: `function name() { [native code] }`. This is the shape V8 uses. +// +// Before this, InternalFunction subclasses (Map, Set, WeakMap, DataView, ...) +// printed one line while host functions (NativeExecutable), builtin functions +// (FunctionExecutable), bound functions and remote functions printed +// function name() {\n [native code]\n} +// lodash's isNative() builds a RegExp from Object.prototype.hasOwnProperty's +// source and tests Map against it, so the two shapes made lodash believe Map +// was not native and fall back to a linear-scan cache in cloneDeep/memoize. + +function shouldBe(actual, expected) { + if (actual !== expected) + throw new Error(`bad value: ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`); +} + +const toString = Function.prototype.toString; +const source = (fn) => toString.call(fn); +const nativeFunction = /^function [^(]*\(\) \{ \[native code\] \}$/; + +// Host functions (NativeExecutable). +shouldBe(source(Object.prototype.hasOwnProperty), "function hasOwnProperty() { [native code] }"); +shouldBe(source(Array.prototype.push), "function push() { [native code] }"); +shouldBe(source(Object.create), "function create() { [native code] }"); +shouldBe(source(Math.max), "function max() { [native code] }"); +shouldBe(source(Symbol.for), "function for() { [native code] }"); + +// Constructors that are InternalFunction subclasses. +shouldBe(source(Map), "function Map() { [native code] }"); +shouldBe(source(Set), "function Set() { [native code] }"); +shouldBe(source(WeakMap), "function WeakMap() { [native code] }"); +shouldBe(source(DataView), "function DataView() { [native code] }"); +shouldBe(source(Array), "function Array() { [native code] }"); +shouldBe(source(Function), "function Function() { [native code] }"); + +// Constructors that are JSFunction subclasses with a NativeExecutable. +shouldBe(source(Promise), "function Promise() { [native code] }"); +shouldBe(source(Number), "function Number() { [native code] }"); +shouldBe(source(String), "function String() { [native code] }"); +shouldBe(source(Boolean), "function Boolean() { [native code] }"); + +// Builtin functions written in JavaScript (FunctionExecutable). +shouldBe(source(Array.prototype.map), "function map() { [native code] }"); +shouldBe(source(Array.from), "function from() { [native code] }"); +shouldBe(source(Promise.prototype.then), "function then() { [native code] }"); +shouldBe(source(Function.prototype.call), "function call() { [native code] }"); + +// Native accessors keep the `get `/`set ` prefix in the name. +shouldBe(source(Object.getOwnPropertyDescriptor(Map.prototype, "size").get), "function get size() { [native code] }"); +shouldBe(source(Object.getOwnPropertyDescriptor(RegExp.prototype, "flags").get), "function get flags() { [native code] }"); + +// Bound functions. +shouldBe(source(function foo() {}.bind(null)), "function foo() { [native code] }"); +shouldBe(source(Map.bind(null)), "function Map() { [native code] }"); +shouldBe(source(Object.prototype.hasOwnProperty.bind({})), "function hasOwnProperty() { [native code] }"); + +// Callable objects that are neither JSFunction nor InternalFunction. +shouldBe(nativeFunction.test(source(new Proxy(function () {}, {}))), true); +shouldBe(nativeFunction.test(source(new Proxy(Map, {}))), true); + +// The result is cached per executable. The cached string has the same shape. +shouldBe(source(Object.prototype.hasOwnProperty), source(Object.prototype.hasOwnProperty)); +shouldBe(source(Array.prototype.map), source(Array.prototype.map)); + +// lodash's isNative(): a RegExp built from hasOwnProperty's source must match +// every other native function. +const reRegExpChar = /[\\^$.*+?()[\]{}|]/g; +const reIsNative = RegExp("^" + source(Object.prototype.hasOwnProperty).replace(reRegExpChar, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$"); +for (const fn of [Map, Set, WeakMap, WeakRef, DataView, Promise, Symbol, Object.create, Array.prototype.map, Function.prototype.bind, Map.prototype.get, Object.prototype.hasOwnProperty.bind({})]) { + if (!reIsNative.test(source(fn))) + throw new Error(`${fn.name} did not pass the lodash isNative RegExp: ${JSON.stringify(source(fn))}`); +} + +// Everything reachable from the global object that is a native function prints one line. +const seen = new Set(); +function visit(object, path, depth) { + if (object === null || (typeof object !== "object" && typeof object !== "function") || seen.has(object) || depth > 3) + return; + seen.add(object); + if (typeof object === "function") { + const text = source(object); + if (text.includes("[native code]") && !nativeFunction.test(text)) + throw new Error(`${path} prints a native function on more than one line: ${JSON.stringify(text)}`); + } + let keys; + try { + keys = Reflect.ownKeys(object); + } catch { + return; + } + for (const key of keys) { + let descriptor; + try { + descriptor = Reflect.getOwnPropertyDescriptor(object, key); + } catch { + continue; + } + if (!descriptor) + continue; + const name = typeof key === "symbol" ? `[${key.description}]` : key; + if ("value" in descriptor) + visit(descriptor.value, `${path}.${name}`, depth + 1); + if (descriptor.get) + visit(descriptor.get, `${path}.${name}[get]`, depth + 1); + if (descriptor.set) + visit(descriptor.set, `${path}.${name}[set]`, depth + 1); + } +} +visit(globalThis, "globalThis", 0); diff --git a/Source/JavaScriptCore/API/tests/testapi.mm b/Source/JavaScriptCore/API/tests/testapi.mm index e9b623e76a45..daa7ef513997 100644 --- a/Source/JavaScriptCore/API/tests/testapi.mm +++ b/Source/JavaScriptCore/API/tests/testapi.mm @@ -1042,7 +1042,7 @@ static void testObjectiveCAPIMain() JSContext *context = [[JSContext alloc] init]; context[@"TestObject"] = [TestObject class]; JSValue *result = [context evaluateScript:@"String(TestObject)"]; - checkResult(@"String(TestObject)", [result isEqualToObject:@"function TestObject() {\n [native code]\n}"]); + checkResult(@"String(TestObject)", [result isEqualToObject:@"function TestObject() { [native code] }"]); } @autoreleasepool { diff --git a/Source/JavaScriptCore/inspector/InjectedScriptSource.js b/Source/JavaScriptCore/inspector/InjectedScriptSource.js index 36aae68bf3fd..11d8b4833507 100644 --- a/Source/JavaScriptCore/inspector/InjectedScriptSource.js +++ b/Source/JavaScriptCore/inspector/InjectedScriptSource.js @@ -806,7 +806,7 @@ let InjectedScript = class InjectedScript extends PrototypelessObjectBase } if (nativeGettersAsValues) { - if (@String(descriptor.get).@endsWith("[native code]\n}") || (!descriptor.get && @Object.@hasOwn(descriptor, "get") && !descriptor.set && @Object.@hasOwn(descriptor, "set"))) { + if (@String(descriptor.get).@endsWith("[native code] }") || (!descriptor.get && @Object.@hasOwn(descriptor, "get") && !descriptor.set && @Object.@hasOwn(descriptor, "set"))) { // Developers may create such a descriptor, so we should be resilient: // let x = {}; Object.defineProperty(x, "p", {get:undefined}); Object.getOwnPropertyDescriptor(x, "p") let fakeDescriptor = createFakeValueDescriptor(name, symbol, descriptor, isOwnProperty, true); diff --git a/Source/JavaScriptCore/runtime/FunctionExecutable.cpp b/Source/JavaScriptCore/runtime/FunctionExecutable.cpp index ec5cb08081c1..221d1ac648c2 100644 --- a/Source/JavaScriptCore/runtime/FunctionExecutable.cpp +++ b/Source/JavaScriptCore/runtime/FunctionExecutable.cpp @@ -189,7 +189,7 @@ JSString* FunctionExecutable::toStringSlow(JSGlobalObject* globalObject) #else if (isBuiltinFunction()) #endif - return cacheIfNoException(jsMakeNontrivialString(globalObject, "function "_s, name().string(), "() {\n [native code]\n}"_s)); + return cacheIfNoException(jsMakeNontrivialString(globalObject, "function "_s, name().string(), "() { [native code] }"_s)); if (isClass()) return cache(jsString(vm, classSource().view())); diff --git a/Source/JavaScriptCore/runtime/JSFunction.cpp b/Source/JavaScriptCore/runtime/JSFunction.cpp index 69b675d2d353..1ee18f03b633 100644 --- a/Source/JavaScriptCore/runtime/JSFunction.cpp +++ b/Source/JavaScriptCore/runtime/JSFunction.cpp @@ -248,13 +248,13 @@ JSString* JSFunction::toString(JSGlobalObject* globalObject) if (inherits()) { JSBoundFunction* function = uncheckedDowncast(this); auto scope = DECLARE_THROW_SCOPE(vm); - JSValue string = jsMakeNontrivialString(globalObject, "function "_s, function->nameString(vm), "() {\n [native code]\n}"_s); + JSValue string = jsMakeNontrivialString(globalObject, "function "_s, function->nameString(vm), "() { [native code] }"_s); RETURN_IF_EXCEPTION(scope, nullptr); return asString(string); } else if (inherits()) { JSRemoteFunction* function = uncheckedDowncast(this); auto scope = DECLARE_THROW_SCOPE(vm); - JSValue string = jsMakeNontrivialString(globalObject, "function "_s, function->nameString(), "() {\n [native code]\n}"_s); + JSValue string = jsMakeNontrivialString(globalObject, "function "_s, function->nameString(), "() { [native code] }"_s); RETURN_IF_EXCEPTION(scope, nullptr); return asString(string); } diff --git a/Source/JavaScriptCore/runtime/NativeExecutable.cpp b/Source/JavaScriptCore/runtime/NativeExecutable.cpp index 18c154a2ff20..ea38d1578827 100644 --- a/Source/JavaScriptCore/runtime/NativeExecutable.cpp +++ b/Source/JavaScriptCore/runtime/NativeExecutable.cpp @@ -108,7 +108,7 @@ JSString* NativeExecutable::toStringSlow(JSGlobalObject *globalObject) auto throwScope = DECLARE_THROW_SCOPE(vm); - JSValue value = jsMakeNontrivialString(globalObject, "function "_s, name(), "() {\n [native code]\n}"_s); + JSValue value = jsMakeNontrivialString(globalObject, "function "_s, name(), "() { [native code] }"_s); RETURN_IF_EXCEPTION(throwScope, nullptr);