diff --git a/Jint.Tests/Runtime/PlainAssignmentTests.cs b/Jint.Tests/Runtime/PlainAssignmentTests.cs new file mode 100644 index 000000000..b10ac4eb1 --- /dev/null +++ b/Jint.Tests/Runtime/PlainAssignmentTests.cs @@ -0,0 +1,96 @@ +using Jint.Runtime; + +namespace Jint.Tests.Runtime; + +/// +/// 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. +/// +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(() => 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(() => engine.Execute("function f() { { x = 1; let x; } } f();")); + Assert.True(ex.Error.InstanceofOperator(engine.Intrinsics.ReferenceError)); + } +} diff --git a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs index 82999ef34..dbbd3911e 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintAssignmentExpression.cs @@ -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); } + /// + /// 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 '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. + /// + 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)