diff --git a/docs/embedding.md b/docs/embedding.md index 6deaf916c..e2a86054b 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -876,7 +876,7 @@ Raises `TGocciaInstructionLimitError` when the limit is reached. A value of zero In bytecode mode the VM uses a trampoline: bytecode-to-bytecode calls are dispatched iteratively via an explicit frame stack, so the Pascal call stack stays flat regardless of JS call depth. The interpreter mode uses Pascal recursion and relies on the depth check to prevent overflow. -Every call of a function written in script counts as one nested call, however it is made: a plain or method call, `new` and `super()`, a getter or setter, a Proxy trap, a `toString`, `valueOf` or `Symbol.toPrimitive` method, an iterator's `next`, or a callback a built-in calls. A built-in function itself adds no frame, so `[n].map(f)` and the `f` it calls count as one. The interpreter does not count `super()` calls, accessors, Proxy traps, `ToPrimitive` methods or iterator methods. Most of those calls are native re-entries into the bytecode VM: the bytecode loop runs on a fresh Pascal stack frame instead of the trampoline, and each one costs a few kilobytes of native stack. How many fit depends on the thread's stack size and on the build. Once less than `NATIVE_STACK_RESERVE` (256 KiB, or a quarter of a stack smaller than 1 MiB) of the thread's stack is left, the VM throws the same `RangeError` instead of entering again, so no `--max-stack` value, including 0, lets these re-entries overflow the native stack. The check runs only where the bytecode loop is entered: a chain of native calls, such as Proxies forwarding to their targets or bound functions calling theirs, is not checked against the stack in between, and the delegation caps below bound only some of those chains. The bounds come from the operating system (`/proc/self/maps` and `RLIMIT_STACK` on Linux, `pthread_get_stackaddr_np` on Darwin, `GetCurrentThreadStackLimits` on Windows) and from the RTL's `StackBottom` where that is higher. On Linux the main thread's stack is taken to grow down to `RLIMIT_STACK` even where its mapping is not marked `[stack]`, as under Valgrind. The first `NATIVE_REENTRY_UNCHECKED_DEPTH` (8) nested re-entries are not checked, so a program that does not nest them never looks the bounds up. Where they cannot be found, at most `MAX_NATIVE_REENTRY_DEPTH` (512) re-entries are live at once. A production x86-64 build on an 8 MiB stack, the Linux and macOS main-thread default and the test runner's worker size, fits 2 200 nested constructor, async-function, generator-`next()` or getter calls. A setter, a Proxy trap, a `toString` method or a callback from `sort`, `JSON.stringify` or `Reflect.construct` costs more, and recursion through one stops at between about 1 600 and 2 100 calls. Property lookups walk a chain of ordinary objects in a loop, with no length limit. Constructing a class walks the classes above it whose constructors are default ones in a loop as well, with no length limit. A Proxy in the chain, a Proxy's target, a Proxy's trap and a handler that is itself a Proxy are entered by native calls, for every internal method a Proxy forwards (`[[GetPrototypeOf]]`, `[[SetPrototypeOf]]`, `[[IsExtensible]]`, `[[PreventExtensions]]`, `[[GetOwnProperty]]`, `[[DefineOwnProperty]]`, `[[HasProperty]]`, `[[Get]]`, `[[Set]]`, `[[Delete]]`, `[[OwnPropertyKeys]]`, `[[Call]]` and `[[Construct]]`); at most `MAX_PROPERTY_DELEGATION_DEPTH` (2 000) of them are live at once, and past that the operation throws the same `RangeError`. A bound function between two Proxies is not counted, so a chain that alternates Proxies with bound functions, or a trap wrapped in several bound functions, can still exhaust the native stack. `typeof` and the callable check step through directly nested Proxies in a loop, since they cannot throw. Arrays, functions, classes and class instances in the chain are entered by native calls too, counted against `MAX_OBJECT_DELEGATION_DEPTH` (3 000). A prototype cycle closed through a Proxy, which ES2026 §10.1.2.1 OrdinarySetPrototypeOf accepts, ends this way. `instanceof` and `Object.prototype.isPrototypeOf` follow `[[GetPrototypeOf]]` in a loop instead and throw after `MAX_PROXY_PROTOTYPE_STEPS` (100 000) steps through a Proxy. +Every call of a function written in script counts as one nested call, however it is made: a plain or method call, `new` and `super()`, a getter or setter, a Proxy trap, a `toString`, `valueOf` or `Symbol.toPrimitive` method, an iterator's `next`, or a callback a built-in calls. A built-in function itself adds no frame, so `[n].map(f)` and the `f` it calls count as one. The interpreter does not count `super()` calls, accessors, Proxy traps, `ToPrimitive` methods or iterator methods. Most of those calls are native re-entries into the bytecode VM: the bytecode loop runs on a fresh Pascal stack frame instead of the trampoline, and each one costs a few kilobytes of native stack. How many fit depends on the thread's stack size and on the build. Once less than `NATIVE_STACK_RESERVE` (256 KiB, or a quarter of a stack smaller than 1 MiB) of the thread's stack is left, the VM throws the same `RangeError` instead of entering again, so no `--max-stack` value, including 0, lets these re-entries overflow the native stack. The same check runs in a chain of native calls that does not enter the bytecode loop, such as Proxies forwarding to their targets, once more than `NATIVE_REENTRY_UNCHECKED_DEPTH` of the counted delegations below are live. The bounds come from the operating system (`/proc/self/maps` and `RLIMIT_STACK` on Linux, `pthread_get_stackaddr_np` on Darwin, `GetCurrentThreadStackLimits` on Windows) and from the RTL's `StackBottom` where that is higher. On Linux the main thread's stack is taken to grow down to `RLIMIT_STACK` even where its mapping is not marked `[stack]`, as under Valgrind. The first `NATIVE_REENTRY_UNCHECKED_DEPTH` (8) nested re-entries are not checked, so a program that does not nest them never looks the bounds up. Where they cannot be found, at most `MAX_NATIVE_REENTRY_DEPTH` (512) re-entries are live at once. A production x86-64 build on an 8 MiB stack, the Linux and macOS main-thread default and the test runner's worker size, fits 2 200 nested constructor, async-function, generator-`next()` or getter calls. A setter, a Proxy trap, a `toString` method or a callback from `sort`, `JSON.stringify` or `Reflect.construct` costs more, and recursion through one stops at between about 1 600 and 2 100 calls. Property lookups walk a chain of ordinary objects in a loop, with no length limit. Constructing a class walks the classes above it whose constructors are default ones in a loop as well, with no length limit. A Proxy in the chain, a Proxy's target, a Proxy's trap and a handler that is itself a Proxy are entered by native calls, for every internal method a Proxy forwards (`[[GetPrototypeOf]]`, `[[SetPrototypeOf]]`, `[[IsExtensible]]`, `[[PreventExtensions]]`, `[[GetOwnProperty]]`, `[[DefineOwnProperty]]`, `[[HasProperty]]`, `[[Get]]`, `[[Set]]`, `[[Delete]]`, `[[OwnPropertyKeys]]`, `[[Call]]` and `[[Construct]]`); at most `MAX_PROPERTY_DELEGATION_DEPTH` (2 000) of them are live at once, and past that the operation throws the same `RangeError`. A Proxy's `[[Call]]` or `[[Construct]]` forward to a bound function counts too, and so does its `[[Construct]]` forward to a class, which a class that extends a Proxy reaches from its implicit constructor; so a chain that alternates Proxies with bound functions, or classes that each extend a Proxy of the previous one, end in the same `RangeError`. A chain of bound functions alone is followed in a loop for `[[Call]]` and `[[Construct]]` (ES2026 §10.4.1.1 and §10.4.1.2), with no length limit, while `instanceof` against one counts each bound function, because each step reads the next target's `@@hasInstance`. `typeof`, the callable check and the constructor check step through directly nested Proxies in a loop, since they cannot throw, and the constructor check steps through bound functions between them as well. Arrays, functions, classes and class instances in the chain are entered by native calls too, counted against `MAX_OBJECT_DELEGATION_DEPTH` (3 000). A prototype cycle closed through a Proxy, which ES2026 §10.1.2.1 OrdinarySetPrototypeOf accepts, ends this way. `instanceof` and `Object.prototype.isPrototypeOf` follow `[[GetPrototypeOf]]` in a loop instead and throw after `MAX_PROXY_PROTOTYPE_STEPS` (100 000) steps through a Proxy. ```pascal uses diff --git a/scripts/depth-probe.ts b/scripts/depth-probe.ts index a6a93d6a9..1ed462711 100755 --- a/scripts/depth-probe.ts +++ b/scripts/depth-probe.ts @@ -92,6 +92,24 @@ const bindChain = (depth, fn) => { } return bound; }; +// bind names each level "bound " + its target's name, so these chains hide +// the name to keep their memory linear in the depth. +const namelessBindChain = (depth, fn) => { + let bound = fn; + for (const _ of Array.from({ length: depth })) { + bound = bound.bind(null); + Object.defineProperty(bound, "name", { value: "" }); + } + return bound; +}; +const proxyBindChain = (depth, fn) => { + const handler = { get: (target, key, receiver) => (key === "name" ? undefined : Reflect.get(target, key, receiver)) }; + let f = fn; + for (const _ of Array.from({ length: depth })) { + f = Function.prototype.bind.call(new Proxy(f, handler), null); + } + return f; +}; const describeError = (error) => { if (error !== null && typeof error === "object") { const ctor = error.constructor; @@ -263,6 +281,39 @@ const CATALOG: Probe[] = [ summary: "Call a function bound DEPTH times", body: (d) => `report(() => bindChain(${d}, () => 1)());\n`, }, + { + id: "bind-chain-construct", + growth: "linear", + summary: "new on a class bound DEPTH times", + body: (d) => `report(() => typeof new (namelessBindChain(${d}, class {}))());\n`, + }, + { + id: "bind-chain-instanceof", + growth: "linear", + summary: "instanceof against a class bound DEPTH times (each level reads @@hasInstance)", + body: (d) => `report(() => { const C = class {}; return new C() instanceof namelessBindChain(${d}, C); });\n`, + }, + { + id: "proxy-bind-alternating-call", + growth: "linear", + summary: "Call a chain that alternates a Proxy and a bound function DEPTH times (#1497)", + body: (d) => + `report(() => proxyBindChain(${d}, () => 1)());\n`, + }, + { + id: "proxy-bind-alternating-construct", + growth: "linear", + summary: "new on a chain that alternates a Proxy and a bound function DEPTH times (#1497)", + body: (d) => + `report(() => typeof new (proxyBindChain(${d}, class {}))());\n`, + }, + { + id: "class-extends-proxy-chain", + growth: "linear", + summary: "Construct the most derived of DEPTH classes that each extend a Proxy of the previous one (#1497)", + body: (d) => + `report(() => { let C = class {}; for (const _ of Array.from({ length: ${d} })) { const B = new Proxy(C, {}); C = class extends B {}; } return typeof new C(); });\n`, + }, { id: "class-extends-chain", growth: "linear", diff --git a/source/units/Goccia.Builtins.TestingLibrary.pas b/source/units/Goccia.Builtins.TestingLibrary.pas index 99bd749e0..ca4764e4b 100644 --- a/source/units/Goccia.Builtins.TestingLibrary.pas +++ b/source/units/Goccia.Builtins.TestingLibrary.pas @@ -61,7 +61,7 @@ TGocciaTestSuite = class(TGocciaRegisteredEntry) const AIsSkipped: Boolean = False; const AIsFocused: Boolean = False); destructor Destroy; override; procedure AddEntry(const AEntry: TGocciaRegisteredEntry); - procedure AddHook(const ACallback: TGocciaFunctionBase; const APhase: TGocciaTestHookPhase); + procedure AddHook(const ACallback: TGocciaValue; const APhase: TGocciaTestHookPhase); procedure ClearRegisteredContent; function DisplayName: string; override; function GetFullName: string; override; @@ -1138,7 +1138,7 @@ procedure TGocciaTestSuite.AddEntry(const AEntry: TGocciaRegisteredEntry); Entries.Add(AEntry); end; -procedure TGocciaTestSuite.AddHook(const ACallback: TGocciaFunctionBase; +procedure TGocciaTestSuite.AddHook(const ACallback: TGocciaValue; const APhase: TGocciaTestHookPhase); begin AddTempRootIfNeeded(ACallback); @@ -3721,11 +3721,11 @@ procedure TGocciaTestAssertions.RegisterHook( begin TGocciaArgumentValidator.RequireExactly(AArgs, 1, AHookName, ThrowError); - if not (AArgs.GetElement(0) is TGocciaFunctionBase) then + // Any callable, a callable Proxy included: RunCallbacks dispatches it. + if not AArgs.GetElement(0).IsCallable then Goccia.Values.ErrorHelper.ThrowTypeError(Format(SErrorFunctionExpectsFunctionArg, [AHookName]), SSuggestTestUsage); - GetCurrentRegistrationSuite.AddHook(TGocciaFunctionBase(AArgs.GetElement(0)), - APhase); + GetCurrentRegistrationSuite.AddHook(AArgs.GetElement(0), APhase); end; function TGocciaTestAssertions.GetEachRowArguments( @@ -4642,7 +4642,8 @@ procedure TGocciaTestAssertions.RunCallbacks(const ACallbacks: TGocciaArgumentsC try try HookFailed := False; - CallbackResult := TGocciaFunctionBase(Callback).Call(EmptyArgs, TGocciaUndefinedLiteralValue.UndefinedValue); + CallbackResult := DispatchCall(Callback, EmptyArgs, + TGocciaUndefinedLiteralValue.UndefinedValue); if (TGarbageCollector.Instance <> nil) then TGarbageCollector.Instance.AddTempRoot(CallbackResult); @@ -5233,7 +5234,8 @@ function TGocciaTestAssertions.OnTestFinished(const AArgs: TGocciaArgumentsColle begin TGocciaArgumentValidator.RequireExactly(AArgs, 1, 'onTestFinished', ThrowError); - if not (AArgs.GetElement(0) is TGocciaFunctionBase) then + // Any callable, a callable Proxy included: RunCallbacks dispatches it. + if not AArgs.GetElement(0).IsCallable then Goccia.Values.ErrorHelper.ThrowTypeError(SErrorOnTestFinishedExpectsFunction, SSuggestTestUsage); AddTempRootIfNeeded(AArgs.GetElement(0)); diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index 8b939d8c6..f75b27fde 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -1660,7 +1660,7 @@ function InvokeConstructableWithReceiver(const AConstructor: TGocciaValue; const AContext: TGocciaEvaluationContext; const ANewTarget: TGocciaValue = nil): TGocciaValue; var - BoundFunction: TGocciaBoundFunctionValue; + BoundTarget, BoundThis: TGocciaValue; ClassConstructor: TGocciaClassValue; CombinedArgs: TGocciaArgumentsCollection; SuperResult: TGocciaValue; @@ -1741,17 +1741,12 @@ function InvokeConstructableWithReceiver(const AConstructor: TGocciaValue; if AConstructor is TGocciaBoundFunctionValue then begin - BoundFunction := TGocciaBoundFunctionValue(AConstructor); - if IsSameValue(BoundFunction, EffectiveNewTarget) then - EffectiveNewTarget := BoundFunction.OriginalFunction; - CombinedArgs := TGocciaArgumentsCollection.CreateWithCapacity( - BoundFunction.BoundArgCount + AArguments.Length); + // ES2026 §10.4.1.2: a chain of bound functions collapses in a loop. + CombinedArgs := CollapseBoundFunctionChain( + TGocciaBoundFunctionValue(AConstructor), AArguments, + EffectiveNewTarget, BoundTarget, BoundThis); try - for I := 0 to BoundFunction.BoundArgCount - 1 do - CombinedArgs.Add(BoundFunction.GetBoundArg(I)); - for I := 0 to AArguments.Length - 1 do - CombinedArgs.Add(AArguments.GetElement(I)); - Exit(InvokeConstructableWithReceiver(BoundFunction.OriginalFunction, + Exit(InvokeConstructableWithReceiver(BoundTarget, CombinedArgs, AReceiver, AContext, EffectiveNewTarget)); finally CombinedArgs.Free; @@ -6666,22 +6661,15 @@ function ConstructBoundFunction(const AConstructor: TGocciaBoundFunctionValue; const AContext: TGocciaEvaluationContext): TGocciaValue; var BoundArgs: TGocciaArgumentsCollection; - I: Integer; - Target: TGocciaValue; + Target, BoundThis, NoNewTarget: TGocciaValue; begin - Target := AConstructor.OriginalFunction; - BoundArgs := TGocciaArgumentsCollection.CreateWithCapacity( - AConstructor.BoundArgCount + AArguments.Length); + // ES2026 §10.4.1.2: a chain of bound functions collapses in a loop, so only + // its innermost target, which is not a bound function, is constructed. + NoNewTarget := nil; + BoundArgs := CollapseBoundFunctionChain(AConstructor, AArguments, + NoNewTarget, Target, BoundThis); try - for I := 0 to AConstructor.BoundArgCount - 1 do - BoundArgs.Add(AConstructor.GetBoundArg(I)); - for I := 0 to AArguments.Length - 1 do - BoundArgs.Add(AArguments.GetElement(I)); - - if Target is TGocciaBoundFunctionValue then - Result := ConstructBoundFunction(TGocciaBoundFunctionValue(Target), - BoundArgs, AContext) - else if Target is TGocciaProxyValue then + if Target is TGocciaProxyValue then Result := TGocciaProxyValue(Target).ConstructTrap(BoundArgs) else if Target is TGocciaClassValue then begin @@ -8072,7 +8060,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const Elem.IsStatic, ElementName, ElementKey)); DecoratorArgs.Add(ContextObject); - DecoratorResult := TGocciaFunctionBase(DecoratorFn).Call(DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); + DecoratorResult := DispatchCall(DecoratorFn, DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); if (DecoratorResult <> nil) and not (DecoratorResult is TGocciaUndefinedLiteralValue) then begin @@ -8106,7 +8094,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const DecoratorArgs.Add(GetterFnValue); DecoratorArgs.Add(ContextObject); - DecoratorResult := TGocciaFunctionBase(DecoratorFn).Call(DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); + DecoratorResult := DispatchCall(DecoratorFn, DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); if (DecoratorResult <> nil) and not (DecoratorResult is TGocciaUndefinedLiteralValue) then begin @@ -8114,7 +8102,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const AContext.OnError('Getter decorator must return a function or undefined', ALine, AColumn); if Elem.IsPrivate then - ClassValue.AddPrivateGetter(ElementName, TGocciaFunctionValue(DecoratorResult)) + ClassValue.AddPrivateGetter(ElementName, DecoratorResult) else begin ExistingDescriptor := GetDecoratedAccessorDescriptor( @@ -8149,7 +8137,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const DecoratorArgs.Add(SetterFnValue); DecoratorArgs.Add(ContextObject); - DecoratorResult := TGocciaFunctionBase(DecoratorFn).Call(DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); + DecoratorResult := DispatchCall(DecoratorFn, DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); if (DecoratorResult <> nil) and not (DecoratorResult is TGocciaUndefinedLiteralValue) then begin @@ -8157,7 +8145,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const AContext.OnError('Setter decorator must return a function or undefined', ALine, AColumn); if Elem.IsPrivate then - ClassValue.AddPrivateSetter(ElementName, TGocciaFunctionValue(DecoratorResult)) + ClassValue.AddPrivateSetter(ElementName, DecoratorResult) else begin ExistingDescriptor := GetDecoratedAccessorDescriptor( @@ -8179,7 +8167,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const DecoratorArgs.Add(TGocciaUndefinedLiteralValue.UndefinedValue); DecoratorArgs.Add(ContextObject); - DecoratorResult := TGocciaFunctionBase(DecoratorFn).Call(DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); + DecoratorResult := DispatchCall(DecoratorFn, DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); if (DecoratorResult <> nil) and not (DecoratorResult is TGocciaUndefinedLiteralValue) then begin @@ -8209,7 +8197,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const DecoratorArgs.Add(AutoAccessorValue); DecoratorArgs.Add(ContextObject); - DecoratorResult := TGocciaFunctionBase(DecoratorFn).Call(DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); + DecoratorResult := DispatchCall(DecoratorFn, DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); if (DecoratorResult <> nil) and not (DecoratorResult is TGocciaUndefinedLiteralValue) then begin @@ -8266,7 +8254,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const DecoratorArgs := TGocciaArgumentsCollection.Create([ClassValue, ContextObject]); try - DecoratorResult := TGocciaFunctionBase(DecoratorFn).Call(DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); + DecoratorResult := DispatchCall(DecoratorFn, DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); if (DecoratorResult <> nil) and not (DecoratorResult is TGocciaUndefinedLiteralValue) then begin @@ -8296,7 +8284,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const begin InitArgs := TGocciaArgumentsCollection.Create; try - TGocciaFunctionBase(InitializerResults[I]).Call(InitArgs, ClassValue); + DispatchCall(InitializerResults[I], InitArgs, ClassValue); finally InitArgs.Free; end; @@ -8308,7 +8296,7 @@ function EvaluateClassDefinition(const AClassDef: TGocciaClassDefinition; const begin InitArgs := TGocciaArgumentsCollection.Create; try - TGocciaFunctionBase(InitializerResults[I]).Call(InitArgs, ClassValue); + DispatchCall(InitializerResults[I], InitArgs, ClassValue); finally InitArgs.Free; end; @@ -8693,7 +8681,7 @@ procedure SetRawPrivateInstanceProperty(const AReceiver: TGocciaObjectValue; function EvaluatePrivateMemberOnInstance(const AInstance: TGocciaInstanceValue; const APrivateName: string; const AContext: TGocciaEvaluationContext): TGocciaValue; var AccessClass: TGocciaClassValue; - GetterFn: TGocciaFunctionBase; + GetterFn: TGocciaValue; EmptyArgs: TGocciaArgumentsCollection; PrivateName: string; Descriptor: TGocciaPropertyDescriptor; @@ -8712,7 +8700,7 @@ function EvaluatePrivateMemberOnInstance(const AInstance: TGocciaInstanceValue; GetterFn := AccessClass.PrivatePropertyGetter[PrivateName]; EmptyArgs := TGocciaArgumentsCollection.Create; try - Result := GetterFn.Call(EmptyArgs, AInstance); + Result := DispatchCall(GetterFn, EmptyArgs, AInstance); finally EmptyArgs.Free; end; @@ -8747,8 +8735,8 @@ function EvaluatePrivateMemberOnInstance(const AInstance: TGocciaInstanceValue; begin EmptyArgs := TGocciaArgumentsCollection.Create; try - Result := TGocciaFunctionBase( - TGocciaPropertyDescriptorAccessor(Descriptor).Getter).Call( + Result := DispatchCall( + TGocciaPropertyDescriptorAccessor(Descriptor).Getter, EmptyArgs, AInstance); finally EmptyArgs.Free; @@ -8788,7 +8776,7 @@ function EvaluatePrivateMemberOnObject(const AReceiver: TGocciaObjectValue; const APrivateName: string; const AContext: TGocciaEvaluationContext): TGocciaValue; var AccessClass: TGocciaClassValue; - GetterFn: TGocciaFunctionBase; + GetterFn: TGocciaValue; EmptyArgs: TGocciaArgumentsCollection; PrivateName: string; Descriptor: TGocciaPropertyDescriptor; @@ -8809,7 +8797,7 @@ function EvaluatePrivateMemberOnObject(const AReceiver: TGocciaObjectValue; GetterFn := AccessClass.PrivatePropertyGetter[PrivateName]; EmptyArgs := TGocciaArgumentsCollection.Create; try - Result := GetterFn.Call(EmptyArgs, AReceiver); + Result := DispatchCall(GetterFn, EmptyArgs, AReceiver); finally EmptyArgs.Free; end; @@ -8853,7 +8841,7 @@ function EvaluatePrivateMemberOnClass(const AClassValue: TGocciaClassValue; const APrivateName: string; const AContext: TGocciaEvaluationContext): TGocciaValue; var AccessClass: TGocciaClassValue; - GetterFn: TGocciaFunctionBase; + GetterFn: TGocciaValue; EmptyArgs: TGocciaArgumentsCollection; PrivateName: string; begin @@ -8869,7 +8857,7 @@ function EvaluatePrivateMemberOnClass(const AClassValue: TGocciaClassValue; GetterFn := AccessClass.GetOwnPrivatePropertyGetter(PrivateName); EmptyArgs := TGocciaArgumentsCollection.Create; try - Result := GetterFn.Call(EmptyArgs, AClassValue); + Result := DispatchCall(GetterFn, EmptyArgs, AClassValue); finally EmptyArgs.Free; end; @@ -8891,7 +8879,7 @@ procedure AssignPrivateMemberOnClass(const AClassValue: TGocciaClassValue; const AContext: TGocciaEvaluationContext); var AccessClass: TGocciaClassValue; - SetterFn: TGocciaFunctionBase; + SetterFn: TGocciaValue; SetterArgs: TGocciaArgumentsCollection; PrivateName: string; Descriptor: TGocciaPropertyDescriptor; @@ -8909,7 +8897,7 @@ procedure AssignPrivateMemberOnClass(const AClassValue: TGocciaClassValue; SetterArgs := TGocciaArgumentsCollection.Create; try SetterArgs.Add(AValue); - SetterFn.Call(SetterArgs, AClassValue); + DispatchCall(SetterFn, SetterArgs, AClassValue); finally SetterArgs.Free; end; @@ -8934,7 +8922,7 @@ procedure AssignPrivateMemberOnInstance(const AInstance: TGocciaInstanceValue; const AContext: TGocciaEvaluationContext); var AccessClass: TGocciaClassValue; - SetterFn: TGocciaFunctionBase; + SetterFn: TGocciaValue; SetterArgs: TGocciaArgumentsCollection; PrivateName: string; Descriptor: TGocciaPropertyDescriptor; @@ -8953,7 +8941,7 @@ procedure AssignPrivateMemberOnInstance(const AInstance: TGocciaInstanceValue; SetterArgs := TGocciaArgumentsCollection.Create; try SetterArgs.Add(AValue); - SetterFn.Call(SetterArgs, AInstance); + DispatchCall(SetterFn, SetterArgs, AInstance); finally SetterArgs.Free; end; @@ -8979,8 +8967,8 @@ procedure AssignPrivateMemberOnInstance(const AInstance: TGocciaInstanceValue; SetterArgs := TGocciaArgumentsCollection.Create; try SetterArgs.Add(AValue); - TGocciaFunctionBase( - TGocciaPropertyDescriptorAccessor(Descriptor).Setter).Call( + DispatchCall( + TGocciaPropertyDescriptorAccessor(Descriptor).Setter, SetterArgs, AInstance); finally SetterArgs.Free; @@ -9010,7 +8998,7 @@ procedure AssignPrivateMemberOnObject(const AReceiver: TGocciaObjectValue; const AContext: TGocciaEvaluationContext); var AccessClass: TGocciaClassValue; - SetterFn: TGocciaFunctionBase; + SetterFn: TGocciaValue; SetterArgs: TGocciaArgumentsCollection; PendingNewTarget: TGocciaValue; FieldInitializer: TGocciaExpression; @@ -9049,7 +9037,7 @@ procedure AssignPrivateMemberOnObject(const AReceiver: TGocciaObjectValue; SetterArgs := TGocciaArgumentsCollection.Create; try SetterArgs.Add(AValue); - SetterFn.Call(SetterArgs, AReceiver); + DispatchCall(SetterFn, SetterArgs, AReceiver); finally SetterArgs.Free; end; @@ -10539,7 +10527,7 @@ procedure AssignPrivateMemberValue(const AObjectValue: TGocciaValue; Instance: TGocciaInstanceValue; ClassValue: TGocciaClassValue; AccessClass: TGocciaClassValue; - SetterFn: TGocciaFunctionBase; + SetterFn: TGocciaValue; SetterArgs: TGocciaArgumentsCollection; begin if AObjectValue is TGocciaInstanceValue then @@ -10553,7 +10541,7 @@ procedure AssignPrivateMemberValue(const AObjectValue: TGocciaValue; SetterArgs := TGocciaArgumentsCollection.Create; try SetterArgs.Add(AValue); - SetterFn.Call(SetterArgs, Instance); + DispatchCall(SetterFn, SetterArgs, Instance); finally SetterArgs.Free; end; diff --git a/source/units/Goccia.StackLimit.pas b/source/units/Goccia.StackLimit.pas index cff866a85..2430cfa7a 100644 --- a/source/units/Goccia.StackLimit.pas +++ b/source/units/Goccia.StackLimit.pas @@ -15,11 +15,11 @@ interface // NATIVE_STACK_RESERVE bytes of the stack free and throws the RangeError of // --max-stack instead of entering once less than that remains, so no // --max-stack value, including 0, lets these re-entries overflow the native - // stack. The check runs only where the bytecode loop is entered: a chain of - // native calls (Proxies forwarding to their targets, bound functions calling - // theirs) is not checked in between, and the delegation caps below bound - // only some of those chains. Native re-entries are function calls, and count - // against --max-stack as well. + // stack. A chain of native calls that never enters the bytecode loop + // (Proxies forwarding to their targets, a bound function between them, a + // class extending a Proxy) is counted by the delegation caps below, which + // check the same reserve once they nest. Native re-entries are function + // calls, and count against --max-stack as well. NATIVE_STACK_RESERVE = 256 * 1024; // Where the bounds of the running thread's stack cannot be found, a native @@ -48,8 +48,9 @@ interface // `new Proxy(Reflect.apply, {})`): in a production build on Linux x86-64, // interpreted, under the deepest recursion the default --max-stack allows, // such a nest ran out of native stack after about 2,130 levels. The cap - // stays below that. A trap wrapped in several bound functions costs more - // again and is not covered: three already exhaust the stack there. + // stays below that. A level that costs more again, such as a trap wrapped + // in several bound functions, is stopped before the cap by the native stack + // check EnterPropertyDelegation makes once delegations nest. MAX_PROPERTY_DELEGATION_DEPTH = 2000; // The same walk enters an array, function, class or class instance by a @@ -431,10 +432,34 @@ procedure CheckNativeStackHeadroom(const ADepth: Integer; CheckNativeStackHeadroomSlow(ADepth, ALimit); end; +// Out of line: it runs only once delegations nest. A level costs a different +// amount of native stack depending on what it forwards through (a bound +// function, a native trap, a class's implicit constructor), so the count alone +// does not keep a nest inside the stack when native re-entries already hold +// most of it. Past NATIVE_REENTRY_UNCHECKED_DEPTH nested delegations each one +// also checks the stack, like a native re-entry into the VM does. Where the +// bounds of the stack cannot be found, the count is the only bound. +{$IFDEF FPC}{$OPTIMIZATION NOAUTOINLINE}{$ENDIF} +procedure CheckDelegationNativeStack; +var + Limit: NativeUInt; +begin + Limit := NativeStackLimit; + if (Limit <> 0) and (NativeStackPosition < Limit) then + ThrowMaxCallStackExceeded; +end; +{$IFDEF PRODUCTION} + {$IFDEF FPC} + {$OPTIMIZATION AUTOINLINE} + {$ENDIF} +{$ENDIF} + procedure EnterPropertyDelegation; begin if GPropertyDelegationDepth >= MAX_PROPERTY_DELEGATION_DEPTH then ThrowMaxCallStackExceeded; + if GPropertyDelegationDepth >= NATIVE_REENTRY_UNCHECKED_DEPTH then + CheckDelegationNativeStack; Inc(GPropertyDelegationDepth); end; @@ -447,6 +472,8 @@ procedure EnterObjectDelegation; begin if GObjectDelegationDepth >= MAX_OBJECT_DELEGATION_DEPTH then ThrowMaxCallStackExceeded; + if GObjectDelegationDepth >= NATIVE_REENTRY_UNCHECKED_DEPTH then + CheckDelegationNativeStack; Inc(GObjectDelegationDepth); end; diff --git a/source/units/Goccia.Test262.Host.pas b/source/units/Goccia.Test262.Host.pas index a6ec4b43d..8bdf04bb6 100644 --- a/source/units/Goccia.Test262.Host.pas +++ b/source/units/Goccia.Test262.Host.pas @@ -651,7 +651,8 @@ function TTest262AgentHost.ReceiveBroadcast( until False; CallbackArgs := TGocciaArgumentsCollection.Create([BroadcastValue]); try - Result := TGocciaFunctionBase(Callback).Call(CallbackArgs, + // Any callable, a callable Proxy included. + Result := DispatchCall(Callback, CallbackArgs, TGocciaUndefinedLiteralValue.UndefinedValue); finally CallbackArgs.Free; diff --git a/source/units/Goccia.VM.DispatchCase.inc b/source/units/Goccia.VM.DispatchCase.inc index e2ac2acb1..71178f85d 100644 --- a/source/units/Goccia.VM.DispatchCase.inc +++ b/source/units/Goccia.VM.DispatchCase.inc @@ -3955,7 +3955,8 @@ // Clear B before the call so that if it throws, the follow-up // OP_AWAIT sees null instead of the stale dispose function. FRegisters[B] := RegisterNull; - RightValue := TGocciaFunctionBase(LeftValue).CallNoArgs( + // GetDisposeMethod admits any callable, a callable Proxy too. + RightValue := DispatchCallNoArgs(LeftValue, RegisterToValue(FRegisters[C])); // Store result in B so a follow-up OP_AWAIT can await it if Assigned(RightValue) then diff --git a/source/units/Goccia.VM.pas b/source/units/Goccia.VM.pas index 8016de372..b6d4ba719 100644 --- a/source/units/Goccia.VM.pas +++ b/source/units/Goccia.VM.pas @@ -5662,7 +5662,7 @@ function InvokeConstructableWithReceiver(const AConstructor: TGocciaValue; const AReceiver: TGocciaValue; const ANewTarget: TGocciaValue = nil): TGocciaValue; var - BoundFunction: TGocciaBoundFunctionValue; + BoundTarget, BoundThis: TGocciaValue; ClassConstructor: TGocciaClassValue; CombinedArgs: TGocciaArgumentsCollection; SuperResult: TGocciaValue; @@ -5700,17 +5700,12 @@ function InvokeConstructableWithReceiver(const AConstructor: TGocciaValue; if AConstructor is TGocciaBoundFunctionValue then begin - BoundFunction := TGocciaBoundFunctionValue(AConstructor); - if IsSameValue(BoundFunction, EffectiveNewTarget) then - EffectiveNewTarget := BoundFunction.OriginalFunction; - CombinedArgs := TGocciaArgumentsCollection.CreateWithCapacity( - BoundFunction.BoundArgCount + AArguments.Length); + // ES2026 §10.4.1.2: a chain of bound functions collapses in a loop. + CombinedArgs := CollapseBoundFunctionChain( + TGocciaBoundFunctionValue(AConstructor), AArguments, + EffectiveNewTarget, BoundTarget, BoundThis); try - for I := 0 to BoundFunction.BoundArgCount - 1 do - CombinedArgs.Add(BoundFunction.GetBoundArg(I)); - for I := 0 to AArguments.Length - 1 do - CombinedArgs.Add(AArguments.GetElement(I)); - Exit(InvokeConstructableWithReceiver(BoundFunction.OriginalFunction, + Exit(InvokeConstructableWithReceiver(BoundTarget, CombinedArgs, AReceiver, EffectiveNewTarget)); finally CombinedArgs.Free; @@ -10593,7 +10588,7 @@ function TGocciaVM.ConstructValue(const AConstructor: TGocciaValue; var BytecodeFunction: TGocciaBytecodeFunctionValue; BoundArgs: TGocciaArgumentsCollection; - BoundFunction: TGocciaBoundFunctionValue; + BoundTarget, BoundThis: TGocciaValue; ClassConstructor: TGocciaClassValue; ConstructorName: string; EvalContext: TGocciaEvaluationContext; @@ -10688,18 +10683,13 @@ function TGocciaVM.ConstructValue(const AConstructor: TGocciaValue; if AConstructor is TGocciaBoundFunctionValue then begin - BoundFunction := TGocciaBoundFunctionValue(AConstructor); - if IsSameValue(BoundFunction, EffectiveNewTarget) then - EffectiveNewTarget := BoundFunction.OriginalFunction; - BoundArgs := TGocciaArgumentsCollection.CreateWithCapacity( - BoundFunction.BoundArgCount + AArguments.Length); + // ES2026 §10.4.1.2: a chain of bound functions collapses in a loop, so + // only its innermost target is constructed by a native call. + BoundArgs := CollapseBoundFunctionChain( + TGocciaBoundFunctionValue(AConstructor), AArguments, + EffectiveNewTarget, BoundTarget, BoundThis); try - for I := 0 to BoundFunction.BoundArgCount - 1 do - BoundArgs.Add(BoundFunction.GetBoundArg(I)); - for I := 0 to AArguments.Length - 1 do - BoundArgs.Add(AArguments.GetElement(I)); - Result := ConstructValue(BoundFunction.OriginalFunction, BoundArgs, - EffectiveNewTarget); + Result := ConstructValue(BoundTarget, BoundArgs, EffectiveNewTarget); finally BoundArgs.Free; end; @@ -11027,8 +11017,7 @@ procedure TGocciaVM.DefineStaticGetterProperty(const ATarget: TGocciaValue; PrivateBrandToken := BytecodePrivateTokenForKey(AName, TGocciaClassValue(ATarget).PrivateBrandToken); TGocciaClassValue(ATarget).AddPrivateGetter( - BytecodePrivateRuntimeKey(AName, PrivateBrandToken), - TGocciaFunctionBase(AGetter)); + BytecodePrivateRuntimeKey(AName, PrivateBrandToken), AGetter); end else TGocciaClassValue(ATarget).AddStaticGetter(AName, TGocciaFunctionBase(AGetter)); @@ -11048,8 +11037,7 @@ procedure TGocciaVM.DefineStaticSetterProperty(const ATarget: TGocciaValue; PrivateBrandToken := BytecodePrivateTokenForKey(AName, TGocciaClassValue(ATarget).PrivateBrandToken); TGocciaClassValue(ATarget).AddPrivateSetter( - BytecodePrivateRuntimeKey(AName, PrivateBrandToken), - TGocciaFunctionBase(ASetter)); + BytecodePrivateRuntimeKey(AName, PrivateBrandToken), ASetter); end else TGocciaClassValue(ATarget).AddStaticSetter(AName, TGocciaFunctionBase(ASetter)); @@ -12779,7 +12767,7 @@ procedure TGocciaVM.ApplyClassDecorator(const ADecoratorFn: TGocciaValue); DecoratorArgs.Add(ClassVal); DecoratorArgs.Add(ContextObject); try - DecoratorResult := TGocciaFunctionBase(ADecoratorFn).Call( + DecoratorResult := DispatchCall(ADecoratorFn, DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); finally ReleaseArguments(DecoratorArgs); @@ -13222,7 +13210,7 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; DecoratorArgs.Add(ElementValue); DecoratorArgs.Add(ContextObject); try - DecoratorResult := TGocciaFunctionBase(ADecoratorFn).Call( + DecoratorResult := DispatchCall(ADecoratorFn, DecoratorArgs, TGocciaUndefinedLiteralValue.UndefinedValue); finally ReleaseArguments(DecoratorArgs); @@ -13249,7 +13237,7 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; ThrowTypeError(SErrorGetterDecoratorReturn, SSuggestDecoratorFunction); if IsPrivate then TGocciaClassValue(ClassVal).AddPrivateGetter( - Name, TGocciaFunctionBase(DecoratorResult)) + Name, DecoratorResult) else DefineDecoratedGetterProperty( IsStatic, ElementName, ElementKey, DecoratorResult); @@ -13260,7 +13248,7 @@ procedure TGocciaVM.ApplyElementDecorator(const ADecoratorFn: TGocciaValue; ThrowTypeError(SErrorSetterDecoratorReturn, SSuggestDecoratorFunction); if IsPrivate then TGocciaClassValue(ClassVal).AddPrivateSetter( - Name, TGocciaFunctionBase(DecoratorResult)) + Name, DecoratorResult) else DefineDecoratedSetterProperty( IsStatic, ElementName, ElementKey, DecoratorResult); @@ -13340,7 +13328,7 @@ function TGocciaVM.FinishDecorators(const ACurrentValue: TGocciaValue): TGocciaV begin InitArgs := AcquireArguments; try - TGocciaFunctionBase(InitializerResults[I]).Call(InitArgs, ClassVal); + DispatchCall(InitializerResults[I], InitArgs, ClassVal); finally ReleaseArguments(InitArgs); end; @@ -13351,7 +13339,7 @@ function TGocciaVM.FinishDecorators(const ACurrentValue: TGocciaValue): TGocciaV begin InitArgs := AcquireArguments; try - TGocciaFunctionBase(InitializerResults[I]).Call(InitArgs, ClassVal); + DispatchCall(InitializerResults[I], InitArgs, ClassVal); finally ReleaseArguments(InitArgs); end; @@ -13857,9 +13845,9 @@ function TGocciaVM.GetPropertyValueGeneric(const AObject: TGocciaValue; begin EmptyArgs := TGocciaArgumentsCollection.Create; try - Exit(TGocciaClassValue(AObject).GetOwnPrivatePropertyGetter( - PrivateName).Call( - EmptyArgs, AObject)); + Exit(DispatchCall( + TGocciaClassValue(AObject).GetOwnPrivatePropertyGetter( + PrivateName), EmptyArgs, AObject)); finally EmptyArgs.Free; end; @@ -14109,9 +14097,9 @@ procedure TGocciaVM.SetPropertyValue(const AObject: TGocciaValue; begin SetterArgs := TGocciaArgumentsCollection.Create([AValue]); try - TGocciaClassValue(AObject).GetOwnPrivatePropertySetter( - PrivateName).Call( - SetterArgs, AObject); + DispatchCall( + TGocciaClassValue(AObject).GetOwnPrivatePropertySetter( + PrivateName), SetterArgs, AObject); finally SetterArgs.Free; end; diff --git a/source/units/Goccia.Values.ClassValue.pas b/source/units/Goccia.Values.ClassValue.pas index aac6c89ba..8bca089f6 100644 --- a/source/units/Goccia.Values.ClassValue.pas +++ b/source/units/Goccia.Values.ClassValue.pas @@ -58,8 +58,10 @@ TGocciaClassValue = class(TGocciaObjectValue) FPrivateStaticProperties: TGocciaValueMap; FPrivateStaticMethods: TGocciaValueMap; FPrivateMethods: TOrderedStringMap; - FPrivateGetters: TOrderedStringMap; - FPrivateSetters: TOrderedStringMap; + // A private accessor is any callable: a decorator may replace it with a + // callable Proxy, which is not a TGocciaFunctionBase. + FPrivateGetters: TOrderedStringMap; + FPrivateSetters: TOrderedStringMap; FCreationRealm: TGocciaRealm; // Each declared private name, in declaration order, mapped to its // internal storage key ('' when it was declared without one). Native @@ -112,8 +114,8 @@ TGocciaClassValue = class(TGocciaObjectValue) function GetPropertySetter(const AName: string): TGocciaFunctionBase; {$IFDEF FPC}inline;{$ENDIF} function GetStaticPropertyGetter(const AName: string): TGocciaFunctionBase; {$IFDEF FPC}inline;{$ENDIF} function GetStaticPropertySetter(const AName: string): TGocciaFunctionBase; {$IFDEF FPC}inline;{$ENDIF} - function GetPrivatePropertyGetter(const AName: string): TGocciaFunctionBase; - function GetPrivatePropertySetter(const AName: string): TGocciaFunctionBase; + function GetPrivatePropertyGetter(const AName: string): TGocciaValue; + function GetPrivatePropertySetter(const AName: string): TGocciaValue; procedure MaterializeIntrinsicProperty(const AName: string); public class procedure SetDefaultPrototype(const AProto: TGocciaObjectValue); static; @@ -131,8 +133,8 @@ TGocciaClassValue = class(TGocciaObjectValue) procedure AddSetter(const AName: string; const ASetter: TGocciaFunctionBase); procedure AddStaticGetter(const AName: string; const AGetter: TGocciaFunctionBase); procedure AddStaticSetter(const AName: string; const ASetter: TGocciaFunctionBase); - procedure AddPrivateGetter(const AName: string; const AGetter: TGocciaFunctionBase); - procedure AddPrivateSetter(const AName: string; const ASetter: TGocciaFunctionBase); + procedure AddPrivateGetter(const AName: string; const AGetter: TGocciaValue); + procedure AddPrivateSetter(const AName: string; const ASetter: TGocciaValue); function HasPrivateGetter(const AName: string): Boolean; function HasPrivateSetter(const AName: string): Boolean; function HasOwnPrivateGetter(const AName: string): Boolean; @@ -142,9 +144,9 @@ TGocciaClassValue = class(TGocciaObjectValue) function HasOwnPrivateName(const AName: string): Boolean; function HasPrivateInstanceElements: Boolean; function GetOwnPrivatePropertyGetter( - const AName: string): TGocciaFunctionBase; + const AName: string): TGocciaValue; function GetOwnPrivatePropertySetter( - const AName: string): TGocciaFunctionBase; + const AName: string): TGocciaValue; procedure AddInstanceProperty(const AName: string; const AExpression: TGocciaExpression); procedure AddPrivateInstanceProperty(const AName: string; const AExpression: TGocciaExpression); procedure AddPrivateStaticProperty(const AName: string; const AValue: TGocciaValue); @@ -254,8 +256,8 @@ TGocciaClassValue = class(TGocciaObjectValue) property PropertySetter[const AName: string]: TGocciaFunctionBase read GetPropertySetter; property StaticPropertyGetter[const AName: string]: TGocciaFunctionBase read GetStaticPropertyGetter; property StaticPropertySetter[const AName: string]: TGocciaFunctionBase read GetStaticPropertySetter; - property PrivatePropertyGetter[const AName: string]: TGocciaFunctionBase read GetPrivatePropertyGetter; - property PrivatePropertySetter[const AName: string]: TGocciaFunctionBase read GetPrivatePropertySetter; + property PrivatePropertyGetter[const AName: string]: TGocciaValue read GetPrivatePropertyGetter; + property PrivatePropertySetter[const AName: string]: TGocciaValue read GetPrivatePropertySetter; procedure MarkReferences; override; procedure AddFieldInitializer(const AName: string; const AInitializer: TGocciaValue; const AIsPrivate, AIsStatic: Boolean); @@ -584,6 +586,7 @@ implementation Goccia.InstructionLimit, Goccia.Intrinsics.FunctionObjects, Goccia.Timeout, + Goccia.Utils, Goccia.Values.ArrayBufferValue, Goccia.Values.ArrayValue, Goccia.Values.BigIntValue, @@ -1057,8 +1060,8 @@ constructor TGocciaClassValue.Create(const AName: string; const ASuperClass: TGo FPrivateStaticProperties := TGocciaValueMap.Create; FPrivateStaticMethods := TGocciaValueMap.Create; FPrivateMethods := TOrderedStringMap.Create; - FPrivateGetters := TOrderedStringMap.Create; - FPrivateSetters := TOrderedStringMap.Create; + FPrivateGetters := TOrderedStringMap.Create; + FPrivateSetters := TOrderedStringMap.Create; FDeclaredPrivateNames := TStringStringMap.Create; FNativeSuperConstructor := nil; FClassPrototype := TGocciaObjectValue.Create; @@ -1142,10 +1145,10 @@ procedure TGocciaClassValue.MarkReferences; for PrivateMethodPair in FPrivateMethods do PrivateMethodPair.Value.MarkReferences; - for FuncPair in FPrivateGetters do - FuncPair.Value.MarkReferences; - for FuncPair in FPrivateSetters do - FuncPair.Value.MarkReferences; + for ValPair in FPrivateGetters do + ValPair.Value.MarkReferences; + for ValPair in FPrivateSetters do + ValPair.Value.MarkReferences; // Mark decorator initializer arrays for I := 0 to High(FMethodInitializers) do @@ -1309,13 +1312,13 @@ procedure TGocciaClassValue.AddStaticSetter(const AName: string; const ASetter: ExistingGetter, ASetter, [pfConfigurable])); end; -procedure TGocciaClassValue.AddPrivateGetter(const AName: string; const AGetter: TGocciaFunctionBase); +procedure TGocciaClassValue.AddPrivateGetter(const AName: string; const AGetter: TGocciaValue); begin FPrivateGetters.AddOrSetValue(AName, AGetter); Inc(FPrivateNamesVersion); end; -procedure TGocciaClassValue.AddPrivateSetter(const AName: string; const ASetter: TGocciaFunctionBase); +procedure TGocciaClassValue.AddPrivateSetter(const AName: string; const ASetter: TGocciaValue); begin FPrivateSetters.AddOrSetValue(AName, ASetter); Inc(FPrivateNamesVersion); @@ -1377,20 +1380,20 @@ function TGocciaClassValue.HasPrivateInstanceElements: Boolean; end; function TGocciaClassValue.GetOwnPrivatePropertyGetter( - const AName: string): TGocciaFunctionBase; + const AName: string): TGocciaValue; begin if not FPrivateGetters.TryGetValue(AName, Result) then Result := nil; end; function TGocciaClassValue.GetOwnPrivatePropertySetter( - const AName: string): TGocciaFunctionBase; + const AName: string): TGocciaValue; begin if not FPrivateSetters.TryGetValue(AName, Result) then Result := nil; end; -function TGocciaClassValue.GetPrivatePropertyGetter(const AName: string): TGocciaFunctionBase; +function TGocciaClassValue.GetPrivatePropertyGetter(const AName: string): TGocciaValue; begin if FPrivateGetters.TryGetValue(AName, Result) then Exit; @@ -1400,7 +1403,7 @@ function TGocciaClassValue.GetPrivatePropertyGetter(const AName: string): TGocci Result := nil; end; -function TGocciaClassValue.GetPrivatePropertySetter(const AName: string): TGocciaFunctionBase; +function TGocciaClassValue.GetPrivatePropertySetter(const AName: string): TGocciaValue; begin if FPrivateSetters.TryGetValue(AName, Result) then Exit; @@ -1531,7 +1534,7 @@ procedure TGocciaClassValue.RunMethodInitializers(const AInstance: TGocciaValue) begin Args := TGocciaArgumentsCollection.Create; try - TGocciaFunctionBase(FMethodInitializers[Idx]).Call(Args, AInstance); + DispatchCall(FMethodInitializers[Idx], Args, AInstance); finally Args.Free; end; @@ -1547,7 +1550,7 @@ procedure TGocciaClassValue.RunFieldInitializers(const AInstance: TGocciaValue); begin Args := TGocciaArgumentsCollection.Create; try - TGocciaFunctionBase(FFieldInitializers[Idx]).Call(Args, AInstance); + DispatchCall(FFieldInitializers[Idx], Args, AInstance); finally Args.Free; end; @@ -1586,7 +1589,8 @@ procedure TGocciaClassValue.RunDecoratorFieldInitializers(const AInstance: TGocc Args := TGocciaArgumentsCollection.Create([OriginalValue]); try - InitResult := TGocciaFunctionBase(FDecoratorFieldInitializers[Idx].Initializer).Call(Args, AInstance); + InitResult := DispatchCall(FDecoratorFieldInitializers[Idx].Initializer, + Args, AInstance); if Assigned(InitResult) and not (InitResult is TGocciaUndefinedLiteralValue) then begin if PropertyKey is TGocciaSymbolValue then @@ -1634,8 +1638,8 @@ procedure TGocciaClassValue.RunDecoratorStaticFieldInitializers; Args := TGocciaArgumentsCollection.Create([OriginalValue]); try - InitResult := TGocciaFunctionBase( - FStaticDecoratorFieldInitializers[Idx].Initializer).Call(Args, Self); + InitResult := DispatchCall( + FStaticDecoratorFieldInitializers[Idx].Initializer, Args, Self); if Assigned(InitResult) and not (InitResult is TGocciaUndefinedLiteralValue) then begin if FStaticDecoratorFieldInitializers[Idx].IsPrivate then @@ -3089,8 +3093,16 @@ function TGocciaInstanceValue.GetPropertyWithContext(const AName: string; const begin Args := TGocciaArgumentsCollection.CreateWithCapacity(0); try - Result := TGocciaFunctionBase(TGocciaPropertyDescriptorAccessor(Descriptor).Getter) - .Call(Args, AThisContext); + // A getter is any callable, a callable Proxy included; only a + // function object takes the direct path. + if TGocciaPropertyDescriptorAccessor(Descriptor).Getter is TGocciaFunctionBase then + Result := TGocciaFunctionBase( + TGocciaPropertyDescriptorAccessor(Descriptor).Getter).Call( + Args, AThisContext) + else + Result := InvokeCallable( + TGocciaPropertyDescriptorAccessor(Descriptor).Getter, Args, + AThisContext); finally Args.Free; end; @@ -3144,13 +3156,21 @@ procedure TGocciaInstanceValue.AssignProperty(const AName: string; const AValue: if Assigned(Descriptor) then begin if (Descriptor is TGocciaPropertyDescriptorAccessor) and - Assigned(TGocciaPropertyDescriptorAccessor(Descriptor).Setter) then + Assigned(TGocciaPropertyDescriptorAccessor(Descriptor).Setter) and + TGocciaPropertyDescriptorAccessor(Descriptor).Setter.IsCallable then begin Args := TGocciaArgumentsCollection.CreateWithCapacity(1); try Args.Add(AValue); - TGocciaFunctionBase(TGocciaPropertyDescriptorAccessor(Descriptor).Setter) - .Call(Args, Self); + // A setter is any callable, a callable Proxy included; only a + // function object takes the direct path. + if TGocciaPropertyDescriptorAccessor(Descriptor).Setter is TGocciaFunctionBase then + TGocciaFunctionBase( + TGocciaPropertyDescriptorAccessor(Descriptor).Setter).Call( + Args, Self) + else + InvokeCallable(TGocciaPropertyDescriptorAccessor(Descriptor).Setter, + Args, Self); finally Args.Free; end; diff --git a/source/units/Goccia.Values.FunctionBase.pas b/source/units/Goccia.Values.FunctionBase.pas index 8138aa985..eaf4f8267 100644 --- a/source/units/Goccia.Values.FunctionBase.pas +++ b/source/units/Goccia.Values.FunctionBase.pas @@ -149,6 +149,20 @@ TGocciaBoundFunctionValue = class(TGocciaFunctionBase) function DispatchCall(const ACallee: TGocciaValue; const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; +// Call(F, V) with no arguments, for any callable F. +function DispatchCallNoArgs(const ACallee, AThisValue: TGocciaValue): TGocciaValue; +// ES2026 §10.4.1.1 [[Call]] and §10.4.1.2 [[Construct]] of a bound function +// forward to its [[BoundTargetFunction]] with its [[BoundArguments]] in front. +// A target that is itself a bound function does the same again, so a chain of +// them is walked here in a loop instead of by a native call per level. +// ATarget receives the innermost target, which is not a bound function, and +// ABoundThis the innermost [[BoundThis]]. ANewTarget gets the §10.4.1.2 +// step 5 substitution at every level; pass nil for a call. The result holds +// every level's bound arguments, innermost first, then AArguments; the caller +// frees it. +function CollapseBoundFunctionChain(const ABound: TGocciaBoundFunctionValue; + const AArguments: TGocciaArgumentsCollection; var ANewTarget: TGocciaValue; + out ATarget, ABoundThis: TGocciaValue): TGocciaArgumentsCollection; // ES2026 §13.10.2 InstanceofOperator(value, target) function InstanceofOperatorResult(const AInstance, ATarget: TGocciaValue): Boolean; // ES2026 §7.3.21 OrdinaryHasInstance(constructor, instance) @@ -362,18 +376,24 @@ function GetFunctionPrototypeFromConstructor(const ANewTarget: TGocciaValue; end; function GetFunctionRealm(const AValue: TGocciaValue): TGocciaRealm; +var + Current: TGocciaValue; begin - if AValue is TGocciaBoundFunctionValue then - Exit(GetFunctionRealm(TGocciaBoundFunctionValue(AValue).OriginalFunction)); + // ES2026 §7.3.24 step 2: a bound function answers with its target's realm. + // A loop, so a long chain takes no stack; the proxy hook steps through + // Proxies and bound functions alike. + Current := AValue; + while Current is TGocciaBoundFunctionValue do + Current := TGocciaBoundFunctionValue(Current).OriginalFunction; - if AValue is TGocciaFunctionBase then - Exit(TGocciaFunctionBase(AValue).CreationRealm); + if Current is TGocciaFunctionBase then + Exit(TGocciaFunctionBase(Current).CreationRealm); - if AValue is TGocciaClassValue then - Exit(TGocciaClassValue(AValue).CreationRealm); + if Current is TGocciaClassValue then + Exit(TGocciaClassValue(Current).CreationRealm); - if IsRegisteredProxyValue(AValue) and Assigned(GProxyGetFunctionRealmHook) then - Exit(GProxyGetFunctionRealmHook(AValue)); + if IsRegisteredProxyValue(Current) and Assigned(GProxyGetFunctionRealmHook) then + Exit(GProxyGetFunctionRealmHook(Current)); Result := nil; end; @@ -541,9 +561,20 @@ function OrdinaryHasInstance(const AConstructor, AInstance: TGocciaValue): Boole if not IsCallableForHasInstance(AConstructor) then Exit(False); + // ES2026 §7.3.21 step 2: InstanceofOperator(O, BC) on the bound target. It + // reads the target's @@hasInstance, which guest code can observe, so a + // chain of bound functions cannot be collapsed here; each step is counted + // like a forward to a Proxy target instead (MAX_PROPERTY_DELEGATION_DEPTH). if AConstructor is TGocciaBoundFunctionValue then - Exit(InstanceofOperatorResult(AInstance, - TGocciaBoundFunctionValue(AConstructor).OriginalFunction)); + begin + EnterPropertyDelegation; + try + Exit(InstanceofOperatorResult(AInstance, + TGocciaBoundFunctionValue(AConstructor).OriginalFunction)); + finally + LeavePropertyDelegation; + end; + end; if not (AInstance is TGocciaObjectValue) then Exit(False); @@ -952,10 +983,18 @@ function TGocciaFunctionBase.IsCallable: Boolean; end; function TGocciaFunctionBase.IsConstructable: Boolean; -begin - if (Self is TGocciaBoundFunctionValue) and - Assigned(TGocciaBoundFunctionValue(Self).OriginalFunction) then - Exit(TGocciaBoundFunctionValue(Self).OriginalFunction.IsConstructable); +var + Target: TGocciaValue; +begin + // ES2026 §10.4.1.3 BoundFunctionCreate step 6: a bound function has + // [[Construct]] when its target does. A chain of bound functions is walked + // in a loop; a Proxy at the end steps through the rest the same way. + Target := Self; + while (Target is TGocciaBoundFunctionValue) and + Assigned(TGocciaBoundFunctionValue(Target).OriginalFunction) do + Target := TGocciaBoundFunctionValue(Target).OriginalFunction; + if Target <> Self then + Exit(Target.IsConstructable); Result := HasOwnProperty(PROP_PROTOTYPE); end; @@ -1161,6 +1200,8 @@ function TGocciaFunctionSharedPrototype.RestrictedFunctionPropertyThrow( function DispatchCall(const ACallee: TGocciaValue; const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; +var + Roots: TGocciaActiveRootFrame; begin if ACallee is TGocciaFunctionBase then Result := TGocciaFunctionBase(ACallee).Call(AArgs, AThisValue) @@ -1169,11 +1210,81 @@ function DispatchCall(const ACallee: TGocciaValue; else if ACallee is TGocciaClassValue then Result := TGocciaClassValue(ACallee).Call(AArgs, AThisValue) else if IsRegisteredProxyValue(ACallee) and ACallee.IsCallable then - Result := GProxyApplyHook(ACallee, AArgs, AThisValue) + begin + // A caller can hold the Proxy only in a Pascal local: OP_USING_DISPOSE + // clears the dispose method's register before the call, and a getter can + // return a fresh Proxy. Its [[Call]] reads the apply trap, which runs + // guest code, and then reads the Proxy's own target and handler, so it is + // rooted until the call returns. The receiver joins the trap's root-bearing + // arguments only after that lookup, so it is rooted here as well. + Roots.Initialize; + Roots.Add(ACallee); + if Assigned(AThisValue) then + Roots.Add(AThisValue); + try + Result := GProxyApplyHook(ACallee, AArgs, AThisValue); + finally + Roots.Clear; + end; + end else raise TGocciaError.Create('not callable', 0, 0, '', nil); end; +function DispatchCallNoArgs(const ACallee, AThisValue: TGocciaValue): TGocciaValue; +var + Args: TGocciaArgumentsCollection; +begin + if ACallee is TGocciaFunctionBase then + Exit(TGocciaFunctionBase(ACallee).CallNoArgs(AThisValue)); + Args := TGocciaArgumentsCollection.Create; + try + Result := DispatchCall(ACallee, Args, AThisValue); + finally + Args.Free; + end; +end; + +function CollapseBoundFunctionChain(const ABound: TGocciaBoundFunctionValue; + const AArguments: TGocciaArgumentsCollection; var ANewTarget: TGocciaValue; + out ATarget, ABoundThis: TGocciaValue): TGocciaArgumentsCollection; +var + Chain: array of TGocciaBoundFunctionValue; + Count, Capacity, I, J: Integer; + Current: TGocciaValue; + Bound: TGocciaBoundFunctionValue; +begin + Count := 0; + Capacity := AArguments.Length; + Current := ABound; + while Current is TGocciaBoundFunctionValue do + begin + Bound := TGocciaBoundFunctionValue(Current); + if Count = Length(Chain) then + SetLength(Chain, Count * 2 + 4); + Chain[Count] := Bound; + Inc(Count); + Inc(Capacity, Bound.BoundArgCount); + // §10.4.1.2 step 5: If SameValue(F, newTarget), set newTarget to target. + if Assigned(ANewTarget) and (ANewTarget = Current) then + ANewTarget := Bound.OriginalFunction; + Current := Bound.OriginalFunction; + end; + ATarget := Current; + ABoundThis := Chain[Count - 1].BoundThis; + Result := TGocciaArgumentsCollection.CreateWithCapacity(Capacity); + try + for I := Count - 1 downto 0 do + for J := 0 to Chain[I].BoundArgCount - 1 do + Result.Add(Chain[I].GetBoundArg(J)); + for I := 0 to AArguments.Length - 1 do + Result.Add(AArguments.GetElement(I)); + except + Result.Free; + raise; + end; +end; + function TryCallStringFromCodePointApplyFast(const ACallee, AArgArray: TGocciaValue; out AResult: TGocciaValue): Boolean; var @@ -1570,8 +1681,27 @@ function TGocciaBoundFunctionValue.GetBoundArg(const AIndex: Integer): TGocciaVa function TGocciaBoundFunctionValue.Call(const AArguments: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; var CombinedArgs: TGocciaArgumentsCollection; + Target, BoundThis, NoNewTarget: TGocciaValue; I: Integer; begin + // A chain of bound functions forwards in a loop, not a native call a level. + if Assigned(FOriginalFunction) and + (FOriginalFunction.ClassType = TGocciaBoundFunctionValue) then + begin + NoNewTarget := nil; + CombinedArgs := CollapseBoundFunctionChain(Self, AArguments, NoNewTarget, + Target, BoundThis); + try + if Assigned(Target) and Target.IsCallable then + Result := DispatchCall(Target, CombinedArgs, BoundThis) + else + raise TGocciaError.Create('BoundFunction.Call: Original function is not callable', 0, 0, '', nil); + finally + CombinedArgs.Free; + end; + Exit; + end; + // Combine bound arguments with call arguments CombinedArgs := TGocciaArgumentsCollection.CreateWithCapacity( FBoundArgCount + AArguments.Length); @@ -1604,8 +1734,10 @@ function TGocciaBoundFunctionValue.CallNoArgs( if not FOriginalFunction.IsCallable then raise TGocciaError.Create('BoundFunction.Call: Original function is not callable', 0, 0, '', nil); - // Fast path for TGocciaFunctionBase targets only - if not (FOriginalFunction is TGocciaFunctionBase) then + // Fast path for TGocciaFunctionBase targets only; Call walks a chain of + // bound functions in a loop. + if not (FOriginalFunction is TGocciaFunctionBase) or + (FOriginalFunction.ClassType = TGocciaBoundFunctionValue) then begin Args := TGocciaArgumentsCollection.Create; try @@ -1633,8 +1765,10 @@ function TGocciaBoundFunctionValue.CallOneArg(const AArg0, if not FOriginalFunction.IsCallable then raise TGocciaError.Create('BoundFunction.Call: Original function is not callable', 0, 0, '', nil); - // Route non-TGocciaFunctionBase through generic Call path - if not (FOriginalFunction is TGocciaFunctionBase) then + // Route non-TGocciaFunctionBase, and a chain of bound functions, through + // the generic Call path + if not (FOriginalFunction is TGocciaFunctionBase) or + (FOriginalFunction.ClassType = TGocciaBoundFunctionValue) then begin Args := TGocciaArgumentsCollection.CreateWithCapacity(1); try @@ -1663,8 +1797,10 @@ function TGocciaBoundFunctionValue.CallTwoArgs(const AArg0, AArg1, if not FOriginalFunction.IsCallable then raise TGocciaError.Create('BoundFunction.Call: Original function is not callable', 0, 0, '', nil); - // Route non-TGocciaFunctionBase through generic Call path - if not (FOriginalFunction is TGocciaFunctionBase) then + // Route non-TGocciaFunctionBase, and a chain of bound functions, through + // the generic Call path + if not (FOriginalFunction is TGocciaFunctionBase) or + (FOriginalFunction.ClassType = TGocciaBoundFunctionValue) then begin Args := TGocciaArgumentsCollection.CreateWithCapacity(2); try diff --git a/source/units/Goccia.Values.ProxyValue.pas b/source/units/Goccia.Values.ProxyValue.pas index 052f98cde..b3ced1516 100644 --- a/source/units/Goccia.Values.ProxyValue.pas +++ b/source/units/Goccia.Values.ProxyValue.pas @@ -2204,6 +2204,18 @@ function TGocciaProxyValue.ApplyTrap( LeavePropertyDelegation; end; end + else if FTarget is TGocciaBoundFunctionValue then + begin + // A bound function forwards to its own target by a native call too, so + // a chain that alternates Proxies with bound functions is counted here. + EnterPropertyDelegation; + try + Result := TGocciaBoundFunctionValue(FTarget).Call(AArguments, + AThisValue); + finally + LeavePropertyDelegation; + end; + end else if FTarget is TGocciaFunctionBase then Result := TGocciaFunctionBase(FTarget).Call(AArguments, AThisValue) else if FTarget is TGocciaClassValue then @@ -2264,9 +2276,15 @@ function TGocciaProxyValue.ConstructTrap( // synthetic receiver), nested proxies, classes, ordinary functions, and // native constructors. Native-constructor newTarget propagation remains // tracked in #530. - if FTarget is TGocciaProxyValue then + if (FTarget is TGocciaProxyValue) or + (FTarget is TGocciaBoundFunctionValue) or + (FTarget is TGocciaClassValue) then begin - // A Proxy target is counted; see MAX_PROPERTY_DELEGATION_DEPTH. + // A Proxy target is counted; see MAX_PROPERTY_DELEGATION_DEPTH. So is + // a bound function, which forwards to its own target natively, and a + // class: one whose heritage is a Proxy constructs it from its implicit + // constructor by a native call, so `class extends new Proxy(C, {})` + // repeated forms a chain with no script frame in it. EnterPropertyDelegation; try Result := ConstructValue(FTarget, AArguments, EffectiveNewTarget); @@ -2278,9 +2296,9 @@ function TGocciaProxyValue.ConstructTrap( Result := ConstructValue(FTarget, AArguments, EffectiveNewTarget); end; -// The innermost target of a nest of Proxies. typeof, IsCallable and -// IsConstructable cannot throw, so they step through the nest in a loop -// rather than recursing through it. +// The innermost target of a nest of Proxies. typeof and IsCallable cannot +// throw, so they step through the nest in a loop rather than recursing +// through it. function InnermostProxyTarget(const AProxy: TGocciaProxyValue): TGocciaValue; begin Result := AProxy.Target; @@ -2288,6 +2306,25 @@ function InnermostProxyTarget(const AProxy: TGocciaProxyValue): TGocciaValue; Result := TGocciaProxyValue(Result).Target; end; +// The same walk, through the bound functions between Proxies as well, for +// IsConstructable, which cannot throw either: ES2026 §10.4.1.3 BoundFunctionCreate step 6 gives a bound +// function [[Construct]] exactly when its target has one. (A bound function +// is callable whatever its target, so typeof and IsCallable stop at it.) +function InnermostConstructTarget(const AProxy: TGocciaProxyValue): TGocciaValue; +begin + Result := AProxy.Target; + while True do + begin + if Result.ClassType = TGocciaProxyValue then + Result := TGocciaProxyValue(Result).Target + else if (Result is TGocciaBoundFunctionValue) and + Assigned(TGocciaBoundFunctionValue(Result).OriginalFunction) then + Result := TGocciaBoundFunctionValue(Result).OriginalFunction + else + Break; + end; +end; + function TGocciaProxyValue.TypeOf: string; begin // ES2026 §28.1.1: Revocation disables operations, not type @@ -2302,7 +2339,7 @@ function TGocciaProxyValue.IsCallable: Boolean; function TGocciaProxyValue.IsConstructable: Boolean; begin - Result := InnermostProxyTarget(Self).IsConstructable; + Result := InnermostConstructTarget(Self).IsConstructable; end; function TGocciaProxyValue.ToStringTag: string; @@ -2398,23 +2435,32 @@ function DispatchProxyGetPrototype(const AProxy: TGocciaObjectValue): TGocciaVal function DispatchProxyGetFunctionRealm( const AProxy: TGocciaValue): TGocciaRealm; var + Current: TGocciaValue; Proxy: TGocciaProxyValue; begin // ES2026 §7.3.24 GetFunctionRealm steps through a nest of Proxies, each - // of which must not be revoked; a loop, so a deep nest takes no stack. - Proxy := TGocciaProxyValue(AProxy); - Proxy.CheckRevoked; - while Proxy.FTarget is TGocciaProxyValue do + // of which must not be revoked, and through the bound functions between + // them (step 2); a loop, so a deep nest takes no stack. + Current := AProxy; + while True do begin - Proxy := TGocciaProxyValue(Proxy.FTarget); - Proxy.CheckRevoked; + if Current is TGocciaProxyValue then + begin + Proxy := TGocciaProxyValue(Current); + Proxy.CheckRevoked; + Current := Proxy.FTarget; + end + else if Current is TGocciaBoundFunctionValue then + Current := TGocciaBoundFunctionValue(Current).OriginalFunction + else + Break; end; - if Proxy.FTarget is TGocciaFunctionBase then - Exit(TGocciaFunctionBase(Proxy.FTarget).CreationRealm); + if Current is TGocciaFunctionBase then + Exit(TGocciaFunctionBase(Current).CreationRealm); - if Proxy.FTarget is TGocciaClassValue then - Exit(TGocciaClassValue(Proxy.FTarget).CreationRealm); + if Current is TGocciaClassValue then + Exit(TGocciaClassValue(Current).CreationRealm); Result := nil; end; diff --git a/source/units/Goccia.Values.ToPrimitive.pas b/source/units/Goccia.Values.ToPrimitive.pas index 8a6fb4921..10b3f2641 100644 --- a/source/units/Goccia.Values.ToPrimitive.pas +++ b/source/units/Goccia.Values.ToPrimitive.pas @@ -36,6 +36,7 @@ implementation Goccia.Error.Messages, Goccia.Error.Suggestions, Goccia.GarbageCollector, + Goccia.Utils, Goccia.Values.ErrorHelper, Goccia.Values.FunctionBase, Goccia.Values.ObjectValue, @@ -58,7 +59,13 @@ function TryCallMethod(const AObj: TGocciaObjectValue; const AMethodName: string Roots.Initialize; Args := TGocciaArgumentsCollection.Create; try - AResult := TGocciaFunctionBase(Method).Call(Args, AThisValue); + // ES2026 §7.1.1.1 OrdinaryToPrimitive step 3.b.i: Call(method, O). + // IsCallable admits any callable; only a function object takes the + // direct path, and a callable Proxy or a class goes through [[Call]]. + if Method is TGocciaFunctionBase then + AResult := TGocciaFunctionBase(Method).Call(Args, AThisValue) + else + AResult := InvokeCallable(Method, Args, AThisValue); Roots.Add(AResult); Result := AResult.IsPrimitive; finally @@ -121,7 +128,13 @@ function ToPrimitiveGeneric(const AValue: TGocciaValue; const AHint: TGocciaToPr Args := TGocciaArgumentsCollection.Create; try Args.Add(HintValue(AHint)); - Result := TGocciaFunctionBase(ExoticToPrim).Call(Args, AValue); + // A callable Proxy or a class passes GetMethod too (§7.3.10 step 3 + // asks only IsCallable), so only a function object takes the direct + // path. + if ExoticToPrim is TGocciaFunctionBase then + Result := TGocciaFunctionBase(ExoticToPrim).Call(Args, AValue) + else + Result := InvokeCallable(ExoticToPrim, Args, AValue); finally Args.Free; end; diff --git a/source/units/Goccia.Values.TypedArrayValue.pas b/source/units/Goccia.Values.TypedArrayValue.pas index c8f7a004f..bd9d6673c 100644 --- a/source/units/Goccia.Values.TypedArrayValue.pas +++ b/source/units/Goccia.Values.TypedArrayValue.pas @@ -1796,7 +1796,9 @@ function InvokeCallback(const ACallback: TGocciaValue; const AElement, AIndex, A Args.Add(AElement); Args.Add(AIndex); Args.Add(AArray); - Result := TGocciaFunctionBase(ACallback).Call(Args, AThisArg); + // The caller checked IsCallable, which a callable Proxy or a class also + // passes; DispatchCall reaches each through its own [[Call]]. + Result := DispatchCall(ACallback, Args, AThisArg); finally Args.Free; RemoveTempRootIfNeeded(ThisRoot); diff --git a/tests/built-ins/Goccia/callable-proxy-hooks.js b/tests/built-ins/Goccia/callable-proxy-hooks.js new file mode 100644 index 000000000..23615e779 --- /dev/null +++ b/tests/built-ins/Goccia/callable-proxy-hooks.js @@ -0,0 +1,52 @@ +/*--- +description: > + The lifecycle hooks and onTestFinished take any callable, a callable Proxy + included, and the runner calls it. +features: [Proxy] +---*/ + +const log = []; +const proxied = (name) => new Proxy(() => { log.push(name); }, {}); + +describe("lifecycle hooks given a callable Proxy", () => { + beforeAll(proxied("beforeAll")); + beforeEach(proxied("beforeEach")); + afterEach(proxied("afterEach")); + afterAll(proxied("afterAll")); + + test("beforeAll and beforeEach run before the first test", () => { + expect(log).toEqual(["beforeAll", "beforeEach"]); + }); + + test("afterEach runs after each test", () => { + expect(log).toEqual(["beforeAll", "beforeEach", "afterEach", "beforeEach"]); + }); +}); + +describe("after a suite whose afterAll is a callable Proxy", () => { + test("afterAll ran", () => { + expect(log.at(-1)).toBe("afterAll"); + }); +}); + +describe("onTestFinished given a callable Proxy", () => { + let finished = 0; + + test("registers it", () => { + onTestFinished(new Proxy(() => {}, { + apply: () => { finished++; }, + })); + expect(finished).toBe(0); + }); + + test("its apply trap ran when the previous test finished", () => { + expect(finished).toBe(1); + }); +}); + +describe("a hook that is not callable", () => { + test("a Proxy of a plain object is still rejected", () => { + expect(() => beforeEach(new Proxy({}, {}))).toThrow(TypeError); + expect(() => onTestFinished(new Proxy({}, {}))).toThrow(TypeError); + }); +}); diff --git a/tests/built-ins/Proxy/bound-function-chains.js b/tests/built-ins/Proxy/bound-function-chains.js new file mode 100644 index 000000000..e75aefd45 --- /dev/null +++ b/tests/built-ins/Proxy/bound-function-chains.js @@ -0,0 +1,145 @@ +/*--- +description: Chains of bound functions, chains that alternate Proxies with bound functions, and classes that extend a Proxy forward [[Call]] and [[Construct]], and a deep chain ends in RangeError instead of a crash +features: [Proxy, Reflect, class] +---*/ + +// ES2026 §10.4.1.1 and §10.4.1.2: a bound function's [[Call]] and +// [[Construct]] forward to its [[BoundTargetFunction]] with [[BoundArguments]] +// in front, and §10.5.12 and §10.5.13 forward a Proxy without a trap to its +// [[ProxyTarget]]. The specification bounds none of these chains. + +// `bind` reads the target's name and builds "bound " + name, so a long chain +// would carry names whose total length grows with the square of its depth. +// Reading `name` through these handlers gives undefined, which keeps every +// bound name short. +const nameless = { get: (target, key, receiver) => (key === "name" ? undefined : Reflect.get(target, key, receiver)) }; +const alternate = (depth, target) => { + let f = target; + for (const _ of Array.from({ length: depth })) { + f = Function.prototype.bind.call(new Proxy(f, nameless), null); + } + return f; +}; +const bindChain = (depth, target, ...args) => { + let f = target; + for (const _ of Array.from({ length: depth })) { + f = f.bind(null, ...args); + Object.defineProperty(f, "name", { value: "" }); + } + return f; +}; +const classesExtendingProxies = (depth, base) => { + let C = base; + for (const _ of Array.from({ length: depth })) { + const B = new Proxy(C, {}); + C = class extends B {}; + } + return C; +}; + +describe("a chain of bound functions", () => { + test("a call reaches the target with the innermost bound this and every bound argument, innermost first", () => { + const target = ({ m(...args) { return [this.tag, ...args].join(","); } }).m; + const inner = target.bind({ tag: "inner" }, "a"); + const outer = inner.bind({ tag: "outer" }, "b").bind({ tag: "outermost" }, "c"); + expect(outer("d")).toBe("inner,a,b,c,d"); + }); + + test("a chain 5,000 deep is called, constructed and reports constructability", () => { + class Made {} + const callable = bindChain(5000, (...args) => args.length, 1); + expect(callable(2)).toBe(5001); + const constructable = bindChain(5000, Made); + expect(new constructable() instanceof Made).toBe(true); + expect(Reflect.construct(constructable, []) instanceof Made).toBe(true); + expect(typeof Reflect.construct(class {}, [], constructable)).toBe("object"); + expect(() => Reflect.construct(class {}, [], bindChain(5000, () => 1))).toThrow(TypeError); + }); + + test("new.target is the innermost target when the bound function is new.target", () => { + let seen; + class Made { constructor() { seen = new.target; } } + new (bindChain(3, Made))(); + expect(seen).toBe(Made); + }); +}); + +describe("a chain that alternates Proxies with bound functions", () => { + test("500 levels forward a call and a construction", () => { + expect(alternate(500, () => 1)()).toBe(1); + expect(typeof alternate(500, () => 1)).toBe("function"); + // new.target stays the outermost Proxy (§10.4.1.2 step 5 replaces only a + // bound function that is new.target itself), and its "prototype" is the + // bound function's, which is undefined; §10.1.14 + // GetPrototypeFromConstructor then falls back to %Object.prototype%. + let constructed = false; + class Made { constructor() { constructed = true; } } + const instance = new (alternate(500, Made))(); + expect(constructed).toBe(true); + expect(Object.getPrototypeOf(instance)).toBe(Object.prototype); + }); + + test("constructability steps through the chain", () => { + expect(typeof Reflect.construct(class {}, [], alternate(500, class {}))).toBe("object"); + expect(() => Reflect.construct(class {}, [], alternate(500, () => 1))).toThrow(TypeError); + expect(() => new (alternate(500, () => 1))()).toThrow(TypeError); + }); +}); + +describe("classes that extend a Proxy of the previous class", () => { + test("300 levels construct an instance of every class in the chain", () => { + class Base { constructor() { this.base = true; } } + const C = classesExtendingProxies(300, Base); + const instance = new C(); + expect(instance instanceof C).toBe(true); + expect(instance instanceof Base).toBe(true); + expect(instance.base).toBe(true); + }); +}); + +// The engine counts each forward through these chains against the same bound +// as a Proxy nest (MAX_PROPERTY_DELEGATION_DEPTH, 2,000), and checks the +// native stack once they nest, so these chains end in RangeError however much +// stack is left. Node.js has no such bound and completes the call and `new` +// through an alternating chain. +describe.runIf(typeof Goccia !== "undefined")("chains past the engine's bound", () => { + test("a call and a construction through 3,000 alternating levels throw RangeError", () => { + expect(() => alternate(3000, () => 1)()).toThrow(RangeError); + expect(() => new (alternate(3000, class {}))()).toThrow(RangeError); + expect(() => Reflect.construct(alternate(3000, class {}), [])).toThrow(RangeError); + expect(typeof alternate(3000, () => 1)).toBe("function"); + }); + + test("constructing the last of 3,000 classes that extend Proxies throws RangeError", () => { + expect(() => new (classesExtendingProxies(3000, class {}))()).toThrow(RangeError); + }); + + test("instanceof through a chain of 3,000 bound functions throws RangeError", () => { + class Made {} + expect(() => new Made() instanceof bindChain(3000, Made)).toThrow(RangeError); + }); +}); + +// Deep enough to exhaust the native stack without the bound; GocciaScript only, +// on 64-bit builds, like the 20,000-deep Proxy nests in nested-targets.js. +const is64Bit = typeof Goccia !== "undefined" && + ["x86_64", "aarch64", "powerpc64"].includes(Goccia.build.arch); + +describe.runIf(is64Bit)("chains 20,000 deep", () => { + test("alternating Proxies and bound functions throw RangeError, and the engine keeps working", () => { + expect(() => alternate(20000, () => 1)()).toThrow(RangeError); + expect(() => new (alternate(20000, class {}))()).toThrow(RangeError); + expect([1, 2].map((x) => x * 2)).toEqual([2, 4]); + }); + + test("classes extending Proxies throw RangeError, and the engine keeps working", () => { + expect(() => new (classesExtendingProxies(20000, class {}))()).toThrow(RangeError); + expect(new (class {})() instanceof Object).toBe(true); + }); + + test("a chain of bound functions is called and constructed in a loop", () => { + class Made {} + expect(bindChain(20000, () => 1)()).toBe(1); + expect(new (bindChain(20000, Made))() instanceof Made).toBe(true); + }); +}); diff --git a/tests/built-ins/Proxy/callable-proxy-receiver-gc-roots.js b/tests/built-ins/Proxy/callable-proxy-receiver-gc-roots.js new file mode 100644 index 000000000..2a3473e24 --- /dev/null +++ b/tests/built-ins/Proxy/callable-proxy-receiver-gc-roots.js @@ -0,0 +1,53 @@ +/*--- +description: A callable Proxy called as a method keeps its receiver reachable while the apply-trap lookup runs guest code +features: [Goccia.gc, Proxy, Symbol] +---*/ + +const hasGoccia = typeof Goccia !== "undefined"; + +// A bare gc() usually leaves a freed slot readable; the allocation churn after +// it is what makes a collected receiver observable. +const churn = () => { + Goccia.gc(); + let total = 0; + for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) { + const scratch = { v: i * 7.5, tag: "x" + i, list: [i, i + 1] }; + total += scratch.v + scratch.list[0]; + } + return total; +}; + +// Reading `apply` off the handler is guest code that runs before the Proxy +// forwards the call, so the receiver must already be rooted by then. +const churningHandler = () => ({ + get apply() { + churn(); + return undefined; + }, +}); + +describe.runIf(hasGoccia)("callable Proxy receiver GC roots", () => { + test("Symbol.toPrimitive on a fresh object reads its receiver after the trap lookup", () => { + const methods = { + toPrimitive() { + return this.v + this.tag.length; + }, + }; + const toPrimitive = new Proxy(methods.toPrimitive, churningHandler()); + const results = [1, 2, 3, 4, 5].map( + (i) => ({ v: i * 10.5, tag: "abc", [Symbol.toPrimitive]: toPrimitive }) + 1, + ); + expect(results).toEqual([14.5, 25, 35.5, 46, 56.5]); + }); + + test("valueOf on a fresh object reads its receiver after the trap lookup", () => { + const methods = { + valueOf() { + return this.v * 2; + }, + }; + const valueOf = new Proxy(methods.valueOf, churningHandler()); + const results = [1, 2, 3].map((i) => ({ v: i + 0.25, valueOf }) * 1); + expect(results).toEqual([2.5, 4.5, 6.5]); + }); +}); diff --git a/tests/built-ins/TypedArray/prototype/callable-proxy-callback.js b/tests/built-ins/TypedArray/prototype/callable-proxy-callback.js new file mode 100644 index 000000000..bc4013161 --- /dev/null +++ b/tests/built-ins/TypedArray/prototype/callable-proxy-callback.js @@ -0,0 +1,60 @@ +/*--- +description: TypedArray methods accept a callable Proxy as their callback +features: [TypedArray, Proxy] +---*/ + +// The callback only has to be callable (for example ES2026 §23.2.3.22 +// %TypedArray%.prototype.map step 3), and a Proxy whose target is a function +// is. Expected values match Node.js 24. +const proxied = (fn) => new Proxy(fn, {}); + +describe("TypedArray callbacks that are callable Proxies", () => { + test("map and filter", () => { + expect(Array.from(new Uint8Array([1, 2]).map(proxied((x) => x * 2)))).toEqual([2, 4]); + expect(Array.from(new Uint8Array([1, 3, 2]).filter(proxied((x) => x > 1)))).toEqual([3, 2]); + }); + + test("forEach passes the element, the index and the array", () => { + const array = new Uint8Array([5, 6]); + const seen = []; + array.forEach(proxied((value, index, self) => seen.push([value, index, self === array]))); + expect(seen).toEqual([[5, 0, true], [6, 1, true]]); + }); + + test("forEach passes thisArg", () => { + const thisArg = { tag: "t" }; + let received; + new Uint8Array([1]).forEach(proxied(({ m() { received = this; } }).m), thisArg); + expect(received).toBe(thisArg); + }); + + test("every, some, find, findIndex, findLast and findLastIndex", () => { + const array = new Uint8Array([1, 3, 2]); + expect(array.every(proxied((x) => x > 0))).toBe(true); + expect(array.some(proxied((x) => x > 2))).toBe(true); + expect(array.find(proxied((x) => x > 2))).toBe(3); + expect(array.findIndex(proxied((x) => x > 2))).toBe(1); + expect(array.findLast(proxied((x) => x > 0))).toBe(2); + expect(array.findLastIndex(proxied((x) => x > 0))).toBe(2); + }); + + test("reduce, reduceRight, sort, toSorted and from", () => { + const array = new Uint8Array([1, 3, 2]); + expect(array.reduce(proxied((a, b) => a + b))).toBe(6); + expect(array.reduceRight(proxied((a, b) => a + b))).toBe(6); + expect(Array.from(array.toSorted(proxied((a, b) => b - a)))).toEqual([3, 2, 1]); + expect(Array.from(new Uint8Array([1, 3, 2]).sort(proxied((a, b) => b - a)))).toEqual([3, 2, 1]); + expect(Array.from(Uint8Array.from([1, 2], proxied((x) => x + 1)))).toEqual([2, 3]); + }); + + test("the Proxy's apply trap runs for each element", () => { + let calls = 0; + const callback = new Proxy((x) => x, { apply: (target, thisArg, args) => { calls += 1; return target(...args) + 10; } }); + expect(Array.from(new Uint8Array([1, 2]).map(callback))).toEqual([11, 12]); + expect(calls).toBe(2); + }); + + test("an error thrown by the callback propagates", () => { + expect(() => new Uint8Array([1]).forEach(proxied(() => { throw new SyntaxError("x"); }))).toThrow(SyntaxError); + }); +}); diff --git a/tests/language/classes/callable-proxy-accessors.js b/tests/language/classes/callable-proxy-accessors.js new file mode 100644 index 000000000..bba0d6421 --- /dev/null +++ b/tests/language/classes/callable-proxy-accessors.js @@ -0,0 +1,47 @@ +/*--- +description: An accessor whose get or set is a callable Proxy is called on class instances, arrays and classes +features: [Proxy, class] +---*/ + +// ES2026 §10.1.8.1 OrdinaryGet step 7 and §10.1.9.2 OrdinarySetWithOwnDescriptor +// step 6 call the accessor's getter or setter, which ToPropertyDescriptor +// requires only to be callable; a Proxy whose target is a function is. +describe("accessors that are callable Proxies", () => { + test("a getter on an array", () => { + const array = [1]; + Object.defineProperty(array, "x", { get: new Proxy(() => 12, {}) }); + expect(array.x).toBe(12); + }); + + test("a setter on a class prototype, assigned through an instance", () => { + const values = []; + class C {} + Object.defineProperty(C.prototype, "y", { set: new Proxy((v) => { values.push(v); }, {}) }); + new C().y = 1; + expect(values).toEqual([1]); + }); + + test("a getter on a class prototype receives the instance", () => { + class C { constructor() { this.n = 7; } } + Object.defineProperty(C.prototype, "x", { get: new Proxy(({ m() { return this.n; } }).m, {}) }); + expect(new C().x).toBe(7); + }); + + test("a static getter and setter on a class", () => { + const values = []; + class C {} + Object.defineProperty(C, "x", { + get: new Proxy(() => 6, {}), + set: new Proxy((v) => { values.push(v); }, {}), + }); + expect(C.x).toBe(6); + C.x = 3; + expect(values).toEqual([3]); + }); + + test("an inherited getter-only accessor still refuses an assignment", () => { + class C {} + Object.defineProperty(C.prototype, "z", { get: new Proxy(() => 1, {}) }); + expect(() => { new C().z = 2; }).toThrow(TypeError); + }); +}); diff --git a/tests/language/decorators/callable-proxy.js b/tests/language/decorators/callable-proxy.js new file mode 100644 index 000000000..147149e28 --- /dev/null +++ b/tests/language/decorators/callable-proxy.js @@ -0,0 +1,115 @@ +/*--- +description: Decorators, the values they return, and addInitializer initializers may be callable Proxies +features: [decorators, Proxy] +---*/ + +// The decorators proposal calls a decorator and its initializers through +// Call(), and only checks IsCallable on what it gets; a Proxy whose target is +// a function qualifies. +const proxied = (fn) => new Proxy(fn, {}); + +describe("callable Proxies as decorators", () => { + test("a class decorator", () => { + const seen = []; + const dec = proxied((value, context) => { seen.push(context.kind); }); + @dec class A {} + expect(typeof A).toBe("function"); + expect(seen).toEqual(["class"]); + }); + + test("a method decorator replaces the method", () => { + const dec = proxied((method, context) => ({ m() { return "decorated " + method.call(this); } }).m); + class B { @dec m() { return "m"; } } + expect(new B().m()).toBe("decorated m"); + }); + + test("a field decorator returns the initializer", () => { + const dec = proxied((value, context) => (x) => x + 1); + class F { @dec x = 4; } + expect(new F().x).toBe(5); + }); + + test("getter and accessor decorators that return undefined leave the member", () => { + const dec = proxied(() => undefined); + class G { @dec get x() { return 1; } @dec accessor y = 2; } + expect(new G().x).toBe(1); + expect(new G().y).toBe(2); + }); +}); + +describe("callable Proxies returned by decorators", () => { + test("as a field initializer", () => { + const dec = (value, context) => proxied((x) => x + 1); + class F { @dec x = 4; } + expect(new F().x).toBe(5); + }); + + test("as a static field initializer", () => { + const dec = (value, context) => proxied((x) => x + 2); + class S { @dec static x = 4; } + expect(S.x).toBe(6); + }); + + test("as a public getter and a method", () => { + const getter = (g, context) => proxied(() => "pg"); + const method = (m, context) => proxied(() => "pm"); + class P { @getter get x() { return 1; } @method m() { return 1; } } + expect(new P().x).toBe("pg"); + expect(new P().m()).toBe("pm"); + }); + + test("as an auto-accessor's get, set and init", () => { + const log = []; + const dec = (value, context) => ({ + get: proxied(() => "ag"), + set: proxied((x) => { log.push(x); }), + init: proxied((x) => x * 10), + }); + class A { @dec accessor x = 1; } + const a = new A(); + expect(a.x).toBe("ag"); + a.x = 2; + expect(log[log.length - 1]).toBe(2); + }); + + test("as a private getter and setter, reading and writing them does not crash", () => { + const getter = (g, context) => proxied(() => "privg"); + const setter = (s, context) => proxied(() => {}); + class P { + @getter get #x() { return 1; } + @setter set #y(v) {} + read() { return this.#x; } + write() { this.#y = 8; } + } + expect(() => new P().read()).not.toThrow(); + expect(() => new P().write()).not.toThrow(); + }); +}); + +describe("callable Proxies passed to addInitializer", () => { + test("a class decorator's initializer runs with the class as this", () => { + let receiver; + const dec = (value, context) => { + context.addInitializer(proxied(({ m() { receiver = this; } }).m)); + }; + @dec class C {} + expect(receiver).toBe(C); + }); + + test("method and field initializers run for each instance", () => { + const log = []; + const method = (m, context) => { context.addInitializer(proxied(() => log.push("method"))); }; + const field = (v, context) => { context.addInitializer(proxied(() => log.push("field"))); }; + class I { @method m() {} @field x = 1; } + new I(); + expect(log.sort()).toEqual(["field", "method"]); + }); + + test("static method and static field initializers run once", () => { + const log = []; + const method = (m, context) => { context.addInitializer(proxied(() => log.push("static method"))); }; + const field = (v, context) => { context.addInitializer(proxied(() => log.push("static field"))); }; + class S { @method static m() {} @field static x = 1; } + expect(log.sort()).toEqual(["static field", "static method"]); + }); +}); diff --git a/tests/language/explicit-resource-management/callable-proxy-dispose.js b/tests/language/explicit-resource-management/callable-proxy-dispose.js new file mode 100644 index 000000000..23ca8a55e --- /dev/null +++ b/tests/language/explicit-resource-management/callable-proxy-dispose.js @@ -0,0 +1,58 @@ +/*--- +description: A callable Proxy works as a Symbol.dispose method +features: [explicit-resource-management, Proxy] +---*/ + +// GetDisposeMethod reads Symbol.dispose with GetMethod, which asks only +// IsCallable (ES2026 §7.3.10 step 3), so a Proxy whose target is a function is +// a dispose method. (Node.js 24 rejects it; that is a V8 bug.) +describe("using with a callable Proxy as the dispose method", () => { + test("the Proxy is called at block exit with the resource as this", () => { + const calls = []; + const resource = { + [Symbol.dispose]: new Proxy(({ m() { calls.push(this === resource); } }).m, {}), + }; + { + using r = resource; + expect(calls).toEqual([]); + } + expect(calls).toEqual([true]); + }); + + test("the Proxy's apply trap runs", () => { + let trapped = false; + { + using r = { [Symbol.dispose]: new Proxy(() => {}, { apply: () => { trapped = true; } }) }; + } + expect(trapped).toBe(true); + }); + + test("an error thrown by the Proxy propagates out of the block", () => { + expect(() => { + { + using r = { [Symbol.dispose]: new Proxy(() => { throw new SyntaxError("dispose"); }, {}) }; + } + }).toThrow(SyntaxError); + }); +}); + +describe("a callable Proxy dispose method held only by the disposal", () => { + // OP_USING_DISPOSE clears the method's register before the call, so a + // Proxy a getter returned is referenced only by the call that runs it. + test("survives a collection while its apply trap is looked up", () => { + let disposed = 0; + { + using r = { + get [Symbol.dispose]() { + return new Proxy(() => {}, { + get apply() { + Goccia.gc(); + return () => { disposed++; }; + }, + }); + }, + }; + } + expect(disposed).toBe(1); + }); +}); diff --git a/tests/language/expressions/to-primitive-proxy.js b/tests/language/expressions/to-primitive-proxy.js new file mode 100644 index 000000000..83215e324 --- /dev/null +++ b/tests/language/expressions/to-primitive-proxy.js @@ -0,0 +1,84 @@ +/*--- +description: A callable Proxy works as Symbol.toPrimitive, valueOf or toString in every conversion to a primitive +features: [Proxy, Symbol.toPrimitive] +---*/ + +// ES2026 §7.1.1 ToPrimitive step 1.a reads @@toPrimitive with GetMethod, which +// asks only IsCallable (§7.3.10 step 3), and step 1.b.iv calls it. A Proxy whose +// target is a function has [[Call]] (§10.5.12), so it qualifies. So does a +// valueOf or toString method, which OrdinaryToPrimitive (§7.1.1.1 step 3.b) +// calls when IsCallable is true. Expected values match Node.js 24. + +const hinted = () => ({ [Symbol.toPrimitive]: new Proxy((hint) => "h:" + hint, {}) }); + +describe("a callable Proxy as Symbol.toPrimitive", () => { + test("unary plus and Number() call it with the number hint", () => { + expect(+{ [Symbol.toPrimitive]: new Proxy(() => 1, {}) }).toBe(1); + expect(Number({ [Symbol.toPrimitive]: new Proxy(() => 3, {}) })).toBe(3); + expect(+{ [Symbol.toPrimitive]: new Proxy((hint) => (hint === "number" ? 7 : 0), {}) }).toBe(7); + }); + + test("template literals, String() and + pass the string and default hints", () => { + expect(`${hinted()}`).toBe("h:string"); + expect(String(hinted())).toBe("h:string"); + expect(hinted() + "").toBe("h:default"); + }); + + test("a computed property key is converted through it", () => { + expect(({ a: 1 })[{ [Symbol.toPrimitive]: new Proxy(() => "a", {}) }]).toBe(1); + }); + + test("the Proxy's apply trap runs with the target, the receiver and the hint", () => { + const calls = []; + const target = () => 1; + const object = { + [Symbol.toPrimitive]: new Proxy(target, { + apply: (t, receiver, args) => { + calls.push([t === target, receiver === object, args[0]]); + return 41 + t(); + }, + }), + }; + expect(+object).toBe(42); + expect(calls).toEqual([[true, true, "number"]]); + }); + + test("an error thrown inside it propagates as an ordinary exception", () => { + const object = { [Symbol.toPrimitive]: new Proxy(() => { throw new SyntaxError("from the proxy"); }, {}) }; + expect(() => +object).toThrow(SyntaxError); + }); + + test("an object result throws TypeError", () => { + expect(() => +{ [Symbol.toPrimitive]: new Proxy(() => ({}), {}) }).toThrow(TypeError); + }); + + test("a class passes GetMethod but throws TypeError when called", () => { + expect(() => +{ [Symbol.toPrimitive]: class {} }).toThrow(TypeError); + }); +}); + +describe("a callable Proxy as valueOf or toString", () => { + test("valueOf is called for the number hint", () => { + expect(+{ valueOf: new Proxy(() => 2, {}) }).toBe(2); + expect(Math.max({ valueOf: new Proxy(() => 9, {}) }, 1)).toBe(9); + }); + + test("toString is called for the string hint", () => { + expect(`${{ toString: new Proxy(() => "ts", {}) }}`).toBe("ts"); + expect(String({ toString: new Proxy(() => "es", {}) })).toBe("es"); + expect(String.raw({ raw: { length: 1, 0: { toString: new Proxy(() => "raw", {}) } } })).toBe("raw"); + }); + + test("valueOf is called with the object as this", () => { + const object = { n: 5, valueOf: new Proxy(({ m() { return this.n; } }).m, {}) }; + expect(object * 2).toBe(10); + }); + + test("an error thrown inside valueOf propagates as an ordinary exception", () => { + expect(() => +{ valueOf: new Proxy(() => { throw new SyntaxError("x"); }, {}) }).toThrow(SyntaxError); + }); + + test("a class as valueOf throws TypeError", () => { + expect(() => +{ valueOf: class {} }).toThrow(TypeError); + }); +});