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
96 changes: 96 additions & 0 deletions Jint.Tests/Runtime/PlainAssignmentTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Jint.Runtime;

namespace Jint.Tests.Runtime;

/// <summary>
/// Plain assignment to slot-stored bindings takes a fast lane; these pin its parity with the
/// materialized AssignToIdentifier path: anonymous function/class naming, abrupt completions,
/// const/TDZ error ordering, and targets rewritten during right-hand side evaluation.
/// </summary>
public class PlainAssignmentTests
{
[Fact]
public void AnonymousFunctionAndClassGetAssignedName()
{
var engine = new Engine(static options => options.Strict());
var result = engine.Evaluate("""
function f() {
var g = function () {};
var C = class {};
var named = function realName() {};
var arrow = () => {};
return [g.name, C.name, named.name, arrow.name].join('|');
}
f();
""").AsString();

Assert.Equal("g|C|realName|arrow", result);
}

[Fact]
public void ValuePositionAndChainedAssignments()
{
var engine = new Engine(static options => options.Strict());
var result = engine.Evaluate("""
function f() {
var a = 1, b, c;
c = (b = a);
return '' + b + c;
}
f();
""").AsString();

Assert.Equal("11", result);
}

[Fact]
public void ThrowingRightHandSideDoesNotAssign()
{
var engine = new Engine(static options => options.Strict());
var result = engine.Evaluate("""
function f() {
var a = 'initial';
try { a = (function () { throw new Error('x'); })(); } catch (e) { }
return a;
}
f();
""").AsString();

Assert.Equal("initial", result);
}

[Fact]
public void RightHandSideRewritingTargetIsOverwritten()
{
var engine = new Engine(static options => options.Strict());
var result = engine.Evaluate("function f() { var b = 0; b = (b = 5, 9); return b; } f();").AsNumber();

Assert.Equal(9, result);
}

[Fact]
public void ConstTargetThrowsTypeErrorAfterEvaluatingRightHandSide()
{
var engine = new Engine(static options => options.Strict());

var ex = Assert.Throws<JavaScriptException>(() => engine.Execute("""
function f() {
const c = 1;
var order = [];
try { c = (order.push('rhs'), 2); } finally { globalThis.order = order.join(','); }
}
f();
"""));
Assert.True(ex.Error.InstanceofOperator(engine.Intrinsics.TypeError));
Assert.Equal("rhs", engine.Evaluate("globalThis.order").AsString());
}

[Fact]
public void AssignmentBeforeLetDeclarationThrowsReferenceError()
{
var engine = new Engine(static options => options.Strict());

var ex = Assert.Throws<JavaScriptException>(() => engine.Execute("function f() { { x = 1; let x; } } f();"));
Assert.True(ex.Error.InstanceofOperator(engine.Intrinsics.ReferenceError));
}
}
88 changes: 88 additions & 0 deletions Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -716,11 +716,99 @@ protected override object EvaluateInternal(EvaluationContext context)
object? completion = null;
if (_leftIdentifier != null)
{
// a populated global-binding cache means the target is a global — the slot lane
// can never apply, and AssignToIdentifier's cached-global arm is the fast path
if (_leftIdentifier._cachedGlobalEnv is null
&& !_evalOrArguments
&& !_leftIsCoverParenthesized
&& TryAssignSlot(context, out var slotResult))
{
return slotResult;
}

completion = AssignToIdentifier(context, _leftIdentifier, _right, _evalOrArguments, !_leftIsCoverParenthesized);
}
return completion ?? SetValue(context);
}

/// <summary>
/// Plain assignment to a slot-stored binding (`b = a`, `x = f()` on locals): resolves the
/// target through the slot-location cache instead of the per-assignment environment walk,
/// mirroring <see cref="AssignToIdentifier"/>'s right-hand side semantics exactly
/// (anonymous function/class naming, abrupt and generator-abort completions). Const and
/// TDZ targets bail before any evaluation so the slow path produces the spec error
/// ordering; the target binding is re-validated after the right-hand side runs since it
/// may have been written (or in degenerate cases deleted) during evaluation.
/// </summary>
private bool TryAssignSlot(EvaluationContext context, out JsValue result)
{
result = null!;

var engine = context.Engine;
if (engine.ExecutionContext.Suspendable is not null)
{
return false;
}

if (!_lhsSlotCache.TryResolve(engine, engine.ExecutionContext.LexicalEnvironment, _leftIdentifier!.Identifier, out var environment, out var slotIndex))
{
return false;
}

var slots = environment._slots;
if (slots is null || (uint) slotIndex >= (uint) slots.Length)
{
return false;
}

{
ref var binding = ref slots[slotIndex];
if (!binding.Mutable || !binding.IsInitialized())
{
return false;
}
}

JsValue completion;
var right = _right;
if (right is JintClassExpression classExpression && right._expression.IsAnonymousFunctionDefinition())
{
completion = classExpression.EvaluateWithName(context, _leftIdentifier.Identifier.Value.ToString());
}
else
{
completion = right.GetValue(context);
}

if (context.IsAbrupt() || context.IsGeneratorAborted())
{
result = completion;
return true;
}

var rval = completion.Clone();

if (right._expression.IsFunctionDefinition() && right is not JintClassExpression)
{
((Function) rval).SetFunctionName(_leftIdentifier.Identifier.Value);
}

ref var bindingAfterRight = ref slots[slotIndex];
if (bindingAfterRight.Mutable && bindingAfterRight.IsInitialized())
{
slots[slotIndex] = bindingAfterRight.ChangeValue(rval);
}
else
{
// degenerate: the right-hand side changed the binding's state; the full store
// produces the exact semantics
environment.SetMutableBinding(_leftIdentifier.Identifier, rval, StrictModeScope.IsStrictModeCode);
}

result = rval;
return true;
}

internal override bool HasDiscardFastPath => _structurallyNumeric;

internal override void EvaluateAndDiscard(EvaluationContext context)
Expand Down
Loading