From e131f0e041136ae41d24c7c9f10a07042252e809 Mon Sep 17 00:00:00 2001 From: Johannes Stein Date: Fri, 26 Jun 2026 00:03:50 +0100 Subject: [PATCH] fix(engine): map ShadowRealm.evaluate declaration early errors to SyntaxError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ShadowRealm.prototype.evaluate treats its source as a Script, so Script-level static early errors must surface as caller-realm SyntaxErrors (proposal-shadowrealm PerformShadowRealmEval §3.1.3 step 2). #814 added the ValidateEvalEarlyErrors pre-evaluation pass for reference/assignment early errors but left EvalDeclarationInstantiation conflicts as a tracked follow-up, where they surfaced as the step-15 caller-realm TypeError instead. Add CheckEvalScriptLexicalEarlyError (ECMA-262 §16.1.1) covering the two top-level declaration rules — a duplicate lexically-declared name, and a lexically-declared name that is also var-declared — and run it on the same seam before evaluation so both map to a caller-realm SyntaxError: new ShadowRealm().evaluate("let y; var y;") // SyntaxError (was TypeError) new ShadowRealm().evaluate("var y; let y;") // SyntaxError (was TypeError) new ShadowRealm().evaluate("let x; let x;") // SyntaxError (was TypeError) var is processed only in ShadowRealm child realms, so plain GocciaScript (which excludes var) is unaffected, and distinct lexical/var names still bind normally. built-ins/ShadowRealm test262 stays 64/64 and the full JS suite stays green in both interpreter and bytecode modes. Co-Authored-By: Claude Opus 4.8 --- .../Goccia.Builtins.GlobalShadowRealm.pas | 28 +++-- source/units/Goccia.Evaluator.pas | 105 ++++++++++++++---- tests/built-ins/ShadowRealm/evaluate.js | 18 +++ 3 files changed, 124 insertions(+), 27 deletions(-) diff --git a/source/units/Goccia.Builtins.GlobalShadowRealm.pas b/source/units/Goccia.Builtins.GlobalShadowRealm.pas index ba764e19..7c825601 100644 --- a/source/units/Goccia.Builtins.GlobalShadowRealm.pas +++ b/source/units/Goccia.Builtins.GlobalShadowRealm.pas @@ -575,14 +575,26 @@ function TGocciaShadowRealmHost.Evaluate( VarScope := EvalScope else VarScope := ChildEngine.Interpreter.GlobalScope; - // EvalDeclarationInstantiation: var/function bind in the realm's - // global var environment (so they persist across evaluate calls - // and surface on globalThis); let/const bind in this fresh - // per-call lexical scope, so re-evaluating top-level lexical - // declarations does not clash. - RawResult := EvaluateEvalProgram(ParseResult.ProgramNode, - EvalContext, VarScope, EvalScope, StrictEval, False, nil, - False, False, False); + // ValidateEvalEarlyErrors (above) does not cover + // EvalDeclarationInstantiation conflicts. A duplicate top-level + // lexical name, or a lexical name that is also var-declared, is + // likewise a static Script early error (caller-realm SyntaxError, + // §3.1.3 step 2), not the runtime TypeError that + // EvalDeclarationInstantiation would otherwise produce below. + EarlyErrorMessage := CheckEvalScriptLexicalEarlyError( + ParseResult.ProgramNode, StrictEval, + EvalContext.CompatibilityNonStrictMode); + if EarlyErrorMessage <> '' then + EarlyErrorThrew := True + else + // EvalDeclarationInstantiation: var/function bind in the + // realm's global var environment (so they persist across + // evaluate calls and surface on globalThis); let/const bind in + // this fresh per-call lexical scope, so re-evaluating + // top-level lexical declarations does not clash. + RawResult := EvaluateEvalProgram(ParseResult.ProgramNode, + EvalContext, VarScope, EvalScope, StrictEval, False, nil, + False, False, False); finally if Assigned(TGarbageCollector.Instance) then TGarbageCollector.Instance.RemoveTempRoot(EvalScope); diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index 08adcfe7..69191856 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -147,6 +147,16 @@ procedure ValidateEvalEarlyErrors(const AProgram: TGocciaProgram; const AStrictEval, AAllowNewTarget, AAllowSuperProperty, AAllowSuperCall: Boolean); +{ Complements ValidateEvalEarlyErrors with the eval Script's top-level + declaration early errors that EvalDeclarationInstantiation would otherwise + raise mid-evaluation: a duplicate lexically-declared name, or a + lexically-declared name that is also var-declared (ECMA-262 §16.1.1). Returns + the SyntaxError message for the first violation, or '' if there is none. + Exposed so callers that must distinguish these early errors (SyntaxError) from + runtime abrupt completions (TypeError) can validate up front. } +function CheckEvalScriptLexicalEarlyError(const AProgram: TGocciaProgram; + const AStrictEval, ACompatibilityNonStrict: Boolean): string; + implementation uses @@ -1163,13 +1173,21 @@ procedure CollectEvalFunctionDeclarations( end; end; -procedure CollectTopLevelEvalLexicalNames(const ANodes: TObjectList; - const ANames: TStringList); +// Appends the top-level lexically-declared names (let / const / class / enum, +// including their exported forms) of ANodes to ANames in source order WITHOUT +// de-duplicating, so callers can detect duplicate lexical declarations. Empty +// names are skipped. +procedure AppendTopLevelEvalLexicalNames( + const ANodes: TObjectList; const ANames: TStrings); var VarDecl: TGocciaVariableDeclaration; DestructDecl: TGocciaDestructuringDeclaration; - Names: TStringList; I, J: Integer; + procedure AddRaw(const AName: string); + begin + if AName <> '' then + ANames.Add(AName); + end; begin for I := 0 to ANodes.Count - 1 do begin @@ -1179,7 +1197,7 @@ procedure CollectTopLevelEvalLexicalNames(const ANodes: TObjectList; + const ANames: TStringList); +var + Raw: TStringList; + I: Integer; +begin + Raw := TStringList.Create; + try + Raw.CaseSensitive := True; + AppendTopLevelEvalLexicalNames(ANodes, Raw); + for I := 0 to Raw.Count - 1 do + AddUniqueEvalName(ANames, Raw[I]); + finally + Raw.Free; + end; +end; + +// ECMA-262 §16.1.1 Scripts Static Semantics: Early Errors (also reached from +// PerformEval for eval code). See the interface declaration for the contract. +function CheckEvalScriptLexicalEarlyError(const AProgram: TGocciaProgram; + const AStrictEval, ACompatibilityNonStrict: Boolean): string; +var + LexRaw, LexUnique, VarNames: TStringList; + I: Integer; +begin + Result := ''; + LexRaw := TStringList.Create; + LexUnique := TStringList.Create; + VarNames := TStringList.Create; + try + LexRaw.CaseSensitive := True; + LexUnique.CaseSensitive := True; + VarNames.CaseSensitive := True; + + // It is a Syntax Error if the LexicallyDeclaredNames of ScriptBody contains + // any duplicate entries. + AppendTopLevelEvalLexicalNames(AProgram.Body, LexRaw); + for I := 0 to LexRaw.Count - 1 do + if LexUnique.IndexOf(LexRaw[I]) >= 0 then + Exit(Format(SErrorIdentifierAlreadyDeclared, [LexRaw[I]])) + else + LexUnique.Add(LexRaw[I]); + + // It is a Syntax Error if any element of the LexicallyDeclaredNames of + // ScriptBody also occurs in the VarDeclaredNames of ScriptBody. The + // collection mode mirrors EvalDeclarationInstantiation so the two agree on + // which names are lexically vs var-declared. + CollectVarBindingNamesFromStatements(AProgram.Body, VarNames, + VarBindingNameCollectionMode(not AStrictEval, ACompatibilityNonStrict)); + for I := 0 to LexUnique.Count - 1 do + if VarNames.IndexOf(LexUnique[I]) >= 0 then + Exit(Format(SErrorIdentifierAlreadyDeclared, [LexUnique[I]])); + finally + VarNames.Free; + LexUnique.Free; + LexRaw.Free; end; end; diff --git a/tests/built-ins/ShadowRealm/evaluate.js b/tests/built-ins/ShadowRealm/evaluate.js index fad8c314..bc50d327 100644 --- a/tests/built-ins/ShadowRealm/evaluate.js +++ b/tests/built-ins/ShadowRealm/evaluate.js @@ -111,6 +111,24 @@ describe("ShadowRealm.prototype.evaluate", () => { // Re-declaring the same top-level lexical name must not clash. expect(realm.evaluate("const scoped = 2; scoped")).toBe(2); }); + + test("throws a SyntaxError when a top-level lexical binding is declared twice", () => { + const realm = new ShadowRealm(); + expect(() => realm.evaluate("let x; let x;")).toThrow(SyntaxError); + expect(() => realm.evaluate("let z; const z = 1;")).toThrow(SyntaxError); + }); + + test("throws a SyntaxError when a top-level name is both lexically and var declared", () => { + const realm = new ShadowRealm(); + expect(() => realm.evaluate("let y; var y;")).toThrow(SyntaxError); + expect(() => realm.evaluate("var y; let y;")).toThrow(SyntaxError); + }); + + test("allows distinct top-level lexical and var names", () => { + const realm = new ShadowRealm(); + expect(realm.evaluate("var a = 1; let b = 2; a + b")).toBe(3); + expect(realm.evaluate("let c = 4; var d = 5; c + d")).toBe(9); + }); }); describe("ShadowRealm wrapped functions", () => {