diff --git a/source/app/GocciaTestRunner.dpr b/source/app/GocciaTestRunner.dpr index 091ebaab7..1b8743f2b 100644 --- a/source/app/GocciaTestRunner.dpr +++ b/source/app/GocciaTestRunner.dpr @@ -2,20 +2,6 @@ program GocciaTestRunner; {$I Goccia.inc} -// Run the full JavaScript test suite with access to the entire 4 GB user -// address space available to a 32-bit process on 64-bit Windows. The runner -// builds a fresh engine per file and, on worker threads, keeps automatic GC -// disabled and retains each engine's object graph until the thread finishes -// the batch (the deliberate "bulk reclaim at shutdown" strategy). The full -// suite therefore peaks near ~2 GB on i386-win32 — already at the default -// 2 GB per-process limit on main, and over it once the suite grows — which -// surfaces as "Out of memory"/"Access violation" failures. Marking the -// binary large-address-aware lifts that ceiling to 4 GB. 64-bit Windows is -// already large-address-aware, so this only applies to the 32-bit build. -{$IF DEFINED(MSWINDOWS) AND DEFINED(CPU32)} - {$SETPEFLAGS $20} // IMAGE_FILE_LARGE_ADDRESS_AWARE -{$ENDIF} - uses {$IFDEF UNIX}cthreads,{$ENDIF} Classes, diff --git a/source/units/Goccia.Builtins.Atomics.pas b/source/units/Goccia.Builtins.Atomics.pas index c663ac5e0..53a11f8cf 100644 --- a/source/units/Goccia.Builtins.Atomics.pas +++ b/source/units/Goccia.Builtins.Atomics.pas @@ -25,7 +25,8 @@ procedure ShutdownAtomicsWaitersForCurrentThread; TGocciaAtomics = class(TGocciaBuiltin) public constructor Create(const AName: string; AScope: TGocciaScope; - AThrowError: TGocciaThrowErrorCallback); + AThrowError: TGocciaThrowErrorCallback; + const ADefineGlobalBinding: Boolean = True); destructor Destroy; override; published function AtomicsAdd(const AArgs: TGocciaArgumentsCollection; @@ -942,7 +943,8 @@ procedure ShutdownAtomicsWaitersForCurrentThread; end; constructor TGocciaAtomics.Create(const AName: string; AScope: TGocciaScope; - AThrowError: TGocciaThrowErrorCallback); + AThrowError: TGocciaThrowErrorCallback; + const ADefineGlobalBinding: Boolean = True); var Members: TGocciaMemberCollection; StaticMembers: TArray; @@ -989,7 +991,8 @@ constructor TGocciaAtomics.Create(const AName: string; AScope: TGocciaScope; end; RegisterMemberDefinitions(FBuiltinObject, StaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet, True); + if ADefineGlobalBinding then + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet, True); end; destructor TGocciaAtomics.Destroy; diff --git a/source/units/Goccia.Builtins.DisposableStack.pas b/source/units/Goccia.Builtins.DisposableStack.pas index 39c7aab1c..a11cd711e 100644 --- a/source/units/Goccia.Builtins.DisposableStack.pas +++ b/source/units/Goccia.Builtins.DisposableStack.pas @@ -16,6 +16,8 @@ TGocciaBuiltinDisposableStack = class(TGocciaBuiltin) private FDisposableStackPrototype: TGocciaValue; FAsyncDisposableStackPrototype: TGocciaValue; + FDisposableStackConstructorValue: TGocciaValue; + FAsyncDisposableStackConstructorValue: TGocciaValue; published function DisposableStackConstructor(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; @@ -51,7 +53,11 @@ TGocciaBuiltinDisposableStack = class(TGocciaBuiltin) const AThisValue: TGocciaValue): TGocciaValue; public constructor Create(const AName: string; const AScope: TGocciaScope; - const AThrowError: TGocciaThrowErrorCallback); + const AThrowError: TGocciaThrowErrorCallback; + const ADefineGlobalBinding: Boolean = True); + + property DisposableStackConstructorValue: TGocciaValue read FDisposableStackConstructorValue; + property AsyncDisposableStackConstructorValue: TGocciaValue read FAsyncDisposableStackConstructorValue; end; procedure ClearDisposableStackSlotMap; @@ -455,7 +461,8 @@ function CreateDisposableStackInstance(const AIsAsync: Boolean; { TGocciaBuiltinDisposableStack } constructor TGocciaBuiltinDisposableStack.Create(const AName: string; - const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); + const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback; + const ADefineGlobalBinding: Boolean = True); var DisposableStackFunc: TGocciaNativeFunctionValue; AsyncDisposableStackFunc: TGocciaNativeFunctionValue; @@ -474,7 +481,9 @@ constructor TGocciaBuiltinDisposableStack.Create(const AName: string; TGocciaPropertyDescriptorData.Create(DisposableStackFunc, [pfWritable, pfConfigurable])); DisposableStackFunc.DefineProperty(PROP_PROTOTYPE, TGocciaPropertyDescriptorData.Create(FDisposableStackPrototype, [])); - AScope.DefineLexicalBinding(CONSTRUCTOR_DISPOSABLE_STACK, DisposableStackFunc, dtConst, True); + FDisposableStackConstructorValue := DisposableStackFunc; + if ADefineGlobalBinding then + AScope.DefineLexicalBinding(CONSTRUCTOR_DISPOSABLE_STACK, DisposableStackFunc, dtConst, True); AsyncDisposableStackFunc := TGocciaNativeFunctionValue.Create( AsyncDisposableStackConstructor, CONSTRUCTOR_ASYNC_DISPOSABLE_STACK, 0); @@ -482,7 +491,9 @@ constructor TGocciaBuiltinDisposableStack.Create(const AName: string; TGocciaPropertyDescriptorData.Create(AsyncDisposableStackFunc, [pfWritable, pfConfigurable])); AsyncDisposableStackFunc.DefineProperty(PROP_PROTOTYPE, TGocciaPropertyDescriptorData.Create(FAsyncDisposableStackPrototype, [])); - AScope.DefineLexicalBinding(CONSTRUCTOR_ASYNC_DISPOSABLE_STACK, AsyncDisposableStackFunc, dtConst, True); + FAsyncDisposableStackConstructorValue := AsyncDisposableStackFunc; + if ADefineGlobalBinding then + AScope.DefineLexicalBinding(CONSTRUCTOR_ASYNC_DISPOSABLE_STACK, AsyncDisposableStackFunc, dtConst, True); UseFunc := TGocciaNativeFunctionValue.CreateWithoutPrototype(StackUse, PROP_USE, 1); AdoptFunc := TGocciaNativeFunctionValue.CreateWithoutPrototype(StackAdopt, PROP_ADOPT, 2); diff --git a/source/units/Goccia.Builtins.GlobalProxy.pas b/source/units/Goccia.Builtins.GlobalProxy.pas index 75e50584a..d7ddc708b 100644 --- a/source/units/Goccia.Builtins.GlobalProxy.pas +++ b/source/units/Goccia.Builtins.GlobalProxy.pas @@ -19,7 +19,8 @@ TGocciaGlobalProxy = class function ProxyRevocable(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; public - constructor Create(const AScope: TGocciaScope); + constructor Create(const AScope: TGocciaScope; + const ADefineGlobalBinding: Boolean = True); property ConstructorValue: TGocciaValue read FConstructorValue; end; @@ -37,7 +38,8 @@ implementation Goccia.Values.ObjectValue, Goccia.Values.ProxyValue; -constructor TGocciaGlobalProxy.Create(const AScope: TGocciaScope); +constructor TGocciaGlobalProxy.Create(const AScope: TGocciaScope; + const ADefineGlobalBinding: Boolean = True); var ConstructorFn: TGocciaNativeFunctionValue; begin @@ -48,7 +50,8 @@ constructor TGocciaGlobalProxy.Create(const AScope: TGocciaScope); PROP_REVOCABLE, 2)); FConstructorValue := ConstructorFn; - AScope.DefineLexicalBinding(CONSTRUCTOR_PROXY, FConstructorValue, dtConst, True); + if ADefineGlobalBinding then + AScope.DefineLexicalBinding(CONSTRUCTOR_PROXY, FConstructorValue, dtConst, True); end; // ES2026 §28.2.1 Proxy(target, handler) diff --git a/source/units/Goccia.Builtins.GlobalReflect.pas b/source/units/Goccia.Builtins.GlobalReflect.pas index f8ea267d4..32441d03d 100644 --- a/source/units/Goccia.Builtins.GlobalReflect.pas +++ b/source/units/Goccia.Builtins.GlobalReflect.pas @@ -30,7 +30,9 @@ TGocciaGlobalReflect = class(TGocciaBuiltin) function ReflectSet(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; function ReflectSetPrototypeOf(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; public - constructor Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); + constructor Create(const AName: string; const AScope: TGocciaScope; + const AThrowError: TGocciaThrowErrorCallback; + const ADefineGlobalBinding: Boolean = True); end; implementation @@ -66,7 +68,9 @@ procedure RequireObjectTarget(const ATarget: TGocciaValue; const AMethodName: st { TGocciaGlobalReflect } -constructor TGocciaGlobalReflect.Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); +constructor TGocciaGlobalReflect.Create(const AName: string; + const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback; + const ADefineGlobalBinding: Boolean = True); var Members: TGocciaMemberCollection; begin @@ -97,7 +101,8 @@ constructor TGocciaGlobalReflect.Create(const AName: string; const AScope: TGocc end; RegisterMemberDefinitions(FBuiltinObject, FStaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtConst, True); + if ADefineGlobalBinding then + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtConst, True); end; // ES2026 §28.1.1 Reflect.apply(target, thisArgument, argumentsList) diff --git a/source/units/Goccia.Builtins.Intl.pas b/source/units/Goccia.Builtins.Intl.pas index 40114b653..9fe0f2c96 100644 --- a/source/units/Goccia.Builtins.Intl.pas +++ b/source/units/Goccia.Builtins.Intl.pas @@ -58,7 +58,11 @@ TGocciaIntlBuiltin = class(TGocciaBuiltin) procedure RegisterSegmenter; procedure RegisterDurationFormat; public - constructor Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); + constructor Create(const AName: string; const AScope: TGocciaScope; + const AThrowError: TGocciaThrowErrorCallback; + const ADefineGlobalBinding: Boolean = True); + + property IntlNamespace: TGocciaObjectValue read FIntlNamespace; end; function CanonicalizeLocaleListFromValue(const ALocales: TGocciaValue): IntlTypes.TStringArray; @@ -100,8 +104,9 @@ implementation { TGocciaIntlBuiltin } -constructor TGocciaIntlBuiltin.Create(const AName: string; const AScope: TGocciaScope; - const AThrowError: TGocciaThrowErrorCallback); +constructor TGocciaIntlBuiltin.Create(const AName: string; + const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback; + const ADefineGlobalBinding: Boolean = True); var IntlMembers: array[0..2] of TGocciaMemberDefinition; begin @@ -133,7 +138,8 @@ constructor TGocciaIntlBuiltin.Create(const AName: string; const AScope: TGoccia [pfConfigurable]); RegisterMemberDefinitions(FIntlNamespace, IntlMembers); - AScope.DefineLexicalBinding(AName, FIntlNamespace, dtLet, True); + if ADefineGlobalBinding then + AScope.DefineLexicalBinding(AName, FIntlNamespace, dtLet, True); finally TGarbageCollector.Instance.RemoveTempRoot(FIntlNamespace); end; diff --git a/source/units/Goccia.Builtins.Temporal.pas b/source/units/Goccia.Builtins.Temporal.pas index 572466aa9..54ccc9d79 100644 --- a/source/units/Goccia.Builtins.Temporal.pas +++ b/source/units/Goccia.Builtins.Temporal.pas @@ -78,7 +78,11 @@ TGocciaTemporalBuiltin = class(TGocciaBuiltin) procedure RegisterZonedDateTime; procedure RegisterNow; public - constructor Create(const AName: string; const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback); + constructor Create(const AName: string; const AScope: TGocciaScope; + const AThrowError: TGocciaThrowErrorCallback; + const ADefineGlobalBinding: Boolean = True); + + property TemporalNamespace: TGocciaObjectValue read FTemporalNamespace; end; implementation @@ -121,8 +125,9 @@ implementation { TGocciaTemporalBuiltin } -constructor TGocciaTemporalBuiltin.Create(const AName: string; const AScope: TGocciaScope; - const AThrowError: TGocciaThrowErrorCallback); +constructor TGocciaTemporalBuiltin.Create(const AName: string; + const AScope: TGocciaScope; const AThrowError: TGocciaThrowErrorCallback; + const ADefineGlobalBinding: Boolean = True); var TemporalMembers: array[0..0] of TGocciaMemberDefinition; begin @@ -147,7 +152,8 @@ constructor TGocciaTemporalBuiltin.Create(const AName: string; const AScope: TGo [pfConfigurable]); RegisterMemberDefinitions(FTemporalNamespace, TemporalMembers); - AScope.DefineLexicalBinding(AName, FTemporalNamespace, dtLet, True); + if ADefineGlobalBinding then + AScope.DefineLexicalBinding(AName, FTemporalNamespace, dtLet, True); finally TGarbageCollector.Instance.RemoveTempRoot(FTemporalNamespace); end; diff --git a/source/units/Goccia.Engine.pas b/source/units/Goccia.Engine.pas index 9bf56294a..4ae439756 100644 --- a/source/units/Goccia.Engine.pas +++ b/source/units/Goccia.Engine.pas @@ -52,6 +52,7 @@ interface Goccia.ObjectModel.Engine, Goccia.Realm, Goccia.Scope, + Goccia.Scope.BindingMap, Goccia.SourceMap, Goccia.SourcePipeline, Goccia.Values.BigIntValue, @@ -59,6 +60,7 @@ interface Goccia.Values.FunctionBase, Goccia.Values.HoleValue, Goccia.Values.IteratorValue, + Goccia.Values.ObjectPropertyDescriptor, Goccia.Values.ObjectValue, Goccia.Values.Primitives, Goccia.Values.TypedArrayValue; @@ -178,6 +180,11 @@ TGocciaEngine = class procedure PinSingletons; procedure RegisterBuiltIns; procedure RegisterBuiltinConstructors; + procedure DefineBuiltinBindingPlaceholder(const AName: string; + const ADeclarationType: TGocciaDeclarationType); + procedure DefineLazyGlobalThisProperty(const AName: string; + const AFactory: TGocciaLazyPropertyFactory); + procedure RegisterLazyBuiltinGlobalProperties; procedure ExecuteShims; procedure RegisterTypedArrayConstructor(const AName: string; const AKind: TGocciaTypedArrayKind; const AObjectConstructor: TGocciaClassValue); procedure RegisterGlobalThis; @@ -191,6 +198,13 @@ TGocciaEngine = class function GocciaGCMaxBytesGetter(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; function GocciaGCSuggestedMaxBytesGetter(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; function GocciaGCBytesAllocatedGetter(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; + function MaterializeTemporalGlobal: TGocciaValue; + function MaterializeIntlGlobal: TGocciaValue; + function MaterializeAtomicsGlobal: TGocciaValue; + function MaterializeProxyGlobal: TGocciaValue; + function MaterializeReflectGlobal: TGocciaValue; + function MaterializeDisposableStackGlobal: TGocciaValue; + function MaterializeAsyncDisposableStackGlobal: TGocciaValue; procedure DoRetainModule(const AModule: TObject); procedure DiscardRuntimePending; procedure PrintSourcePipelineWarnings( @@ -326,7 +340,6 @@ implementation Goccia.Values.MapValue, Goccia.Values.NativeFunction, Goccia.Values.NumberObjectValue, - Goccia.Values.ObjectPropertyDescriptor, Goccia.Values.PromiseValue, Goccia.Values.SetValue, Goccia.Values.SharedArrayBufferValue, @@ -697,19 +710,108 @@ procedure TGocciaEngine.RegisterBuiltIns; FBuiltinSymbol := TGocciaGlobalSymbol.Create(CONSTRUCTOR_SYMBOL, Scope, ThrowError); FBuiltinMap := TGocciaGlobalMap.Create(CONSTRUCTOR_MAP, Scope, ThrowError); FBuiltinPromise := TGocciaGlobalPromise.Create(CONSTRUCTOR_PROMISE, Scope, ThrowError); - FBuiltinTemporal := TGocciaTemporalBuiltin.Create('Temporal', Scope, ThrowError); - FBuiltinIntl := TGocciaIntlBuiltin.Create('Intl', Scope, ThrowError); - FBuiltinAtomics := TGocciaAtomics.Create(CONSTRUCTOR_ATOMICS, Scope, ThrowError); + DefineBuiltinBindingPlaceholder('Temporal', dtLet); + DefineBuiltinBindingPlaceholder('Intl', dtLet); + DefineBuiltinBindingPlaceholder(CONSTRUCTOR_ATOMICS, dtLet); FBuiltinArrayBuffer := TGocciaGlobalArrayBuffer.Create(CONSTRUCTOR_ARRAY_BUFFER, Scope, ThrowError); - FBuiltinProxy := TGocciaGlobalProxy.Create(Scope); - FBuiltinReflect := TGocciaGlobalReflect.Create('Reflect', Scope, ThrowError); + DefineBuiltinBindingPlaceholder(CONSTRUCTOR_PROXY, dtConst); + DefineBuiltinBindingPlaceholder('Reflect', dtConst); FBuiltinGlobalString := TGocciaGlobalString.Create(CONSTRUCTOR_STRING, Scope, ThrowError); FBuiltinGlobals := TGocciaGlobals.Create('Globals', Scope, ThrowError); - FBuiltinDisposableStack := TGocciaBuiltinDisposableStack.Create('DisposableStack', Scope, ThrowError); + DefineBuiltinBindingPlaceholder(CONSTRUCTOR_DISPOSABLE_STACK, dtConst); + DefineBuiltinBindingPlaceholder(CONSTRUCTOR_ASYNC_DISPOSABLE_STACK, dtConst); Scope.DefineLexicalBinding(CONSTRUCTOR_ITERATOR, TGocciaIteratorValue.CreateGlobalObject, dtConst, True); RegisterBuiltinConstructors; end; +procedure TGocciaEngine.DefineBuiltinBindingPlaceholder(const AName: string; + const ADeclarationType: TGocciaDeclarationType); +begin + FInterpreter.GlobalScope.DefineLexicalBinding(AName, + TGocciaUndefinedLiteralValue.UndefinedValue, ADeclarationType, True); +end; + +procedure TGocciaEngine.DefineLazyGlobalThisProperty(const AName: string; + const AFactory: TGocciaLazyPropertyFactory); +begin + if not (FRealm.GlobalObject is TGocciaObjectValue) then + Exit; + + TGocciaObjectValue(FRealm.GlobalObject).DefineProperty(AName, + TGocciaLazyPropertyDescriptorData.Create(AFactory, + [pfWritable, pfConfigurable])); +end; + +procedure TGocciaEngine.RegisterLazyBuiltinGlobalProperties; +begin + DefineLazyGlobalThisProperty('Temporal', MaterializeTemporalGlobal); + DefineLazyGlobalThisProperty('Intl', MaterializeIntlGlobal); + DefineLazyGlobalThisProperty(CONSTRUCTOR_ATOMICS, MaterializeAtomicsGlobal); + DefineLazyGlobalThisProperty(CONSTRUCTOR_PROXY, MaterializeProxyGlobal); + DefineLazyGlobalThisProperty('Reflect', MaterializeReflectGlobal); + DefineLazyGlobalThisProperty(CONSTRUCTOR_DISPOSABLE_STACK, + MaterializeDisposableStackGlobal); + DefineLazyGlobalThisProperty(CONSTRUCTOR_ASYNC_DISPOSABLE_STACK, + MaterializeAsyncDisposableStackGlobal); +end; + +function TGocciaEngine.MaterializeTemporalGlobal: TGocciaValue; +begin + if not Assigned(FBuiltinTemporal) then + FBuiltinTemporal := TGocciaTemporalBuiltin.Create('Temporal', + FInterpreter.GlobalScope, ThrowError, False); + Result := FBuiltinTemporal.TemporalNamespace; +end; + +function TGocciaEngine.MaterializeIntlGlobal: TGocciaValue; +begin + if not Assigned(FBuiltinIntl) then + FBuiltinIntl := TGocciaIntlBuiltin.Create('Intl', FInterpreter.GlobalScope, + ThrowError, False); + Result := FBuiltinIntl.IntlNamespace; +end; + +function TGocciaEngine.MaterializeAtomicsGlobal: TGocciaValue; +begin + if not Assigned(FBuiltinAtomics) then + FBuiltinAtomics := TGocciaAtomics.Create(CONSTRUCTOR_ATOMICS, + FInterpreter.GlobalScope, ThrowError, False); + Result := FBuiltinAtomics.BuiltinObject; +end; + +function TGocciaEngine.MaterializeProxyGlobal: TGocciaValue; +begin + if not Assigned(FBuiltinProxy) then + FBuiltinProxy := TGocciaGlobalProxy.Create(FInterpreter.GlobalScope, False); + Result := FBuiltinProxy.ConstructorValue; +end; + +function TGocciaEngine.MaterializeReflectGlobal: TGocciaValue; +begin + if not Assigned(FBuiltinReflect) then + FBuiltinReflect := TGocciaGlobalReflect.Create('Reflect', + FInterpreter.GlobalScope, ThrowError, False); + Result := FBuiltinReflect.BuiltinObject; +end; + +function TGocciaEngine.MaterializeDisposableStackGlobal: TGocciaValue; +begin + if not Assigned(FBuiltinDisposableStack) then + FBuiltinDisposableStack := TGocciaBuiltinDisposableStack.Create( + CONSTRUCTOR_DISPOSABLE_STACK, FInterpreter.GlobalScope, ThrowError, + False); + Result := FBuiltinDisposableStack.DisposableStackConstructorValue; +end; + +function TGocciaEngine.MaterializeAsyncDisposableStackGlobal: TGocciaValue; +begin + if not Assigned(FBuiltinDisposableStack) then + FBuiltinDisposableStack := TGocciaBuiltinDisposableStack.Create( + CONSTRUCTOR_DISPOSABLE_STACK, FInterpreter.GlobalScope, ThrowError, + False); + Result := FBuiltinDisposableStack.AsyncDisposableStackConstructorValue; +end; + function ObjectPrototypeProvider: TGocciaObjectValue; begin Result := TGocciaObjectValue.SharedObjectPrototype; @@ -1043,6 +1145,7 @@ procedure TGocciaEngine.RegisterBuiltinConstructors; RegisterGocciaScriptGlobal; RegisterGlobalThis; + RegisterLazyBuiltinGlobalProperties; end; procedure TGocciaEngine.RegisterTypedArrayConstructor(const AName: string; const AKind: TGocciaTypedArrayKind; const AObjectConstructor: TGocciaClassValue); diff --git a/source/units/Goccia.Shims.pas b/source/units/Goccia.Shims.pas index 5c6b2f665..9cb99b4cc 100644 --- a/source/units/Goccia.Shims.pas +++ b/source/units/Goccia.Shims.pas @@ -432,8 +432,6 @@ implementation ' catch (e) {}'#10 + ' return NaN;'#10 + '};'#10 + - 'const __GocciaShimNumberFormat: any = Intl.NumberFormat;'#10 + - 'const __GocciaShimDateTimeFormat: any = Intl.DateTimeFormat;'#10 + 'const __GocciaDateSlots = new WeakMap();'#10 + 'const __GocciaDateIsObject = (value: any): boolean =>'#10 + ' (typeof value === "object" && value !== null) || typeof value === "function";'#10 + @@ -510,7 +508,7 @@ 'const __GocciaDateClass = class Date {'#10 + ' const numberLocale = {'#10 + ' toLocaleString(...args: any[]): string {'#10 + ' const value: number = Number.prototype.valueOf.call(this);'#10 + - ' return new __GocciaShimNumberFormat(args[0], args[1]).format(value);'#10 + + ' return new Intl.NumberFormat(args[0], args[1]).format(value);'#10 + ' }'#10 + ' }.toLocaleString;'#10 + ' const arrayLocale = {'#10 + @@ -763,17 +761,17 @@ 'const __GocciaDateClass = class Date {'#10 + ' toLocaleString(...args: any[]): string {'#10 + ' Date.#require(this);'#10 + ' if (!Date.#valid(this)) return "Invalid Date";'#10 + - ' return new __GocciaShimDateTimeFormat(args[0], Date.#localeOptions(args[1], true, true)).format(Date.#get(this));'#10 + + ' return new Intl.DateTimeFormat(args[0], Date.#localeOptions(args[1], true, true)).format(Date.#get(this));'#10 + ' }'#10 + ' toLocaleDateString(...args: any[]): string {'#10 + ' Date.#require(this);'#10 + ' if (!Date.#valid(this)) return "Invalid Date";'#10 + - ' return new __GocciaShimDateTimeFormat(args[0], Date.#localeOptions(args[1], true, false)).format(Date.#get(this));'#10 + + ' return new Intl.DateTimeFormat(args[0], Date.#localeOptions(args[1], true, false)).format(Date.#get(this));'#10 + ' }'#10 + ' toLocaleTimeString(...args: any[]): string {'#10 + ' Date.#require(this);'#10 + ' if (!Date.#valid(this)) return "Invalid Date";'#10 + - ' return new __GocciaShimDateTimeFormat(args[0], Date.#localeOptions(args[1], false, true)).format(Date.#get(this));'#10 + + ' return new Intl.DateTimeFormat(args[0], Date.#localeOptions(args[1], false, true)).format(Date.#get(this));'#10 + ' }'#10 + ' toTemporalInstant(): any {'#10 + ' const value: number = __GocciaDateGetSlot(this);'#10 + diff --git a/source/units/Goccia.Values.ObjectPropertyDescriptor.pas b/source/units/Goccia.Values.ObjectPropertyDescriptor.pas index 66a944d50..5dd953cfd 100644 --- a/source/units/Goccia.Values.ObjectPropertyDescriptor.pas +++ b/source/units/Goccia.Values.ObjectPropertyDescriptor.pas @@ -10,6 +10,8 @@ interface Goccia.Values.Primitives; type + TGocciaLazyPropertyFactory = function: TGocciaValue of object; + // Flags store resolved descriptor attribute values; Fields store which // descriptor keys were explicitly present in a partial descriptor. TPropertyFlag = (pfEnumerable, pfConfigurable, pfWritable); @@ -63,6 +65,16 @@ TGocciaPropertyDescriptorData = class(TGocciaPropertyDescriptor) property Value: TGocciaValue read FValue write FValue; end; + TGocciaLazyPropertyDescriptorData = class(TGocciaPropertyDescriptorData) + private + FFactory: TGocciaLazyPropertyFactory; + FMaterialized: Boolean; + public + constructor Create(const AFactory: TGocciaLazyPropertyFactory; + const AFlags: TPropertyFlags); + function Materialize: TGocciaValue; + end; + TGocciaPropertyDescriptorAccessor = class(TGocciaPropertyDescriptor) private FGetter: TGocciaValue; @@ -180,6 +192,27 @@ procedure TGocciaPropertyDescriptorData.MarkValues; FValue.MarkReferences; end; +constructor TGocciaLazyPropertyDescriptorData.Create( + const AFactory: TGocciaLazyPropertyFactory; const AFlags: TPropertyFlags); +begin + inherited Create(TGocciaUndefinedLiteralValue.UndefinedValue, AFlags); + FFactory := AFactory; + FMaterialized := False; +end; + +function TGocciaLazyPropertyDescriptorData.Materialize: TGocciaValue; +begin + if not FMaterialized then + begin + if TMethod(FFactory).Code <> nil then + Value := FFactory() + else + Value := TGocciaUndefinedLiteralValue.UndefinedValue; + FMaterialized := True; + end; + Result := Value; +end; + constructor TGocciaPropertyDescriptorAccessor.Create(const AGetter: TGocciaValue; const ASetter: TGocciaValue; const AFlags: TPropertyFlags); begin inherited Create(AFlags, [pdfEnumerable, pdfConfigurable, pdfGet, pdfSet]); @@ -231,7 +264,12 @@ function IsGenericDescriptor(const ADescriptor: TGocciaPropertyDescriptor): Bool function ClonePropertyDescriptor( const ADescriptor: TGocciaPropertyDescriptor): TGocciaPropertyDescriptor; begin - if ADescriptor is TGocciaPropertyDescriptorData then + if ADescriptor is TGocciaLazyPropertyDescriptorData then + Result := TGocciaPropertyDescriptorData.CreatePartial( + TGocciaLazyPropertyDescriptorData(ADescriptor).Materialize, + ADescriptor.Flags, + ADescriptor.Fields) + else if ADescriptor is TGocciaPropertyDescriptorData then Result := TGocciaPropertyDescriptorData.CreatePartial( TGocciaPropertyDescriptorData(ADescriptor).Value, ADescriptor.Flags, diff --git a/source/units/Goccia.Values.ObjectValue.pas b/source/units/Goccia.Values.ObjectValue.pas index 85240abd5..6c3dac61c 100644 --- a/source/units/Goccia.Values.ObjectValue.pas +++ b/source/units/Goccia.Values.ObjectValue.pas @@ -22,6 +22,12 @@ interface TGocciaObjectValue = class(TGocciaValue) private procedure SetRegExpData(const AValue: TObject); + function MaterializeOwnLazyProperty(const AName: string; + const ADescriptor: TGocciaPropertyDescriptor): TGocciaPropertyDescriptor; + procedure MaterializeAllLazyStringProperties; + function StoreLazyPropertyDescriptor(const AName: string; + const ADescriptor: TGocciaPropertyDescriptor; + out ABlockedByExtensibility: Boolean): Boolean; protected FProperties: TGocciaPropertyMap; FSymbolDescriptors: TSymbolDescriptorMap; @@ -362,6 +368,65 @@ procedure TGocciaObjectValue.SetRegExpData(const AValue: TObject); FRegExpData := AValue; end; +function TGocciaObjectValue.MaterializeOwnLazyProperty(const AName: string; + const ADescriptor: TGocciaPropertyDescriptor): TGocciaPropertyDescriptor; +var + LazyDescriptor: TGocciaLazyPropertyDescriptorData; + MaterializedDescriptor: TGocciaPropertyDescriptorData; + Value: TGocciaValue; +begin + Result := ADescriptor; + if not (ADescriptor is TGocciaLazyPropertyDescriptorData) then + Exit; + + LazyDescriptor := TGocciaLazyPropertyDescriptorData(ADescriptor); + Value := LazyDescriptor.Materialize; + MaterializedDescriptor := TGocciaPropertyDescriptorData.Create( + Value, LazyDescriptor.Flags); + FProperties.Add(AName, MaterializedDescriptor); + LazyDescriptor.Free; + Result := MaterializedDescriptor; +end; + +procedure TGocciaObjectValue.MaterializeAllLazyStringProperties; +var + Descriptor: TGocciaPropertyDescriptor; + I: Integer; + Keys: TArray; +begin + Keys := FProperties.Keys; + for I := 0 to High(Keys) do + if FProperties.TryGetValue(Keys[I], Descriptor) then + MaterializeOwnLazyProperty(Keys[I], Descriptor); +end; + +function TGocciaObjectValue.StoreLazyPropertyDescriptor(const AName: string; + const ADescriptor: TGocciaPropertyDescriptor; + out ABlockedByExtensibility: Boolean): Boolean; +var + Current: TGocciaPropertyDescriptor; +begin + Result := False; + ABlockedByExtensibility := False; + if not (ADescriptor is TGocciaLazyPropertyDescriptorData) then + Exit; + + Current := nil; + FProperties.TryGetValue(AName, Current); + if not Assigned(Current) and not FExtensible then + begin + ABlockedByExtensibility := True; + Exit; + end; + if Assigned(Current) and not Current.Configurable then + Exit; + + if Assigned(Current) then + Current.Free; + FProperties.Add(AName, ADescriptor); + Result := True; +end; + class function TGocciaObjectValue.GetSharedObjectPrototype: TGocciaObjectValue; begin Result := GetSharedObjectPrototypeForRealm(CurrentRealm); @@ -745,6 +810,7 @@ procedure TGocciaObjectValue.AssignProperty(const AName: string; const AValue: T begin if FProperties.TryGetValue(AName, Descriptor) then begin + Descriptor := MaterializeOwnLazyProperty(AName, Descriptor); if Descriptor is TGocciaPropertyDescriptorAccessor then begin Accessor := TGocciaPropertyDescriptorAccessor(Descriptor); @@ -1099,9 +1165,19 @@ procedure TGocciaObjectValue.DefineProperty(const AName: string; const ADescript var Current: TGocciaPropertyDescriptor; Applied: TGocciaPropertyDescriptor; + BlockedByExtensibility: Boolean; begin + if StoreLazyPropertyDescriptor(AName, ADescriptor, + BlockedByExtensibility) then + Exit; + if BlockedByExtensibility then + ThrowTypeError(Format(SErrorCannotAddPropertyNotExtensible, [AName]), + SSuggestObjectNotExtensible); + Current := nil; FProperties.TryGetValue(AName, Current); + if Assigned(Current) then + Current := MaterializeOwnLazyProperty(AName, Current); if not ValidateAndCreatePropertyDescriptor(Current, ADescriptor, FExtensible, Applied) then begin @@ -1127,9 +1203,21 @@ function TGocciaObjectValue.TryDefineProperty(const AName: string; const ADescri var Current: TGocciaPropertyDescriptor; Applied: TGocciaPropertyDescriptor; + BlockedByExtensibility: Boolean; begin + if StoreLazyPropertyDescriptor(AName, ADescriptor, + BlockedByExtensibility) then + Exit(True); + if BlockedByExtensibility then + begin + ADescriptor.Free; + Exit(False); + end; + Current := nil; FProperties.TryGetValue(AName, Current); + if Assigned(Current) then + Current := MaterializeOwnLazyProperty(AName, Current); if not ValidateAndCreatePropertyDescriptor(Current, ADescriptor, FExtensible, Applied) then begin @@ -1208,6 +1296,7 @@ function TGocciaObjectValue.GetOwnPropertyKeys: TArray; Keys: TArray; Count: Integer; begin + MaterializeAllLazyStringProperties; SetLength(Keys, FProperties.Count); Count := 0; for Key in FProperties.Keys do @@ -1264,6 +1353,7 @@ function TGocciaObjectValue.GetPropertyWithContext(const AName: string; const AT begin if FProperties.TryGetValue(AName, Descriptor) then begin + Descriptor := MaterializeOwnLazyProperty(AName, Descriptor); if Descriptor is TGocciaPropertyDescriptorAccessor then begin Accessor := TGocciaPropertyDescriptorAccessor(Descriptor); @@ -1323,7 +1413,9 @@ function TGocciaObjectValue.GetPropertyWithContext(const AName: string; const AT function TGocciaObjectValue.GetOwnPropertyDescriptor(const AName: string): TGocciaPropertyDescriptor; begin - if not FProperties.TryGetValue(AName, Result) then + if FProperties.TryGetValue(AName, Result) then + Result := MaterializeOwnLazyProperty(AName, Result) + else Result := nil; end; @@ -1336,8 +1428,12 @@ function TGocciaObjectValue.HasProperty(const AName: string): Boolean; end; function TGocciaObjectValue.HasOwnProperty(const AName: string): Boolean; +var + Descriptor: TGocciaPropertyDescriptor; begin - Result := FProperties.ContainsKey(AName); + Result := FProperties.TryGetValue(AName, Descriptor); + if Result then + MaterializeOwnLazyProperty(AName, Descriptor); end; function TGocciaObjectValue.DeleteProperty(const AName: string): Boolean; @@ -1349,6 +1445,7 @@ function TGocciaObjectValue.DeleteProperty(const AName: string): Boolean; Result := True; Exit; end; + Descriptor := MaterializeOwnLazyProperty(AName, Descriptor); if not Descriptor.Configurable then begin @@ -1367,6 +1464,7 @@ function TGocciaObjectValue.GetEnumerablePropertyNames: TArray; Names: TArray; Count: Integer; begin + MaterializeAllLazyStringProperties; SetLength(Names, FProperties.Count); Count := 0; @@ -1441,6 +1539,7 @@ function TGocciaObjectValue.GetAllPropertyNames: TArray; Names: TArray; Count: Integer; begin + MaterializeAllLazyStringProperties; SetLength(Names, FProperties.Count); Count := 0; for Key in FProperties.Keys do diff --git a/tests/built-ins/global-properties/lazy-builtins.js b/tests/built-ins/global-properties/lazy-builtins.js new file mode 100644 index 000000000..e70d14fc0 --- /dev/null +++ b/tests/built-ins/global-properties/lazy-builtins.js @@ -0,0 +1,65 @@ +/*--- +description: > + Heavy built-in globals are exposed as ordinary global object properties even + when their backing objects are materialized lazily. +features: [global-properties, globalThis] +---*/ + +const lazyGlobals = [ + ["Temporal", "object"], + ["Intl", "object"], + ["Atomics", "object"], + ["Proxy", "function"], + ["Reflect", "object"], + ["DisposableStack", "function"], + ["AsyncDisposableStack", "function"], +]; + +describe("lazy global built-ins", () => { + test("lazy globals are own properties before direct access", () => { + for (const [name] of lazyGlobals) { + expect(name in globalThis).toBe(true); + expect(Object.hasOwn(globalThis, name)).toBe(true); + } + }); + + test("lazy globals materialize as ordinary writable configurable data properties", () => { + for (const [name, expectedType] of lazyGlobals) { + const desc = Object.getOwnPropertyDescriptor(globalThis, name); + + expect(desc !== undefined).toBe(true); + expect(desc.enumerable).toBe(false); + expect(desc.writable).toBe(true); + expect(desc.configurable).toBe(true); + expect(typeof desc.value).toBe(expectedType); + expect(desc.value === globalThis[name]).toBe(true); + } + }); + + test("lazy globals keep their declaration order on the global object", () => { + const names = Object.getOwnPropertyNames(globalThis); + let previousIndex = -1; + + for (const [name] of lazyGlobals) { + const index = names.indexOf(name); + + expect(index > previousIndex).toBe(true); + previousIndex = index; + } + }); + + test("lazy global property values are identical to identifier bindings", () => { + expect(globalThis.Temporal === Temporal).toBe(true); + expect(globalThis.Intl === Intl).toBe(true); + expect(globalThis.Atomics === Atomics).toBe(true); + expect(globalThis.Proxy === Proxy).toBe(true); + expect(globalThis.Reflect === Reflect).toBe(true); + expect(globalThis.DisposableStack === DisposableStack).toBe(true); + expect(globalThis.AsyncDisposableStack === AsyncDisposableStack).toBe(true); + }); + + test("configurable lazy globals can be deleted from globalThis", () => { + expect(delete globalThis.Reflect).toBe(true); + expect("Reflect" in globalThis).toBe(false); + }); +});