Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 54 additions & 48 deletions source/units/Goccia.Builtins.GlobalShadowRealm.pas
Original file line number Diff line number Diff line change
Expand Up @@ -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 := '<shadow-realm-eval>';
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 := '<shadow-realm-eval>';
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;
Expand Down
187 changes: 172 additions & 15 deletions source/units/Goccia.Evaluator.pas
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2868,49 +2893,163 @@ 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
EvalProgramContainsArgumentsReference(AProgram) then
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;
Expand All @@ -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<TGocciaASTNode>; const AContext: TGocciaEvaluationContext): TGocciaControlFlow;
var
I: Integer;
Expand Down
29 changes: 29 additions & 0 deletions tests/built-ins/ShadowRealm/evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading