From 99ad72ae5d9362b4d4bdfc661dacd2769ede4ea9 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Fri, 8 May 2026 11:44:51 +0100 Subject: [PATCH 1/5] Allow var/function declarations to shadow built-in globals in script mode (#521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per ES2026 §16.1.7, top-level var and function declarations may shadow built-in globals (Array, NaN, Infinity, undefined, etc.) in script mode. Previously all such redeclarations were rejected with SyntaxError. Add a BuiltIn flag to TLexicalBinding so the engine can distinguish engine-registered bindings from user declarations. CheckTopLevelRedeclarations now skips var/function declarations when the existing binding is built-in, and DefineVariableBinding removes the built-in lexical binding at runtime so the var binding becomes visible through GetBinding. Closes #521 Co-Authored-By: Claude Opus 4.6 (1M context) --- source/units/Goccia.Builtins.Benchmark.pas | 6 ++-- source/units/Goccia.Builtins.CSV.pas | 2 +- source/units/Goccia.Builtins.Console.pas | 2 +- .../units/Goccia.Builtins.DisposableStack.pas | 4 +-- source/units/Goccia.Builtins.GlobalBigInt.pas | 2 +- source/units/Goccia.Builtins.GlobalFFI.pas | 2 +- source/units/Goccia.Builtins.GlobalFetch.pas | 2 +- .../units/Goccia.Builtins.GlobalPromise.pas | 2 +- source/units/Goccia.Builtins.GlobalProxy.pas | 2 +- .../units/Goccia.Builtins.GlobalReflect.pas | 2 +- source/units/Goccia.Builtins.GlobalRegExp.pas | 2 +- source/units/Goccia.Builtins.GlobalSymbol.pas | 2 +- source/units/Goccia.Builtins.Globals.pas | 36 +++++++++---------- source/units/Goccia.Builtins.JSON.pas | 2 +- source/units/Goccia.Builtins.JSON5.pas | 2 +- source/units/Goccia.Builtins.JSONL.pas | 2 +- source/units/Goccia.Builtins.Math.pas | 2 +- source/units/Goccia.Builtins.Performance.pas | 2 +- source/units/Goccia.Builtins.TOML.pas | 2 +- source/units/Goccia.Builtins.TSV.pas | 2 +- source/units/Goccia.Builtins.Temporal.pas | 2 +- .../units/Goccia.Builtins.TestingLibrary.pas | 6 ++-- source/units/Goccia.Builtins.YAML.pas | 2 +- source/units/Goccia.Engine.pas | 18 +++++----- source/units/Goccia.ObjectModel.Engine.pas | 2 +- source/units/Goccia.Runtime.pas | 2 +- source/units/Goccia.Scope.BindingMap.pas | 1 + source/units/Goccia.Scope.Redeclaration.pas | 21 ++++++----- source/units/Goccia.Scope.pas | 34 +++++++++++++++--- .../const/cannot-shadow-builtin-globals.js | 24 +++++++++++++ .../let/cannot-shadow-builtin-globals.js | 36 +++++++++++++++++++ .../shadow-builtin-globals.js | 22 ++++++++++++ tests/language/var/shadow-builtin-globals.js | 35 ++++++++++++++++++ 33 files changed, 216 insertions(+), 69 deletions(-) create mode 100644 tests/language/declarations/const/cannot-shadow-builtin-globals.js create mode 100644 tests/language/declarations/let/cannot-shadow-builtin-globals.js create mode 100644 tests/language/function-keyword/shadow-builtin-globals.js create mode 100644 tests/language/var/shadow-builtin-globals.js diff --git a/source/units/Goccia.Builtins.Benchmark.pas b/source/units/Goccia.Builtins.Benchmark.pas index 7270c38d..31df09d6 100644 --- a/source/units/Goccia.Builtins.Benchmark.pas +++ b/source/units/Goccia.Builtins.Benchmark.pas @@ -185,9 +185,9 @@ constructor TGocciaBenchmark.Create(const AName: string; const AScope: TGocciaSc FRegisteredBenchmarks := TObjectList.Create; FCurrentSuiteName := ''; - AScope.DefineLexicalBinding('suite', TGocciaNativeFunctionValue.Create(Suite, 'suite', 2), dtConst); - AScope.DefineLexicalBinding('bench', TGocciaNativeFunctionValue.Create(Bench, 'bench', 2), dtConst); - AScope.DefineLexicalBinding('runBenchmarks', TGocciaNativeFunctionValue.Create(RunBenchmarks, 'runBenchmarks', 0), dtConst); + AScope.DefineLexicalBinding('suite', TGocciaNativeFunctionValue.Create(Suite, 'suite', 2), dtConst, True); + AScope.DefineLexicalBinding('bench', TGocciaNativeFunctionValue.Create(Bench, 'bench', 2), dtConst, True); + AScope.DefineLexicalBinding('runBenchmarks', TGocciaNativeFunctionValue.Create(RunBenchmarks, 'runBenchmarks', 0), dtConst, True); end; destructor TGocciaBenchmark.Destroy; diff --git a/source/units/Goccia.Builtins.CSV.pas b/source/units/Goccia.Builtins.CSV.pas index 08ef6f49..f2dfce28 100644 --- a/source/units/Goccia.Builtins.CSV.pas +++ b/source/units/Goccia.Builtins.CSV.pas @@ -81,7 +81,7 @@ constructor TGocciaCSVBuiltin.Create(const AName: string; end; RegisterMemberDefinitions(FBuiltinObject, FStaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet); + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet, True); end; destructor TGocciaCSVBuiltin.Destroy; diff --git a/source/units/Goccia.Builtins.Console.pas b/source/units/Goccia.Builtins.Console.pas index e9d98a48..e78f7cb8 100644 --- a/source/units/Goccia.Builtins.Console.pas +++ b/source/units/Goccia.Builtins.Console.pas @@ -118,7 +118,7 @@ constructor TGocciaConsole.Create(const AName: string; const AScope: TGocciaScop end; RegisterMemberDefinitions(FBuiltinObject, FStaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet); + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet, True); end; procedure TGocciaConsole.EmitLine(const AMethod, ALine: string); diff --git a/source/units/Goccia.Builtins.DisposableStack.pas b/source/units/Goccia.Builtins.DisposableStack.pas index 476c43e8..98e74cd8 100644 --- a/source/units/Goccia.Builtins.DisposableStack.pas +++ b/source/units/Goccia.Builtins.DisposableStack.pas @@ -359,11 +359,11 @@ constructor TGocciaBuiltinDisposableStack.Create(const AName: string; DisposableStackFunc := TGocciaNativeFunctionValue.Create( DisposableStackConstructor, CONSTRUCTOR_DISPOSABLE_STACK, 0); - AScope.DefineLexicalBinding(CONSTRUCTOR_DISPOSABLE_STACK, DisposableStackFunc, dtConst); + AScope.DefineLexicalBinding(CONSTRUCTOR_DISPOSABLE_STACK, DisposableStackFunc, dtConst, True); AsyncDisposableStackFunc := TGocciaNativeFunctionValue.Create( AsyncDisposableStackConstructor, CONSTRUCTOR_ASYNC_DISPOSABLE_STACK, 0); - AScope.DefineLexicalBinding(CONSTRUCTOR_ASYNC_DISPOSABLE_STACK, AsyncDisposableStackFunc, dtConst); + AScope.DefineLexicalBinding(CONSTRUCTOR_ASYNC_DISPOSABLE_STACK, AsyncDisposableStackFunc, dtConst, True); end; function TGocciaBuiltinDisposableStack.DisposableStackConstructor( diff --git a/source/units/Goccia.Builtins.GlobalBigInt.pas b/source/units/Goccia.Builtins.GlobalBigInt.pas index 3aa5656f..4d98509e 100644 --- a/source/units/Goccia.Builtins.GlobalBigInt.pas +++ b/source/units/Goccia.Builtins.GlobalBigInt.pas @@ -98,7 +98,7 @@ constructor TGocciaGlobalBigInt.Create(const AName: string; const AScope: TGocci TGocciaPropertyDescriptorData.Create(Proto, [])); // Bind BigInt in scope - AScope.DefineLexicalBinding(AName, FBigIntFunction, dtLet); + AScope.DefineLexicalBinding(AName, FBigIntFunction, dtLet, True); end; // ES2026 §21.2.1.1 BigInt(value) — conversion function diff --git a/source/units/Goccia.Builtins.GlobalFFI.pas b/source/units/Goccia.Builtins.GlobalFFI.pas index 81073be7..0727ca2e 100644 --- a/source/units/Goccia.Builtins.GlobalFFI.pas +++ b/source/units/Goccia.Builtins.GlobalFFI.pas @@ -67,7 +67,7 @@ constructor TGocciaGlobalFFI.Create(const AName: string; const AScope: TGocciaSc end; RegisterMemberDefinitions(FBuiltinObject, FStaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtConst); + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtConst, True); end; function TGocciaGlobalFFI.FFIOpen(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue; diff --git a/source/units/Goccia.Builtins.GlobalFetch.pas b/source/units/Goccia.Builtins.GlobalFetch.pas index ba636df4..f9f25170 100644 --- a/source/units/Goccia.Builtins.GlobalFetch.pas +++ b/source/units/Goccia.Builtins.GlobalFetch.pas @@ -107,7 +107,7 @@ constructor TGocciaGlobalFetch.Create(const AName: string; // Register fetch as a global function AScope.DefineLexicalBinding('fetch', - TGocciaNativeFunctionValue.Create(FetchCallback, 'fetch', 1), dtConst); + TGocciaNativeFunctionValue.Create(FetchCallback, 'fetch', 1), dtConst, True); end; destructor TGocciaGlobalFetch.Destroy; diff --git a/source/units/Goccia.Builtins.GlobalPromise.pas b/source/units/Goccia.Builtins.GlobalPromise.pas index 494a0d83..c74ae11c 100644 --- a/source/units/Goccia.Builtins.GlobalPromise.pas +++ b/source/units/Goccia.Builtins.GlobalPromise.pas @@ -478,7 +478,7 @@ constructor TGocciaGlobalPromise.Create(const AName: string; const AScope: TGocc end; RegisterMemberDefinitions(FPromiseConstructor, FStaticMembers); - AScope.DefineLexicalBinding(AName, FPromiseConstructor, dtLet); + AScope.DefineLexicalBinding(AName, FPromiseConstructor, dtLet, True); end; // ES2026 §27.2.4.8 get Promise [ @@species ] diff --git a/source/units/Goccia.Builtins.GlobalProxy.pas b/source/units/Goccia.Builtins.GlobalProxy.pas index d154baa5..75e50584 100644 --- a/source/units/Goccia.Builtins.GlobalProxy.pas +++ b/source/units/Goccia.Builtins.GlobalProxy.pas @@ -48,7 +48,7 @@ constructor TGocciaGlobalProxy.Create(const AScope: TGocciaScope); PROP_REVOCABLE, 2)); FConstructorValue := ConstructorFn; - AScope.DefineLexicalBinding(CONSTRUCTOR_PROXY, FConstructorValue, dtConst); + 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 0cc3dac5..347903e5 100644 --- a/source/units/Goccia.Builtins.GlobalReflect.pas +++ b/source/units/Goccia.Builtins.GlobalReflect.pas @@ -96,7 +96,7 @@ constructor TGocciaGlobalReflect.Create(const AName: string; const AScope: TGocc end; RegisterMemberDefinitions(FBuiltinObject, FStaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtConst); + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtConst, True); end; // ES2026 §28.1.1 Reflect.apply(target, thisArgument, argumentsList) diff --git a/source/units/Goccia.Builtins.GlobalRegExp.pas b/source/units/Goccia.Builtins.GlobalRegExp.pas index ecbac7bf..7e633adb 100644 --- a/source/units/Goccia.Builtins.GlobalRegExp.pas +++ b/source/units/Goccia.Builtins.GlobalRegExp.pas @@ -350,7 +350,7 @@ constructor TGocciaGlobalRegExp.Create(const AName: string; end; RegisterMemberDefinitions(FRegExpConstructor, FStaticMembers); - AScope.DefineLexicalBinding(AName, FRegExpConstructor, dtConst); + AScope.DefineLexicalBinding(AName, FRegExpConstructor, dtConst, True); end; // ES2026 §22.2.4.2 get RegExp [ @@species ] diff --git a/source/units/Goccia.Builtins.GlobalSymbol.pas b/source/units/Goccia.Builtins.GlobalSymbol.pas index 4232899d..635900bc 100644 --- a/source/units/Goccia.Builtins.GlobalSymbol.pas +++ b/source/units/Goccia.Builtins.GlobalSymbol.pas @@ -116,7 +116,7 @@ constructor TGocciaGlobalSymbol.Create(const AName: string; const AScope: TGocci TGocciaPropertyDescriptorData.Create(FSymbolFunction, [pfConfigurable, pfWritable])); // Bind Symbol in scope - AScope.DefineLexicalBinding(AName, FSymbolFunction, dtLet); + AScope.DefineLexicalBinding(AName, FSymbolFunction, dtLet, True); end; destructor TGocciaGlobalSymbol.Destroy; diff --git a/source/units/Goccia.Builtins.Globals.pas b/source/units/Goccia.Builtins.Globals.pas index b523306c..7a85ced1 100644 --- a/source/units/Goccia.Builtins.Globals.pas +++ b/source/units/Goccia.Builtins.Globals.pas @@ -209,9 +209,9 @@ constructor TGocciaGlobals.Create(const AName: string; const AScope: TGocciaScop begin inherited Create(AName, AScope, AThrowError); - AScope.DefineLexicalBinding('undefined', TGocciaUndefinedLiteralValue.UndefinedValue, dtConst); - AScope.DefineLexicalBinding('NaN', TGocciaNumberLiteralValue.NaNValue, dtConst); - AScope.DefineLexicalBinding('Infinity', TGocciaNumberLiteralValue.InfinityValue, dtConst); + AScope.DefineLexicalBinding('undefined', TGocciaUndefinedLiteralValue.UndefinedValue, dtConst, True); + AScope.DefineLexicalBinding('NaN', TGocciaNumberLiteralValue.NaNValue, dtConst, True); + AScope.DefineLexicalBinding('Infinity', TGocciaNumberLiteralValue.InfinityValue, dtConst, True); FErrorProto := TGocciaObjectValue.Create; FErrorProto.DefineProperty(PROP_NAME, TGocciaPropertyDescriptorData.Create(TGocciaStringLiteralValue.Create(ERROR_NAME), [pfConfigurable, pfWritable])); @@ -323,36 +323,36 @@ constructor TGocciaGlobals.Create(const AName: string; const AScope: TGocciaScop FSuppressedErrorProto.DefineProperty(PROP_CONSTRUCTOR, TGocciaPropertyDescriptorData.Create(SuppressedErrorConstructorFunc, [pfConfigurable, pfWritable])); FDOMExceptionProto.DefineProperty(PROP_CONSTRUCTOR, TGocciaPropertyDescriptorData.Create(DOMExceptionConstructorFunc, [pfConfigurable, pfWritable])); - AScope.DefineLexicalBinding(ERROR_NAME, ErrorConstructorFunc, dtConst); - AScope.DefineLexicalBinding(TYPE_ERROR_NAME, TypeErrorConstructorFunc, dtConst); - AScope.DefineLexicalBinding(REFERENCE_ERROR_NAME, ReferenceErrorConstructorFunc, dtConst); - AScope.DefineLexicalBinding(RANGE_ERROR_NAME, RangeErrorConstructorFunc, dtConst); - AScope.DefineLexicalBinding(SYNTAX_ERROR_NAME, SyntaxErrorConstructorFunc, dtConst); - AScope.DefineLexicalBinding(URI_ERROR_NAME, URIErrorConstructorFunc, dtConst); - AScope.DefineLexicalBinding(AGGREGATE_ERROR_NAME, AggregateErrorConstructorFunc, dtConst); - AScope.DefineLexicalBinding(SUPPRESSED_ERROR_NAME, SuppressedErrorConstructorFunc, dtConst); - AScope.DefineLexicalBinding(DOM_EXCEPTION_NAME, DOMExceptionConstructorFunc, dtConst); + AScope.DefineLexicalBinding(ERROR_NAME, ErrorConstructorFunc, dtConst, True); + AScope.DefineLexicalBinding(TYPE_ERROR_NAME, TypeErrorConstructorFunc, dtConst, True); + AScope.DefineLexicalBinding(REFERENCE_ERROR_NAME, ReferenceErrorConstructorFunc, dtConst, True); + AScope.DefineLexicalBinding(RANGE_ERROR_NAME, RangeErrorConstructorFunc, dtConst, True); + AScope.DefineLexicalBinding(SYNTAX_ERROR_NAME, SyntaxErrorConstructorFunc, dtConst, True); + AScope.DefineLexicalBinding(URI_ERROR_NAME, URIErrorConstructorFunc, dtConst, True); + AScope.DefineLexicalBinding(AGGREGATE_ERROR_NAME, AggregateErrorConstructorFunc, dtConst, True); + AScope.DefineLexicalBinding(SUPPRESSED_ERROR_NAME, SuppressedErrorConstructorFunc, dtConst, True); + AScope.DefineLexicalBinding(DOM_EXCEPTION_NAME, DOMExceptionConstructorFunc, dtConst, True); AScope.DefineLexicalBinding('encodeURI', - TGocciaNativeFunctionValue.Create(EncodeURICallback, 'encodeURI', 1), dtConst); + TGocciaNativeFunctionValue.Create(EncodeURICallback, 'encodeURI', 1), dtConst, True); AScope.DefineLexicalBinding('decodeURI', - TGocciaNativeFunctionValue.Create(DecodeURICallback, 'decodeURI', 1), dtConst); + TGocciaNativeFunctionValue.Create(DecodeURICallback, 'decodeURI', 1), dtConst, True); AScope.DefineLexicalBinding('encodeURIComponent', - TGocciaNativeFunctionValue.Create(EncodeURIComponentCallback, 'encodeURIComponent', 1), dtConst); + TGocciaNativeFunctionValue.Create(EncodeURIComponentCallback, 'encodeURIComponent', 1), dtConst, True); AScope.DefineLexicalBinding('decodeURIComponent', - TGocciaNativeFunctionValue.Create(DecodeURIComponentCallback, 'decodeURIComponent', 1), dtConst); + TGocciaNativeFunctionValue.Create(DecodeURIComponentCallback, 'decodeURIComponent', 1), dtConst, True); end; procedure TGocciaGlobals.RegisterRuntimeGlobals; begin FScope.DefineLexicalBinding('queueMicrotask', - TGocciaNativeFunctionValue.Create(QueueMicrotaskCallback, 'queueMicrotask', 1), dtConst); + TGocciaNativeFunctionValue.Create(QueueMicrotaskCallback, 'queueMicrotask', 1), dtConst, True); FScope.DefineLexicalBinding('structuredClone', - TGocciaNativeFunctionValue.Create(StructuredCloneCallback, 'structuredClone', 1), dtConst); + TGocciaNativeFunctionValue.Create(StructuredCloneCallback, 'structuredClone', 1), dtConst, True); end; { NativeError ( message [ , options ] ) — §20.5.6.1.1 (shared by all NativeError constructors) diff --git a/source/units/Goccia.Builtins.JSON.pas b/source/units/Goccia.Builtins.JSON.pas index 82b8f08c..a33ec142 100644 --- a/source/units/Goccia.Builtins.JSON.pas +++ b/source/units/Goccia.Builtins.JSON.pas @@ -92,7 +92,7 @@ constructor TGocciaJSONBuiltin.Create(const AName: string; const AScope: TGoccia end; RegisterMemberDefinitions(FBuiltinObject, FStaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet); + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet, True); end; destructor TGocciaJSONBuiltin.Destroy; diff --git a/source/units/Goccia.Builtins.JSON5.pas b/source/units/Goccia.Builtins.JSON5.pas index 75e7c360..8eb65b44 100644 --- a/source/units/Goccia.Builtins.JSON5.pas +++ b/source/units/Goccia.Builtins.JSON5.pas @@ -130,7 +130,7 @@ constructor TGocciaJSON5Builtin.Create(const AName: string; end; RegisterMemberDefinitions(FBuiltinObject, FStaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet); + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet, True); end; destructor TGocciaJSON5Builtin.Destroy; diff --git a/source/units/Goccia.Builtins.JSONL.pas b/source/units/Goccia.Builtins.JSONL.pas index 12ad6869..5464c261 100644 --- a/source/units/Goccia.Builtins.JSONL.pas +++ b/source/units/Goccia.Builtins.JSONL.pas @@ -75,7 +75,7 @@ constructor TGocciaJSONLBuiltin.Create(const AName: string; end; RegisterMemberDefinitions(FBuiltinObject, FStaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet); + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet, True); end; destructor TGocciaJSONLBuiltin.Destroy; diff --git a/source/units/Goccia.Builtins.Math.pas b/source/units/Goccia.Builtins.Math.pas index 08515fa1..9f4f6525 100644 --- a/source/units/Goccia.Builtins.Math.pas +++ b/source/units/Goccia.Builtins.Math.pas @@ -151,7 +151,7 @@ constructor TGocciaMath.Create(const AName: string; const AScope: TGocciaScope; end; RegisterMemberDefinitions(FBuiltinObject, FStaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet); + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet, True); end; // §21.3.2.1 Math.abs ( x ) diff --git a/source/units/Goccia.Builtins.Performance.pas b/source/units/Goccia.Builtins.Performance.pas index 9027dfe2..d5821c90 100644 --- a/source/units/Goccia.Builtins.Performance.pas +++ b/source/units/Goccia.Builtins.Performance.pas @@ -239,7 +239,7 @@ constructor TGocciaPerformance.Create(const AName: string; const AScope: TGoccia TGocciaObjectValue.InitializeSharedPrototype; FBuiltinObject := TGocciaPerformanceValue.Create; - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet); + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet, True); end; initialization diff --git a/source/units/Goccia.Builtins.TOML.pas b/source/units/Goccia.Builtins.TOML.pas index 685c8704..394ede16 100644 --- a/source/units/Goccia.Builtins.TOML.pas +++ b/source/units/Goccia.Builtins.TOML.pas @@ -63,7 +63,7 @@ constructor TGocciaTOMLBuiltin.Create(const AName: string; end; RegisterMemberDefinitions(FBuiltinObject, FStaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet); + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet, True); end; destructor TGocciaTOMLBuiltin.Destroy; diff --git a/source/units/Goccia.Builtins.TSV.pas b/source/units/Goccia.Builtins.TSV.pas index f5da3a91..cf6467eb 100644 --- a/source/units/Goccia.Builtins.TSV.pas +++ b/source/units/Goccia.Builtins.TSV.pas @@ -81,7 +81,7 @@ constructor TGocciaTSVBuiltin.Create(const AName: string; end; RegisterMemberDefinitions(FBuiltinObject, FStaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet); + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet, True); end; destructor TGocciaTSVBuiltin.Destroy; diff --git a/source/units/Goccia.Builtins.Temporal.pas b/source/units/Goccia.Builtins.Temporal.pas index 233b8f72..6fa5ecd6 100644 --- a/source/units/Goccia.Builtins.Temporal.pas +++ b/source/units/Goccia.Builtins.Temporal.pas @@ -140,7 +140,7 @@ constructor TGocciaTemporalBuiltin.Create(const AName: string; const AScope: TGo [pfConfigurable]); RegisterMemberDefinitions(FTemporalNamespace, TemporalMembers); - AScope.DefineLexicalBinding(AName, FTemporalNamespace, dtLet); + AScope.DefineLexicalBinding(AName, FTemporalNamespace, dtLet, True); finally TGarbageCollector.Instance.RemoveTempRoot(FTemporalNamespace); end; diff --git a/source/units/Goccia.Builtins.TestingLibrary.pas b/source/units/Goccia.Builtins.TestingLibrary.pas index 4956de56..bced84f6 100644 --- a/source/units/Goccia.Builtins.TestingLibrary.pas +++ b/source/units/Goccia.Builtins.TestingLibrary.pas @@ -2384,7 +2384,7 @@ constructor TGocciaTestAssertions.Create(const AName: string; const AScope: TGoc GlobalObject.DefineProperty(AName, TGocciaPropertyDescriptorData.Create(AValue, [pfWritable, pfConfigurable])) else - AScope.DefineLexicalBinding(AName, AValue, dtConst); + AScope.DefineLexicalBinding(AName, AValue, dtConst, True); end; begin @@ -2419,8 +2419,8 @@ constructor TGocciaTestAssertions.Create(const AName: string; const AScope: TGoc // Private aliases used by generated Test262 wrappers. Some conformance // tests intentionally declare globals named describe/test. AScope.DefineLexicalBinding('__gocciaTest262Describe', DescribeFunction, - dtConst); - AScope.DefineLexicalBinding('__gocciaTest262Test', TestFunction, dtConst); + dtConst, True); + AScope.DefineLexicalBinding('__gocciaTest262Test', TestFunction, dtConst, True); ItFunction := TGocciaNativeFunctionValue.Create(It, 'it', 2); ConfigureTestFunction(ItFunction); diff --git a/source/units/Goccia.Builtins.YAML.pas b/source/units/Goccia.Builtins.YAML.pas index c0f560e8..24c627d3 100644 --- a/source/units/Goccia.Builtins.YAML.pas +++ b/source/units/Goccia.Builtins.YAML.pas @@ -66,7 +66,7 @@ constructor TGocciaYAMLBuiltin.Create(const AName: string; end; RegisterMemberDefinitions(FBuiltinObject, FStaticMembers); - AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet); + AScope.DefineLexicalBinding(AName, FBuiltinObject, dtLet, True); end; destructor TGocciaYAMLBuiltin.Destroy; diff --git a/source/units/Goccia.Engine.pas b/source/units/Goccia.Engine.pas index 5dfc452a..4ed872f7 100644 --- a/source/units/Goccia.Engine.pas +++ b/source/units/Goccia.Engine.pas @@ -442,7 +442,7 @@ procedure TGocciaEngine.RegisterGlobal(const AName: string; end else begin - FInterpreter.GlobalScope.DefineLexicalBinding(AName, AValue, dtConst); + FInterpreter.GlobalScope.DefineLexicalBinding(AName, AValue, dtConst, True); FInjectedGlobals.Add(AName); end; end; @@ -587,7 +587,7 @@ procedure TGocciaEngine.ExecuteShims; LoadShimValue(FInterpreter, Shim) else FInterpreter.GlobalScope.DefineLexicalBinding(Shim.Name, - LoadShimValue(FInterpreter, Shim), dtConst); + LoadShimValue(FInterpreter, Shim), dtConst, True); end; end; @@ -745,7 +745,7 @@ procedure TGocciaEngine.RegisterBuiltIns; FBuiltinGlobalString := TGocciaGlobalString.Create(CONSTRUCTOR_STRING, Scope, ThrowError); FBuiltinGlobals := TGocciaGlobals.Create('Globals', Scope, ThrowError); FBuiltinDisposableStack := TGocciaBuiltinDisposableStack.Create('DisposableStack', Scope, ThrowError); - Scope.DefineLexicalBinding(CONSTRUCTOR_ITERATOR, TGocciaIteratorValue.CreateGlobalObject, dtConst); + Scope.DefineLexicalBinding(CONSTRUCTOR_ITERATOR, TGocciaIteratorValue.CreateGlobalObject, dtConst, True); RegisterBuiltinConstructors; end; @@ -892,12 +892,12 @@ procedure TGocciaEngine.RegisterBuiltinConstructors; if Assigned(FBuiltinArrayBuffer) then for Key in FBuiltinArrayBuffer.BuiltinObject.GetAllPropertyNames do ArrayBufferConstructor.SetProperty(Key, FBuiltinArrayBuffer.BuiltinObject.GetProperty(Key)); - FInterpreter.GlobalScope.DefineLexicalBinding(CONSTRUCTOR_ARRAY_BUFFER, ArrayBufferConstructor, dtConst); + FInterpreter.GlobalScope.DefineLexicalBinding(CONSTRUCTOR_ARRAY_BUFFER, ArrayBufferConstructor, dtConst, True); SharedArrayBufferConstructor := TGocciaSharedArrayBufferClassValue.Create(CONSTRUCTOR_SHARED_ARRAY_BUFFER, nil); TGocciaSharedArrayBufferValue.ExposePrototype(SharedArrayBufferConstructor); SharedArrayBufferConstructor.Prototype.Prototype := ObjectConstructor.Prototype; - FInterpreter.GlobalScope.DefineLexicalBinding(CONSTRUCTOR_SHARED_ARRAY_BUFFER, SharedArrayBufferConstructor, dtConst); + FInterpreter.GlobalScope.DefineLexicalBinding(CONSTRUCTOR_SHARED_ARRAY_BUFFER, SharedArrayBufferConstructor, dtConst, True); // Create %TypedArray% intrinsic (not globally exposed per spec §23.2.1) FTypedArrayIntrinsic := TGocciaClassValue.Create('TypedArray', nil); @@ -992,7 +992,7 @@ procedure TGocciaEngine.RegisterBuiltinConstructors; TGocciaClassValue.PatchDefaultPrototype(NumberConstructor); TGocciaClassValue.PatchDefaultPrototype(BooleanConstructor); TGocciaClassValue.PatchDefaultPrototype(FunctionConstructor); - FInterpreter.GlobalScope.DefineLexicalBinding('Function', FunctionConstructor, dtConst); + FInterpreter.GlobalScope.DefineLexicalBinding('Function', FunctionConstructor, dtConst, True); // ES2026 §20.4.3: Symbol.prototype's [[Prototype]] is %Object.prototype% if Assigned(TGocciaSymbolValue.SharedPrototype) then @@ -1048,7 +1048,7 @@ procedure TGocciaEngine.RegisterTypedArrayConstructor(const AName: string; const TGocciaTypedArrayValue.SetUint8Prototype(TAConstructor.Prototype); end; - FInterpreter.GlobalScope.DefineLexicalBinding(AName, TAConstructor, dtConst); + FInterpreter.GlobalScope.DefineLexicalBinding(AName, TAConstructor, dtConst, True); end; procedure TGocciaEngine.RegisterGlobalThis; @@ -1084,7 +1084,7 @@ procedure TGocciaEngine.RegisterGlobalThis; if Scope.ContainsOwnLexicalBinding('globalThis') then Scope.ForceUpdateBinding('globalThis', GlobalThisObj) else - Scope.DefineLexicalBinding('globalThis', GlobalThisObj, dtConst); + Scope.DefineLexicalBinding('globalThis', GlobalThisObj, dtConst, True); // ES2026 §9.1.2.5 NewGlobalEnvironment: a global Environment Record's // [[GlobalThisValue]] is the global object. Top-level `this` resolves @@ -1150,7 +1150,7 @@ procedure TGocciaEngine.RegisterGocciaScriptGlobal; GocciaObj.AssignProperty('gc', GCFunc); FGocciaGlobal := GocciaObj; - FInterpreter.GlobalScope.DefineLexicalBinding('Goccia', FGocciaGlobal, dtConst); + FInterpreter.GlobalScope.DefineLexicalBinding('Goccia', FGocciaGlobal, dtConst, True); end; function TGocciaEngine.GetResolver: TGocciaModuleResolver; diff --git a/source/units/Goccia.ObjectModel.Engine.pas b/source/units/Goccia.ObjectModel.Engine.pas index cffdd780..155fd1d7 100644 --- a/source/units/Goccia.ObjectModel.Engine.pas +++ b/source/units/Goccia.ObjectModel.Engine.pas @@ -136,7 +136,7 @@ procedure RegisterTypeDefinition(const AScope: TGocciaScope; ASpeciesGetter, 'get [Symbol.species]', 0), nil, [pfConfigurable])); - AScope.DefineLexicalBinding(ATypeDefinition.ConstructorName, AConstructor, dtConst); + AScope.DefineLexicalBinding(ATypeDefinition.ConstructorName, AConstructor, dtConst, True); end; end. diff --git a/source/units/Goccia.Runtime.pas b/source/units/Goccia.Runtime.pas index 98fb8428..a48aa5fa 100644 --- a/source/units/Goccia.Runtime.pas +++ b/source/units/Goccia.Runtime.pas @@ -812,7 +812,7 @@ procedure TGocciaRuntimeExtension.RegisterRuntimeConstructors; begin PerformanceConstructor := TGocciaPerformance.CreateInterfaceObject; FEngine.Interpreter.GlobalScope.DefineLexicalBinding( - CONSTRUCTOR_PERFORMANCE, PerformanceConstructor, dtConst); + CONSTRUCTOR_PERFORMANCE, PerformanceConstructor, dtConst, True); end; end; diff --git a/source/units/Goccia.Scope.BindingMap.pas b/source/units/Goccia.Scope.BindingMap.pas index 11082d80..16dce310 100644 --- a/source/units/Goccia.Scope.BindingMap.pas +++ b/source/units/Goccia.Scope.BindingMap.pas @@ -23,6 +23,7 @@ TLexicalBinding = record Value: TGocciaValue; DeclarationType: TGocciaDeclarationType; Initialized: Boolean; + BuiltIn: Boolean; { Strict-types annotation enforced on every assignment. Default sltUntyped means no enforcement (typical untyped binding). } TypeHint: TGocciaLocalType; diff --git a/source/units/Goccia.Scope.Redeclaration.pas b/source/units/Goccia.Scope.Redeclaration.pas index 1fe235a4..66d5c2c6 100644 --- a/source/units/Goccia.Scope.Redeclaration.pas +++ b/source/units/Goccia.Scope.Redeclaration.pas @@ -76,14 +76,19 @@ procedure CheckTopLevelRedeclarations(const AProgram: TGocciaProgram; if Stmt is TGocciaVariableDeclaration then begin VarDecl := TGocciaVariableDeclaration(Stmt); - for J := 0 to High(VarDecl.Variables) do - begin - DeclName := VarDecl.Variables[J].Name; - if AScope.ContainsOwnLexicalBinding(DeclName) then - raise TGocciaSyntaxError.Create( - SysUtils.Format('Identifier ''%s'' has already been declared', - [DeclName]), Stmt.Line, Stmt.Column, ASourcePath, nil); - end; + // §16.1.7: var and function declarations may shadow built-in + // globals in script mode. Only block the redeclaration when the + // existing binding is a user-declared lexical (let/const/class). + if not (VarDecl.IsVar or VarDecl.IsFunctionDeclaration) then + for J := 0 to High(VarDecl.Variables) do + begin + DeclName := VarDecl.Variables[J].Name; + if AScope.ContainsOwnLexicalBinding(DeclName) and + not AScope.IsBuiltInBinding(DeclName) then + raise TGocciaSyntaxError.Create( + SysUtils.Format('Identifier ''%s'' has already been declared', + [DeclName]), Stmt.Line, Stmt.Column, ASourcePath, nil); + end; end else if Stmt is TGocciaClassDeclaration then begin diff --git a/source/units/Goccia.Scope.pas b/source/units/Goccia.Scope.pas index ed523c47..d3440ae9 100644 --- a/source/units/Goccia.Scope.pas +++ b/source/units/Goccia.Scope.pas @@ -50,7 +50,7 @@ TGocciaScope = class(TGCManagedObject) // New Define/Assign pattern procedure PredeclareLexicalBinding(const AName: string; const ADeclarationType: TGocciaDeclarationType; const ALine: Integer = 0; const AColumn: Integer = 0); - procedure DefineLexicalBinding(const AName: string; const AValue: TGocciaValue; const ADeclarationType: TGocciaDeclarationType; const ALine: Integer = 0; const AColumn: Integer = 0); + procedure DefineLexicalBinding(const AName: string; const AValue: TGocciaValue; const ADeclarationType: TGocciaDeclarationType; const ABuiltIn: Boolean = False; const ALine: Integer = 0; const AColumn: Integer = 0); procedure AssignBinding(const AName: string; const AValue: TGocciaValue; const ALine: Integer = 0; const AColumn: Integer = 0); virtual; procedure ForceUpdateBinding(const AName: string; const AValue: TGocciaValue); @@ -74,6 +74,7 @@ TGocciaScope = class(TGCManagedObject) function ResolveIdentifier(const AName: string): TGocciaValue; inline; function ContainsOwnLexicalBinding(const AName: string): Boolean; inline; + function IsBuiltInBinding(const AName: string): Boolean; function Contains(const AName: string): Boolean; inline; function GetOwnBindingNames: TGocciaStringArray; inline; @@ -348,7 +349,7 @@ procedure TGocciaScope.PredeclareLexicalBinding(const AName: string; FLexicalBindings.Add(AName, LexicalBinding); end; -procedure TGocciaScope.DefineLexicalBinding(const AName: string; const AValue: TGocciaValue; const ADeclarationType: TGocciaDeclarationType; const ALine: Integer = 0; const AColumn: Integer = 0); +procedure TGocciaScope.DefineLexicalBinding(const AName: string; const AValue: TGocciaValue; const ADeclarationType: TGocciaDeclarationType; const ABuiltIn: Boolean = False; const ALine: Integer = 0; const AColumn: Integer = 0); var LexicalBinding: TLexicalBinding; ExistingLexicalBinding: TLexicalBinding; @@ -386,6 +387,7 @@ procedure TGocciaScope.DefineLexicalBinding(const AName: string; const AValue: T // - dtLet: no TDZ after declaration statement is processed // - dtParameter: parameters have no TDZ LexicalBinding.Initialized := True; + LexicalBinding.BuiltIn := ABuiltIn; LexicalBinding.TypeHint := sltUntyped; FLexicalBindings.AddOrSetValue(AName, LexicalBinding); @@ -422,9 +424,24 @@ procedure TGocciaScope.DefineVariableBinding(const AName: string; const AValue: var TargetScope: TGocciaScope; Binding: TLexicalBinding; + ExistingBuiltIn: TLexicalBinding; + EffectiveValue: TGocciaValue; GlobalObject: TGocciaObjectValue; begin TargetScope := FindFunctionOrModuleScope; + EffectiveValue := AValue; + + // §16.1.7: var/function declarations may shadow built-in globals in + // script mode. Remove the lexical binding so the var binding is + // visible through GetBinding. Preserve the original value when the + // declaration has no initializer (e.g. bare `var NaN;`). + if TargetScope.FLexicalBindings.TryGetValue(AName, ExistingBuiltIn) and + ExistingBuiltIn.BuiltIn then + begin + if not AHasInitializer then + EffectiveValue := ExistingBuiltIn.Value; + TargetScope.FLexicalBindings.Remove(AName); + end; if (TargetScope.FScopeKind = skGlobal) and (TargetScope.FThisValue is TGocciaObjectValue) then @@ -432,7 +449,7 @@ procedure TGocciaScope.DefineVariableBinding(const AName: string; const AValue: GlobalObject := TGocciaObjectValue(TargetScope.FThisValue); if (not AHasInitializer) and GlobalObject.HasOwnProperty(AName) then Exit; - GlobalObject.AssignProperty(AName, AValue); + GlobalObject.AssignProperty(AName, EffectiveValue); Exit; end; @@ -445,14 +462,14 @@ procedure TGocciaScope.DefineVariableBinding(const AName: string; const AValue: // Redeclaration: only update if the source had a syntactic initializer. if AHasInitializer then begin - Binding.Value := AValue; + Binding.Value := EffectiveValue; TargetScope.FVarBindings.AddOrSetValue(AName, Binding); end; end else begin // First declaration: create the binding - Binding.Value := AValue; + Binding.Value := EffectiveValue; Binding.DeclarationType := dtVar; Binding.Initialized := True; Binding.TypeHint := sltUntyped; @@ -610,6 +627,13 @@ function TGocciaScope.ContainsOwnLexicalBinding(const AName: string): Boolean; i Result := FLexicalBindings.ContainsKey(AName); end; +function TGocciaScope.IsBuiltInBinding(const AName: string): Boolean; +var + Binding: TLexicalBinding; +begin + Result := FLexicalBindings.TryGetValue(AName, Binding) and Binding.BuiltIn; +end; + function TGocciaScope.Contains(const AName: string): Boolean; inline; begin Result := ContainsOwnLexicalBinding(AName) or diff --git a/tests/language/declarations/const/cannot-shadow-builtin-globals.js b/tests/language/declarations/const/cannot-shadow-builtin-globals.js new file mode 100644 index 00000000..4704f9f2 --- /dev/null +++ b/tests/language/declarations/const/cannot-shadow-builtin-globals.js @@ -0,0 +1,24 @@ +/*--- +description: const declarations cannot shadow built-in globals at the same scope level +features: [const-declaration] +---*/ + +test("const NaN at top level throws SyntaxError", () => { + expect(() => { + eval("const NaN = 42"); + }).toThrow(); +}); + +test("const Array at top level throws SyntaxError", () => { + expect(() => { + eval("const Array = []"); + }).toThrow(); +}); + +test("const in a nested block does not conflict with built-in globals", () => { + { + const NaN = 42; + expect(NaN).toBe(42); + } + expect(NaN).toBeNaN(); +}); diff --git a/tests/language/declarations/let/cannot-shadow-builtin-globals.js b/tests/language/declarations/let/cannot-shadow-builtin-globals.js new file mode 100644 index 00000000..3c772188 --- /dev/null +++ b/tests/language/declarations/let/cannot-shadow-builtin-globals.js @@ -0,0 +1,36 @@ +/*--- +description: let declarations cannot shadow built-in globals at the same scope level +features: [let-declaration] +---*/ + +test("let NaN at top level throws SyntaxError", () => { + expect(() => { + eval("let NaN = 42"); + }).toThrow(); +}); + +test("let Infinity at top level throws SyntaxError", () => { + expect(() => { + eval("let Infinity = 42"); + }).toThrow(); +}); + +test("let undefined at top level throws SyntaxError", () => { + expect(() => { + eval("let undefined = 42"); + }).toThrow(); +}); + +test("let Array at top level throws SyntaxError", () => { + expect(() => { + eval("let Array = 42"); + }).toThrow(); +}); + +test("let in a nested block does not conflict with built-in globals", () => { + { + let NaN = 42; + expect(NaN).toBe(42); + } + expect(NaN).toBeNaN(); +}); diff --git a/tests/language/function-keyword/shadow-builtin-globals.js b/tests/language/function-keyword/shadow-builtin-globals.js new file mode 100644 index 00000000..6da2f355 --- /dev/null +++ b/tests/language/function-keyword/shadow-builtin-globals.js @@ -0,0 +1,22 @@ +/*--- +description: function declarations may shadow built-in globals in script mode (§16.1.7) +features: [compat-function, compat-var] +---*/ + +test("function declaration shadows built-in Array", () => { + function Array() { return "custom"; } + expect(typeof Array).toBe("function"); + expect(Array()).toBe("custom"); +}); + +test("function declaration shadows built-in Object", () => { + function Object() { return "custom-object"; } + expect(typeof Object).toBe("function"); + expect(Object()).toBe("custom-object"); +}); + +test("function declaration shadows built-in Error", () => { + function Error() { return "custom-error"; } + expect(typeof Error).toBe("function"); + expect(Error()).toBe("custom-error"); +}); diff --git a/tests/language/var/shadow-builtin-globals.js b/tests/language/var/shadow-builtin-globals.js new file mode 100644 index 00000000..69a49790 --- /dev/null +++ b/tests/language/var/shadow-builtin-globals.js @@ -0,0 +1,35 @@ +/*--- +description: top-level var declarations may shadow built-in globals in script mode (§16.1.7) +features: [compat-var] +---*/ + +var NaN; +var Infinity; +var undefined; + +test("top-level var NaN without initializer preserves the built-in value", () => { + expect(typeof NaN).toBe("number"); +}); + +test("top-level var Infinity without initializer preserves the built-in value", () => { + expect(typeof Infinity).toBe("number"); +}); + +test("top-level var undefined without initializer preserves the built-in value", () => { + expect(typeof undefined).toBe("undefined"); +}); + +var Array = "shadowed"; + +test("top-level var Array with initializer shadows the constructor", () => { + expect(Array).toBe("shadowed"); +}); + +test("var inside a function creates a local binding, does not touch the global", () => { + const fn = () => { + var Map = "local"; + return Map; + }; + expect(fn()).toBe("local"); + expect(typeof Map).toBe("function"); +}); From b5c6896aab6c505e4b92426033b111db696ae2c0 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Fri, 8 May 2026 11:54:08 +0100 Subject: [PATCH 2/5] Fix review issues: BuiltIn init, redeclaration logic, and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Initialize BuiltIn := False in PredeclareLexicalBinding to avoid undefined record field values. - Restructure CheckTopLevelRedeclarations so var/function declarations still error against user-declared lexical bindings (let/const from a prior script evaluation) — only built-in bindings are exempt. - Move function shadowing tests to file level so they exercise the CheckTopLevelRedeclarations path. - Remove eval-based let/const tests (eval is undefined in GocciaScript); replace with valid block-scope shadowing tests. - Use bare `var NaN;` (not `var NaN = 42`) since NaN on globalThis is non-writable per §19.1 and strict mode correctly throws TypeError on assignment to non-writable properties. Co-Authored-By: Claude Opus 4.6 (1M context) --- source/units/Goccia.Scope.Redeclaration.pas | 24 ++++++----- source/units/Goccia.Scope.pas | 1 + .../const/cannot-shadow-builtin-globals.js | 24 +++++------ .../let/cannot-shadow-builtin-globals.js | 42 ++++++++----------- .../shadow-builtin-globals.js | 13 +++--- tests/language/var/shadow-builtin-globals.js | 3 +- 6 files changed, 50 insertions(+), 57 deletions(-) diff --git a/source/units/Goccia.Scope.Redeclaration.pas b/source/units/Goccia.Scope.Redeclaration.pas index 66d5c2c6..68692273 100644 --- a/source/units/Goccia.Scope.Redeclaration.pas +++ b/source/units/Goccia.Scope.Redeclaration.pas @@ -76,19 +76,21 @@ procedure CheckTopLevelRedeclarations(const AProgram: TGocciaProgram; if Stmt is TGocciaVariableDeclaration then begin VarDecl := TGocciaVariableDeclaration(Stmt); - // §16.1.7: var and function declarations may shadow built-in - // globals in script mode. Only block the redeclaration when the - // existing binding is a user-declared lexical (let/const/class). - if not (VarDecl.IsVar or VarDecl.IsFunctionDeclaration) then - for J := 0 to High(VarDecl.Variables) do + for J := 0 to High(VarDecl.Variables) do + begin + DeclName := VarDecl.Variables[J].Name; + if AScope.ContainsOwnLexicalBinding(DeclName) then begin - DeclName := VarDecl.Variables[J].Name; - if AScope.ContainsOwnLexicalBinding(DeclName) and - not AScope.IsBuiltInBinding(DeclName) then - raise TGocciaSyntaxError.Create( - SysUtils.Format('Identifier ''%s'' has already been declared', - [DeclName]), Stmt.Line, Stmt.Column, ASourcePath, nil); + // §16.1.7: var/function declarations may shadow built-in + // globals in script mode, but not user-declared bindings. + if (VarDecl.IsVar or VarDecl.IsFunctionDeclaration) and + AScope.IsBuiltInBinding(DeclName) then + Continue; + raise TGocciaSyntaxError.Create( + SysUtils.Format('Identifier ''%s'' has already been declared', + [DeclName]), Stmt.Line, Stmt.Column, ASourcePath, nil); end; + end; end else if Stmt is TGocciaClassDeclaration then begin diff --git a/source/units/Goccia.Scope.pas b/source/units/Goccia.Scope.pas index d3440ae9..b08dd031 100644 --- a/source/units/Goccia.Scope.pas +++ b/source/units/Goccia.Scope.pas @@ -345,6 +345,7 @@ procedure TGocciaScope.PredeclareLexicalBinding(const AName: string; LexicalBinding.Value := TGocciaUndefinedLiteralValue.UndefinedValue; LexicalBinding.DeclarationType := ADeclarationType; LexicalBinding.Initialized := False; + LexicalBinding.BuiltIn := False; LexicalBinding.TypeHint := sltUntyped; FLexicalBindings.Add(AName, LexicalBinding); end; diff --git a/tests/language/declarations/const/cannot-shadow-builtin-globals.js b/tests/language/declarations/const/cannot-shadow-builtin-globals.js index 4704f9f2..b11df7b4 100644 --- a/tests/language/declarations/const/cannot-shadow-builtin-globals.js +++ b/tests/language/declarations/const/cannot-shadow-builtin-globals.js @@ -1,24 +1,20 @@ /*--- -description: const declarations cannot shadow built-in globals at the same scope level +description: const cannot shadow built-in globals at the same scope level but can in nested blocks features: [const-declaration] ---*/ -test("const NaN at top level throws SyntaxError", () => { - expect(() => { - eval("const NaN = 42"); - }).toThrow(); -}); - -test("const Array at top level throws SyntaxError", () => { - expect(() => { - eval("const Array = []"); - }).toThrow(); -}); - -test("const in a nested block does not conflict with built-in globals", () => { +test("const in a nested block shadows built-in NaN locally", () => { { const NaN = 42; expect(NaN).toBe(42); } expect(NaN).toBeNaN(); }); + +test("const in a nested block shadows built-in Array locally", () => { + { + const Array = "not-an-array"; + expect(Array).toBe("not-an-array"); + } + expect(typeof Array).toBe("function"); +}); diff --git a/tests/language/declarations/let/cannot-shadow-builtin-globals.js b/tests/language/declarations/let/cannot-shadow-builtin-globals.js index 3c772188..2eb9ce4c 100644 --- a/tests/language/declarations/let/cannot-shadow-builtin-globals.js +++ b/tests/language/declarations/let/cannot-shadow-builtin-globals.js @@ -1,36 +1,28 @@ /*--- -description: let declarations cannot shadow built-in globals at the same scope level +description: let cannot shadow built-in globals at the same scope level but can in nested blocks features: [let-declaration] ---*/ -test("let NaN at top level throws SyntaxError", () => { - expect(() => { - eval("let NaN = 42"); - }).toThrow(); -}); - -test("let Infinity at top level throws SyntaxError", () => { - expect(() => { - eval("let Infinity = 42"); - }).toThrow(); -}); - -test("let undefined at top level throws SyntaxError", () => { - expect(() => { - eval("let undefined = 42"); - }).toThrow(); +test("let in a nested block shadows built-in NaN locally", () => { + { + let NaN = 42; + expect(NaN).toBe(42); + } + expect(NaN).toBeNaN(); }); -test("let Array at top level throws SyntaxError", () => { - expect(() => { - eval("let Array = 42"); - }).toThrow(); +test("let in a nested block shadows built-in Infinity locally", () => { + { + let Infinity = "finite"; + expect(Infinity).toBe("finite"); + } + expect(Infinity).toBe(1 / 0); }); -test("let in a nested block does not conflict with built-in globals", () => { +test("let in a nested block shadows built-in Array locally", () => { { - let NaN = 42; - expect(NaN).toBe(42); + let Array = "not-an-array"; + expect(Array).toBe("not-an-array"); } - expect(NaN).toBeNaN(); + expect(typeof Array).toBe("function"); }); diff --git a/tests/language/function-keyword/shadow-builtin-globals.js b/tests/language/function-keyword/shadow-builtin-globals.js index 6da2f355..05b22222 100644 --- a/tests/language/function-keyword/shadow-builtin-globals.js +++ b/tests/language/function-keyword/shadow-builtin-globals.js @@ -3,20 +3,21 @@ description: function declarations may shadow built-in globals in script mode ( features: [compat-function, compat-var] ---*/ -test("function declaration shadows built-in Array", () => { - function Array() { return "custom"; } +function Array() { return "custom"; } +function Object() { return "custom-object"; } +function Error() { return "custom-error"; } + +test("top-level function declaration shadows built-in Array", () => { expect(typeof Array).toBe("function"); expect(Array()).toBe("custom"); }); -test("function declaration shadows built-in Object", () => { - function Object() { return "custom-object"; } +test("top-level function declaration shadows built-in Object", () => { expect(typeof Object).toBe("function"); expect(Object()).toBe("custom-object"); }); -test("function declaration shadows built-in Error", () => { - function Error() { return "custom-error"; } +test("top-level function declaration shadows built-in Error", () => { expect(typeof Error).toBe("function"); expect(Error()).toBe("custom-error"); }); diff --git a/tests/language/var/shadow-builtin-globals.js b/tests/language/var/shadow-builtin-globals.js index 69a49790..bc471a13 100644 --- a/tests/language/var/shadow-builtin-globals.js +++ b/tests/language/var/shadow-builtin-globals.js @@ -13,6 +13,7 @@ test("top-level var NaN without initializer preserves the built-in value", () => test("top-level var Infinity without initializer preserves the built-in value", () => { expect(typeof Infinity).toBe("number"); + expect(Infinity).toBe(1 / 0); }); test("top-level var undefined without initializer preserves the built-in value", () => { @@ -21,7 +22,7 @@ test("top-level var undefined without initializer preserves the built-in value", var Array = "shadowed"; -test("top-level var Array with initializer shadows the constructor", () => { +test("top-level var with initializer shadows a writable built-in", () => { expect(Array).toBe("shadowed"); }); From 1309295685e7edc9ff0c32bc6a214b1c20682077 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Fri, 8 May 2026 11:59:20 +0100 Subject: [PATCH 3/5] Add function-scoped var NaN = 42 test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bare `var NaN;` at the top level tests the §16.1.7 declaration path. Function-scoped `var NaN = 42` verifies a local binding is created with the initializer value (42), independent of the global NaN property. Top-level `var NaN = 42` correctly throws TypeError because globalThis.NaN is {writable: false} per §19.1 and strict mode rejects the assignment, but testing that path via toThrow is blocked by a pre-existing segfault in the test library when the callback does not throw. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/language/var/shadow-builtin-globals.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/language/var/shadow-builtin-globals.js b/tests/language/var/shadow-builtin-globals.js index bc471a13..c867b034 100644 --- a/tests/language/var/shadow-builtin-globals.js +++ b/tests/language/var/shadow-builtin-globals.js @@ -11,6 +11,14 @@ test("top-level var NaN without initializer preserves the built-in value", () => expect(typeof NaN).toBe("number"); }); +test("function-scoped var NaN with initializer creates a local binding", () => { + const fn = () => { + var NaN = 42; + return NaN; + }; + expect(fn()).toBe(42); +}); + test("top-level var Infinity without initializer preserves the built-in value", () => { expect(typeof Infinity).toBe("number"); expect(Infinity).toBe(1 / 0); From f4bf3eae8d8e9b60d513402ab769ba7d1de63560 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Fri, 8 May 2026 12:09:12 +0100 Subject: [PATCH 4/5] Guard built-in shadow check with skGlobal scope kind Built-in bindings only exist on the global scope. Skip the TryGetValue probe in DefineVariableBinding for function/module-scoped vars, which is the common case during execution. Co-Authored-By: Claude Opus 4.6 (1M context) --- source/units/Goccia.Scope.pas | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/units/Goccia.Scope.pas b/source/units/Goccia.Scope.pas index b08dd031..dca2dff6 100644 --- a/source/units/Goccia.Scope.pas +++ b/source/units/Goccia.Scope.pas @@ -433,10 +433,10 @@ procedure TGocciaScope.DefineVariableBinding(const AName: string; const AValue: EffectiveValue := AValue; // §16.1.7: var/function declarations may shadow built-in globals in - // script mode. Remove the lexical binding so the var binding is - // visible through GetBinding. Preserve the original value when the - // declaration has no initializer (e.g. bare `var NaN;`). - if TargetScope.FLexicalBindings.TryGetValue(AName, ExistingBuiltIn) and + // script mode. Only the global scope carries built-in bindings, so + // skip the lookup for function/module-scoped vars (the common case). + if (TargetScope.FScopeKind = skGlobal) and + TargetScope.FLexicalBindings.TryGetValue(AName, ExistingBuiltIn) and ExistingBuiltIn.BuiltIn then begin if not AHasInitializer then From d364e46fe620c012050d152b0666811722d3dbbc Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Fri, 8 May 2026 12:27:03 +0100 Subject: [PATCH 5/5] Fix four BuiltIn flag correctness issues from review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Injected globals (RegisterGlobal) no longer marked BuiltIn — they are user-provided, not engine intrinsics, and should not be shadowable. - CheckPatternRedeclarations now receives the IsVar flag so var destructuring (e.g. `var { NaN } = obj;`) gets the same built-in exemption as simple var declarations. Closes #580. - DefineLexicalBinding's predeclared-materialization branch now propagates ABuiltIn onto the existing binding instead of silently dropping it. - DefineVariableBinding's first-declaration branch explicitly sets Binding.BuiltIn := False to avoid uninitialized record fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- source/units/Goccia.Engine.pas | 2 +- source/units/Goccia.Scope.Redeclaration.pas | 26 +++++++++++++-------- source/units/Goccia.Scope.pas | 2 ++ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/source/units/Goccia.Engine.pas b/source/units/Goccia.Engine.pas index 4ed872f7..3bf627aa 100644 --- a/source/units/Goccia.Engine.pas +++ b/source/units/Goccia.Engine.pas @@ -442,7 +442,7 @@ procedure TGocciaEngine.RegisterGlobal(const AName: string; end else begin - FInterpreter.GlobalScope.DefineLexicalBinding(AName, AValue, dtConst, True); + FInterpreter.GlobalScope.DefineLexicalBinding(AName, AValue, dtConst); FInjectedGlobals.Add(AName); end; end; diff --git a/source/units/Goccia.Scope.Redeclaration.pas b/source/units/Goccia.Scope.Redeclaration.pas index 68692273..8fc0b732 100644 --- a/source/units/Goccia.Scope.Redeclaration.pas +++ b/source/units/Goccia.Scope.Redeclaration.pas @@ -22,27 +22,32 @@ implementation procedure CheckPatternRedeclarations( const APattern: TGocciaDestructuringPattern; - const AScope: TGocciaScope; const ASourcePath: string); + const AScope: TGocciaScope; const ASourcePath: string; + const AIsVar: Boolean); var ObjPat: TGocciaObjectDestructuringPattern; ArrPat: TGocciaArrayDestructuringPattern; + DeclName: string; I: Integer; begin if APattern is TGocciaIdentifierDestructuringPattern then begin - if AScope.ContainsOwnLexicalBinding( - TGocciaIdentifierDestructuringPattern(APattern).Name) then + DeclName := TGocciaIdentifierDestructuringPattern(APattern).Name; + if AScope.ContainsOwnLexicalBinding(DeclName) then + begin + if AIsVar and AScope.IsBuiltInBinding(DeclName) then + Exit; raise TGocciaSyntaxError.Create( SysUtils.Format('Identifier ''%s'' has already been declared', - [TGocciaIdentifierDestructuringPattern(APattern).Name]), - 0, 0, ASourcePath, nil); + [DeclName]), 0, 0, ASourcePath, nil); + end; end else if APattern is TGocciaObjectDestructuringPattern then begin ObjPat := TGocciaObjectDestructuringPattern(APattern); for I := 0 to ObjPat.Properties.Count - 1 do CheckPatternRedeclarations(ObjPat.Properties[I].Pattern, - AScope, ASourcePath); + AScope, ASourcePath, AIsVar); end else if APattern is TGocciaArrayDestructuringPattern then begin @@ -50,16 +55,16 @@ procedure CheckPatternRedeclarations( for I := 0 to ArrPat.Elements.Count - 1 do if Assigned(ArrPat.Elements[I]) then CheckPatternRedeclarations(ArrPat.Elements[I], - AScope, ASourcePath); + AScope, ASourcePath, AIsVar); end else if APattern is TGocciaAssignmentDestructuringPattern then CheckPatternRedeclarations( TGocciaAssignmentDestructuringPattern(APattern).Left, - AScope, ASourcePath) + AScope, ASourcePath, AIsVar) else if APattern is TGocciaRestDestructuringPattern then CheckPatternRedeclarations( TGocciaRestDestructuringPattern(APattern).Argument, - AScope, ASourcePath); + AScope, ASourcePath, AIsVar); end; procedure CheckTopLevelRedeclarations(const AProgram: TGocciaProgram; @@ -102,7 +107,8 @@ procedure CheckTopLevelRedeclarations(const AProgram: TGocciaProgram; end else if Stmt is TGocciaDestructuringDeclaration then CheckPatternRedeclarations( - TGocciaDestructuringDeclaration(Stmt).Pattern, AScope, ASourcePath) + TGocciaDestructuringDeclaration(Stmt).Pattern, AScope, ASourcePath, + TGocciaDestructuringDeclaration(Stmt).IsVar) else if Stmt is TGocciaEnumDeclaration then begin DeclName := TGocciaEnumDeclaration(Stmt).Name; diff --git a/source/units/Goccia.Scope.pas b/source/units/Goccia.Scope.pas index dca2dff6..be701bf1 100644 --- a/source/units/Goccia.Scope.pas +++ b/source/units/Goccia.Scope.pas @@ -364,6 +364,7 @@ procedure TGocciaScope.DefineLexicalBinding(const AName: string; const AValue: T ExistingLexicalBinding.Value := AValue; ExistingLexicalBinding.DeclarationType := ADeclarationType; ExistingLexicalBinding.Initialized := True; + ExistingLexicalBinding.BuiltIn := ABuiltIn; FLexicalBindings.AddOrSetValue(AName, ExistingLexicalBinding); Exit; end; @@ -473,6 +474,7 @@ procedure TGocciaScope.DefineVariableBinding(const AName: string; const AValue: Binding.Value := EffectiveValue; Binding.DeclarationType := dtVar; Binding.Initialized := True; + Binding.BuiltIn := False; Binding.TypeHint := sltUntyped; TargetScope.FVarBindings.AddOrSetValue(AName, Binding); end;