diff --git a/source/units/Goccia.Builtins.GlobalShadowRealm.pas b/source/units/Goccia.Builtins.GlobalShadowRealm.pas index ba764e19..008c1ae3 100644 --- a/source/units/Goccia.Builtins.GlobalShadowRealm.pas +++ b/source/units/Goccia.Builtins.GlobalShadowRealm.pas @@ -540,60 +540,66 @@ function TGocciaShadowRealmHost.Evaluate( ChildScope := ChildEngine.ActivateRealmExecutionContext; try StrictEval := HasUseStrictDirective(ParseResult.ProgramNode); - // §3.1.3: a static early error (e.g. assigning to `arguments` in a - // strict body) is a SyntaxError. Validate before evaluating so it is - // not conflated with a runtime abrupt completion, which §3.1.3 step - // 15 wraps as a caller-realm TypeError (this includes a runtime - // SyntaxError thrown later by a nested eval). - try - ValidateEvalEarlyErrors(ParseResult.ProgramNode, StrictEval, - False, False, False); - except - on E: TGocciaSyntaxError do - begin - EarlyErrorThrew := True; - EarlyErrorMessage := E.Message; - end; - end; - if not EarlyErrorThrew then + if StrictEval then + EvalScope := ChildEngine.Interpreter.GlobalScope.CreateChild( + skFunction, 'ShadowRealmEval') + else + EvalScope := ChildEngine.Interpreter.GlobalScope.CreateChild( + skBlock, 'ShadowRealmEval'); + EvalScope.ThisValue := ChildEngine.Interpreter.GlobalScope.ThisValue; + if Assigned(TGarbageCollector.Instance) then + TGarbageCollector.Instance.AddTempRoot(EvalScope); try + EvalContext := ChildEngine.Interpreter.CreateEvaluationContext; + EvalContext.Scope := EvalScope; + EvalContext.CurrentFilePath := ''; + EvalContext.NonStrictMode := not StrictEval; if StrictEval then - EvalScope := ChildEngine.Interpreter.GlobalScope.CreateChild( - skFunction, 'ShadowRealmEval') + VarScope := EvalScope else - EvalScope := ChildEngine.Interpreter.GlobalScope.CreateChild( - skBlock, 'ShadowRealmEval'); - EvalScope.ThisValue := ChildEngine.Interpreter.GlobalScope.ThisValue; - if Assigned(TGarbageCollector.Instance) then - TGarbageCollector.Instance.AddTempRoot(EvalScope); + VarScope := ChildEngine.Interpreter.GlobalScope; + // §3.1.3 splits evaluation into two phases that map to two + // different caller-realm errors. PrepareEvalProgram runs the static + // early-error passes and EvalDeclarationInstantiation; an early + // error there — such as a duplicate top-level lexical binding + // (`let x; let x;`), or, under strict eval, assignment to + // `eval`/`arguments` — is a SyntaxError, just like a parse failure + // (step 2). RunEvalProgramBody then evaluates the body; an abrupt + // completion there, including a runtime SyntaxError (e.g. from a + // nested eval), is wrapped as a caller-realm TypeError (step 15). A + // CanDeclareGlobal* conflict raised during instantiation is itself a + // TypeError, so it routes to the same wrap rather than to SyntaxError. + // + // 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. try - EvalContext := ChildEngine.Interpreter.CreateEvaluationContext; - EvalContext.Scope := EvalScope; - EvalContext.CurrentFilePath := ''; - EvalContext.NonStrictMode := not StrictEval; - if StrictEval then - 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 := PrepareEvalProgram(ParseResult.ProgramNode, EvalContext, VarScope, EvalScope, StrictEval, False, nil, - False, False, False); - finally - if Assigned(TGarbageCollector.Instance) then - TGarbageCollector.Instance.RemoveTempRoot(EvalScope); + False, False, False, False); + except + on E: TGocciaSyntaxError do + begin + EarlyErrorThrew := True; + EarlyErrorMessage := E.Message; + end; + on E: TGocciaThrowValue do Threw := True; + on E: EGocciaBytecodeThrow do Threw := True; + on E: TGocciaError do Threw := True; end; - except - // §3.1.3 step 15: a runtime abrupt completion (including a runtime - // SyntaxError, e.g. from a nested eval) becomes a caller-realm - // TypeError. Static early errors were already validated above. - on E: TGocciaThrowValue do Threw := True; - on E: EGocciaBytecodeThrow do Threw := True; - on E: TGocciaError do Threw := True; + if not (EarlyErrorThrew or Threw) then + try + RawResult := RunEvalProgramBody(ParseResult.ProgramNode, + EvalContext); + except + on E: TGocciaThrowValue do Threw := True; + on E: EGocciaBytecodeThrow do Threw := True; + on E: TGocciaError do Threw := True; + end; + finally + if Assigned(TGarbageCollector.Instance) then + TGarbageCollector.Instance.RemoveTempRoot(EvalScope); end; finally ChildScope.Free; diff --git a/source/units/Goccia.Evaluator.pas b/source/units/Goccia.Evaluator.pas index 08adcfe7..cdf9a4a6 100644 --- a/source/units/Goccia.Evaluator.pas +++ b/source/units/Goccia.Evaluator.pas @@ -135,6 +135,31 @@ function EvaluateEvalProgram(const AProgram: TGocciaProgram; const AAllowSuperCall: Boolean = False; const ARejectArgumentsReference: Boolean = False): TGocciaValue; +{ EvaluateEvalProgram split into its two phases, so a caller can distinguish a + pre-execution early error from a runtime abrupt completion (both are a Pascal + TGocciaError, differing only by phase). PrepareEvalProgram runs phase one — all + of the eval Script's pre-execution work: the static early-error passes (forbidden + module/using declarations, a forbidden `arguments` reference, and the + reference/assignment early errors of ValidateEvalEarlyErrors) and then + EvalDeclarationInstantiation. It returns the evaluation context the body runs in. + Every error it raises is a pre-execution error: a TGocciaSyntaxError for a static + early error (including a duplicate-lexical or lexical/var conflict surfaced by + EvalDeclarationInstantiation), or a TGocciaTypeError for a CanDeclareGlobal* + conflict. RunEvalProgramBody runs phase two — evaluating the body with that + context and returning the Script's completion value; an error it raises is a + runtime abrupt completion. EvaluateEvalProgram simply chains the two. } +function PrepareEvalProgram(const AProgram: TGocciaProgram; + const AContext: TGocciaEvaluationContext; const AVarScope, + ALexicalScope: TGocciaScope; const AStrictEval: Boolean; + const ARejectArgumentsVarDeclaration: Boolean; + const ARejectVarDeclarationNames: TGocciaEvalRejectNameArray; + const AAllowNewTarget: Boolean = False; + const AAllowSuperProperty: Boolean = False; + const AAllowSuperCall: Boolean = False; + const ARejectArgumentsReference: Boolean = False): TGocciaEvaluationContext; +function RunEvalProgramBody(const AProgram: TGocciaProgram; + const AEvalContext: TGocciaEvaluationContext): TGocciaValue; + { Runs the eval Script's static early-error checks for forbidden references and assignments — `new.target` / `super` outside a permitting context and, under strict eval, assignment to `eval`/`arguments` — the same pass EvaluateEvalProgram @@ -2868,19 +2893,121 @@ procedure EvalDeclarationInstantiation(const AProgram: TGocciaProgram; end; end; -function EvaluateEvalProgram(const AProgram: TGocciaProgram; +{ A `return`, or an unlabeled `break`/`continue` with no enclosing iteration or + switch, is a Script early error (a SyntaxError) — eval code is never a function + body, loop, or switch. Goccia otherwise detects these only at runtime via + control-flow propagation (the fallback case in RunEvalProgramBody), which would + let ShadowRealm.prototype.evaluate wrap them as a caller-realm TypeError; + validating here, before any evaluation, keeps them SyntaxErrors. Recursion walks + only statements, so it stops at function/class boundaries (which are + expressions) — a `return`/`break`/`continue` inside a nested function is never + flagged. Labeled break/continue are left to the parser, which validates their + targets and rejects undefined labels. + + Raising TGocciaSyntaxError directly (rather than ThrowSyntaxError, which raises a + TGocciaThrowValue) matches the other eval early-error passes so the error is + classified as a static SyntaxError, not a runtime abrupt completion. } +procedure RejectEvalControlFlow(const AMessage: string; + const AStmt: TGocciaStatement); +begin + raise TGocciaSyntaxError.Create(AMessage, AStmt.Line, AStmt.Column, '', nil, + SSuggestExpressionExpected); +end; + +procedure ValidateEvalControlFlowStatement(const AStmt: TGocciaStatement; + const AInIteration, AInSwitch: Boolean); +var + I, J: Integer; + BlockStmt: TGocciaBlockStatement; + IfStmt: TGocciaIfStatement; + TryStmt: TGocciaTryStatement; + SwitchStmt: TGocciaSwitchStatement; +begin + if not Assigned(AStmt) then + Exit; + + if AStmt is TGocciaReturnStatement then + RejectEvalControlFlow('Illegal return statement', AStmt) + else if AStmt is TGocciaBreakStatement then + begin + if (TGocciaBreakStatement(AStmt).TargetLabel = '') and + not (AInIteration or AInSwitch) then + RejectEvalControlFlow(SErrorIllegalBreakStatement, AStmt); + end + else if AStmt is TGocciaContinueStatement then + begin + if (TGocciaContinueStatement(AStmt).TargetLabel = '') and + not AInIteration then + RejectEvalControlFlow(SErrorIllegalContinueStatement, AStmt); + end + else if AStmt is TGocciaBlockStatement then + begin + BlockStmt := TGocciaBlockStatement(AStmt); + for I := 0 to BlockStmt.Nodes.Count - 1 do + if BlockStmt.Nodes[I] is TGocciaStatement then + ValidateEvalControlFlowStatement( + TGocciaStatement(BlockStmt.Nodes[I]), AInIteration, AInSwitch); + end + else if AStmt is TGocciaIfStatement then + begin + IfStmt := TGocciaIfStatement(AStmt); + ValidateEvalControlFlowStatement(IfStmt.Consequent, AInIteration, AInSwitch); + ValidateEvalControlFlowStatement(IfStmt.Alternate, AInIteration, AInSwitch); + end + else if AStmt is TGocciaForStatement then + ValidateEvalControlFlowStatement( + TGocciaForStatement(AStmt).Body, True, AInSwitch) + else if AStmt is TGocciaForOfStatement then + ValidateEvalControlFlowStatement( + TGocciaForOfStatement(AStmt).Body, True, AInSwitch) + else if AStmt is TGocciaForInStatement then + ValidateEvalControlFlowStatement( + TGocciaForInStatement(AStmt).Body, True, AInSwitch) + else if AStmt is TGocciaWhileStatement then + ValidateEvalControlFlowStatement( + TGocciaWhileStatement(AStmt).Body, True, AInSwitch) + else if AStmt is TGocciaDoWhileStatement then + ValidateEvalControlFlowStatement( + TGocciaDoWhileStatement(AStmt).Body, True, AInSwitch) + else if AStmt is TGocciaWithStatement then + ValidateEvalControlFlowStatement( + TGocciaWithStatement(AStmt).Body, AInIteration, AInSwitch) + else if AStmt is TGocciaTryStatement then + begin + TryStmt := TGocciaTryStatement(AStmt); + ValidateEvalControlFlowStatement(TryStmt.Block, AInIteration, AInSwitch); + ValidateEvalControlFlowStatement(TryStmt.CatchBlock, AInIteration, AInSwitch); + ValidateEvalControlFlowStatement( + TryStmt.FinallyBlock, AInIteration, AInSwitch); + end + else if AStmt is TGocciaSwitchStatement then + begin + SwitchStmt := TGocciaSwitchStatement(AStmt); + for I := 0 to SwitchStmt.Cases.Count - 1 do + for J := 0 to SwitchStmt.Cases[I].Consequent.Count - 1 do + if SwitchStmt.Cases[I].Consequent[J] is TGocciaStatement then + ValidateEvalControlFlowStatement( + TGocciaStatement(SwitchStmt.Cases[I].Consequent[J]), + AInIteration, True); + end; +end; + +procedure ValidateEvalControlFlow(const AProgram: TGocciaProgram); +var + I: Integer; +begin + for I := 0 to AProgram.Body.Count - 1 do + ValidateEvalControlFlowStatement(AProgram.Body[I], False, False); +end; + +function PrepareEvalProgram(const AProgram: TGocciaProgram; const AContext: TGocciaEvaluationContext; const AVarScope, ALexicalScope: TGocciaScope; const AStrictEval: Boolean; const ARejectArgumentsVarDeclaration: Boolean; const ARejectVarDeclarationNames: TGocciaEvalRejectNameArray; const AAllowNewTarget: Boolean; const AAllowSuperProperty: Boolean; const AAllowSuperCall: Boolean; - const ARejectArgumentsReference: Boolean): TGocciaValue; -var - EvalContext: TGocciaEvaluationContext; - CF: TGocciaControlFlow; - I: Integer; - LastValue: TGocciaValue; + const ARejectArgumentsReference: Boolean): TGocciaEvaluationContext; begin ValidateEvalScriptBody(AProgram.Body); if ARejectArgumentsReference and @@ -2888,29 +3015,41 @@ function EvaluateEvalProgram(const AProgram: TGocciaProgram; ThrowSyntaxError('arguments is not allowed in this eval context'); ValidateEvalEarlyErrors(AProgram, AStrictEval, AAllowNewTarget, AAllowSuperProperty, AAllowSuperCall); - EvalContext := AContext; - EvalContext.Scope := ALexicalScope; - EvalContext.NonStrictMode := not AStrictEval; - EvalContext.CompatibilityNonStrictMode := AContext.CompatibilityNonStrictMode; - EvalContext.InEvalCode := True; - EvalContext.EvalVarScope := AVarScope; + ValidateEvalControlFlow(AProgram); + Result := AContext; + Result.Scope := ALexicalScope; + Result.NonStrictMode := not AStrictEval; + Result.CompatibilityNonStrictMode := AContext.CompatibilityNonStrictMode; + Result.InEvalCode := True; + Result.EvalVarScope := AVarScope; ALexicalScope.NonStrictMode := not AStrictEval; AVarScope.NonStrictMode := not AStrictEval; - EvalDeclarationInstantiation(AProgram, EvalContext, AVarScope, ALexicalScope, + EvalDeclarationInstantiation(AProgram, Result, AVarScope, ALexicalScope, AStrictEval, ARejectArgumentsVarDeclaration, ARejectVarDeclarationNames); +end; +function RunEvalProgramBody(const AProgram: TGocciaProgram; + const AEvalContext: TGocciaEvaluationContext): TGocciaValue; +var + CF: TGocciaControlFlow; + I: Integer; + LastValue: TGocciaValue; +begin LastValue := TGocciaUndefinedLiteralValue.UndefinedValue; CF := TGocciaControlFlow.Normal(LastValue); for I := 0 to AProgram.Body.Count - 1 do begin - CF := EvaluateStatement(AProgram.Body[I], EvalContext); + CF := EvaluateStatement(AProgram.Body[I], AEvalContext); if (CF.Kind = cfkNormal) and Assigned(CF.Value) then LastValue := CF.Value; if CF.Kind <> cfkNormal then Break; end; + // An abrupt top-level completion is a Script early error that + // PrepareEvalProgram.ValidateEvalControlFlow already rejected as a SyntaxError + // before execution began; these arms are a defensive backstop only. case CF.Kind of cfkNormal: Result := LastValue; @@ -2926,6 +3065,24 @@ function EvaluateEvalProgram(const AProgram: TGocciaProgram; end; end; +function EvaluateEvalProgram(const AProgram: TGocciaProgram; + const AContext: TGocciaEvaluationContext; const AVarScope, + ALexicalScope: TGocciaScope; const AStrictEval: Boolean; + const ARejectArgumentsVarDeclaration: Boolean; + const ARejectVarDeclarationNames: TGocciaEvalRejectNameArray; + const AAllowNewTarget: Boolean; const AAllowSuperProperty: Boolean; + const AAllowSuperCall: Boolean; + const ARejectArgumentsReference: Boolean): TGocciaValue; +var + EvalContext: TGocciaEvaluationContext; +begin + EvalContext := PrepareEvalProgram(AProgram, AContext, AVarScope, ALexicalScope, + AStrictEval, ARejectArgumentsVarDeclaration, ARejectVarDeclarationNames, + AAllowNewTarget, AAllowSuperProperty, AAllowSuperCall, + ARejectArgumentsReference); + Result := RunEvalProgramBody(AProgram, EvalContext); +end; + function EvaluateStatements(const ANodes: TObjectList; const AContext: TGocciaEvaluationContext): TGocciaControlFlow; var I: Integer; diff --git a/tests/built-ins/ShadowRealm/evaluate.js b/tests/built-ins/ShadowRealm/evaluate.js index fad8c314..ac7a83fb 100644 --- a/tests/built-ins/ShadowRealm/evaluate.js +++ b/tests/built-ins/ShadowRealm/evaluate.js @@ -53,6 +53,35 @@ describe("ShadowRealm.prototype.evaluate", () => { expect(() => realm.evaluate(")(")).toThrow(SyntaxError); }); + 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("const y = 1; const y = 2;")).toThrow( + SyntaxError, + ); + expect(() => realm.evaluate("let z; const z = 1;")).toThrow(SyntaxError); + expect(() => realm.evaluate("class C {} class C {}")).toThrow(SyntaxError); + // A declaration conflict is a pre-execution early error, so it stays a + // SyntaxError rather than being wrapped as the TypeError used for runtime + // abrupt completions; re-evaluating a fresh lexical name still succeeds. + expect(realm.evaluate("const ok = 2; ok")).toBe(2); + }); + + test("throws a SyntaxError for illegal top-level control flow", () => { + const realm = new ShadowRealm(); + expect(() => realm.evaluate("return 1;")).toThrow(SyntaxError); + expect(() => realm.evaluate("break;")).toThrow(SyntaxError); + expect(() => realm.evaluate("continue;")).toThrow(SyntaxError); + expect(() => realm.evaluate("{ break; }")).toThrow(SyntaxError); + expect(() => realm.evaluate("if (true) return 1;")).toThrow(SyntaxError); + // A return/break inside a nested function, or a break inside a loop, is + // valid and must not be flagged by the early-error check. + expect( + realm.evaluate("(() => { for (let i = 0; i < 1; i++) break; return 7; })()"), + ).toBe(7); + expect(realm.evaluate("for (let i = 0; i < 1; i++) break; 9")).toBe(9); + }); + test("throws a TypeError when the result is a non-callable object", () => { const realm = new ShadowRealm(); expect(() => realm.evaluate("({})")).toThrow(TypeError);