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

namespace Jint.Tests.Runtime;

/// <summary>
/// Pins the semantics of the JintNewExpression call-site constructor cache: identity-validated,
/// falls through on any miss, and the zero-arg leaf fast path (default-time-system Date) stays
/// unobservable.
/// </summary>
public class NewExpressionCacheTests
{
[Fact]
public void RepeatedNewDateUsesRealmPrototype()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
var ok = 0;
var last = null;
for (var i = 0; i < 1000; i++) {
var d = new Date();
if (Object.getPrototypeOf(d) === Date.prototype && d !== last) ok++;
last = d;
}
return ok;
})()
""").AsNumber();

Assert.Equal(1000, result);
}

[Fact]
public void ReassignedDateConstructorIsPickedUpAtWarmCallSite()
{
var engine = new Engine();
// warm the call site with the built-in Date, then swap the global binding: the cache
// must miss on identity and construct through the new function
var result = engine.Evaluate("""
(function () {
var seen = '';
function make() { return new Date(); }
for (var i = 0; i < 100; i++) make();
seen += (make() instanceof Date);
Date = function () { this.marker = 42; };
var d = make();
seen += ':' + d.marker;
return seen;
})()
""").AsString();

Assert.Equal("true:42", result);
}

[Fact]
public void SubclassedDateGetsSubclassPrototype()
{
var engine = new Engine();
var result = engine.Evaluate("""
(function () {
for (var i = 0; i < 100; i++) new Date();
class D extends Date { }
var d = new D();
return (Object.getPrototypeOf(d) === D.prototype) + ':' + (d instanceof Date);
})()
""").AsString();

Assert.Equal("true:true", result);
}

[Fact]
public void SharedPreparedScriptConstructsAgainstOwnRealm()
{
// one prepared script executed by two engines: the call-site cache must re-resolve per
// engine (identity miss) so each engine gets dates from its own realm intrinsics
var prepared = Engine.PrepareScript("""
var d = new Date();
Object.getPrototypeOf(d) === Date.prototype;
""");

var engine1 = new Engine();
var engine2 = new Engine();
for (var i = 0; i < 5; i++)
{
Assert.True(engine1.Evaluate(prepared).AsBoolean());
Assert.True(engine2.Evaluate(prepared).AsBoolean());
}
}

[Fact]
public void CustomTimeSystemStillServesNewDate()
{
// a custom ITimeSystem disables the leaf fast path (exact-type gate); constructs run the
// generic path and observe the custom clock
var engine = new Engine(options => options.TimeSystem = new FixedTimeSystem());
var result = engine.Evaluate("""
(function () {
var ok = 0;
for (var i = 0; i < 100; i++) {
if (new Date().getTime() === 123456789) ok++;
}
return ok;
})()
""").AsNumber();

Assert.Equal(100, result);
}

[Fact]
public void ThrowingCustomTimeSystemSurfacesExceptionPerConstruct()
{
var engine = new Engine(options => options.TimeSystem = new ThrowingTimeSystem());
var ex = Assert.Throws<InvalidOperationException>(() => engine.Evaluate("new Date()"));
Assert.Equal("clock is broken", ex.Message);
}

private sealed class FixedTimeSystem : ITimeSystem
{
public DateTimeOffset GetUtcNow() => DateTimeOffset.FromUnixTimeMilliseconds(123456789);
public TimeZoneInfo DefaultTimeZone => TimeZoneInfo.Utc;
public TimeSpan GetUtcOffset(long epochMilliseconds) => TimeSpan.Zero;
public bool TryParse(string date, out long epochMilliseconds)
{
epochMilliseconds = 0;
return false;
}
}

private sealed class ThrowingTimeSystem : ITimeSystem
{
public DateTimeOffset GetUtcNow() => throw new InvalidOperationException("clock is broken");
public TimeZoneInfo DefaultTimeZone => TimeZoneInfo.Utc;
public TimeSpan GetUtcOffset(long epochMilliseconds) => TimeSpan.Zero;
public bool TryParse(string date, out long epochMilliseconds)
{
epochMilliseconds = 0;
return false;
}
}
}
8 changes: 8 additions & 0 deletions Jint/Native/Date/DateConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal sealed partial class DateConstructor : Constructor
private static readonly long EpochTicks = Epoch.Ticks;
private static readonly JsString _functionName = new JsString("Date");
private readonly ITimeSystem _timeSystem;
private readonly bool _isZeroArgLeafConstructor;

internal DateConstructor(
Engine engine,
Expand All @@ -30,8 +31,15 @@ internal DateConstructor(
_length = new PropertyDescriptor(7, PropertyFlag.Configurable);
_prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden);
_timeSystem = engine.Options.TimeSystem;

// Only the stock time system is proven side-effect- and throw-free; a derived or custom
// ITimeSystem could throw from GetUtcNow and would then miss its `new Date()` stack frame
// on the leaf construct path, so the exact type is required.
_isZeroArgLeafConstructor = _timeSystem.GetType() == typeof(DefaultTimeSystem);
}

internal override bool IsZeroArgLeafConstructor => _isZeroArgLeafConstructor;

internal DatePrototype PrototypeObject { get; }

protected override void Initialize() => CreateProperties_Generated();
Expand Down
8 changes: 8 additions & 0 deletions Jint/Native/Function/Function.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ internal Function(

internal override bool IsConstructor => this is IConstructor;

/// <summary>
/// True for built-in constructors whose zero-argument [[Construct]] with newTarget == this
/// runs no user-observable code and cannot raise a JavaScript error, allowing call sites to
/// skip the call-stack frame and constructor-resolution ceremony. Must stay false whenever a
/// user callback (e.g. a custom time system) could observe the call or throw through it.
/// </summary>
internal virtual bool IsZeroArgLeafConstructor => false;

public override IEnumerable<KeyValuePair<JsValue, PropertyDescriptor>> GetOwnProperties()
{
if (_prototypeDescriptor != null)
Expand Down
35 changes: 34 additions & 1 deletion Jint/Runtime/Interpreter/Expressions/JintNewExpression.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
using Jint.Native;
using Jint.Native.Function;

namespace Jint.Runtime.Interpreter.Expressions;

internal sealed class JintNewExpression : JintExpression
{
private readonly ExpressionCache _arguments = new();
private readonly JintExpression _calleeExpression;
private readonly bool _zeroArgs;

// Monomorphic call-site cache: the constructor object seen by the last evaluation.
// Identity pins the realm and the immutable per-instance IsConstructor /
// IsZeroArgLeafConstructor verdicts, so a hit skips the constructor type-test — and for
// zero-argument leaf built-ins (default-time-system Date) the whole Engine.Construct
// call-stack ceremony, which such constructors can neither observe nor throw through.
// A miss (callee reassigned, different engine sharing this prepared node) falls through
// to the generic path and re-caches — never a permanent decline.
private Function? _cachedConstructor;
private bool _cachedLeafZeroArg;

public JintNewExpression(NewExpression expression) : base(expression)
{
_arguments.Initialize(expression.Arguments.AsSpan());
_calleeExpression = Build(expression.Callee);
_zeroArgs = expression.Arguments.Count == 0;
}

protected override object EvaluateInternal(EvaluationContext context)
Expand All @@ -18,6 +33,18 @@ protected override object EvaluateInternal(EvaluationContext context)
// todo: optimize by defining a common abstract class or interface
var jsValue = _calleeExpression.GetValue(context);

var isCachedConstructor = ReferenceEquals(jsValue, _cachedConstructor);
if (isCachedConstructor
&& _cachedLeafZeroArg
&& !engine._isDebugMode
&& engine.ExecutionContext.Suspendable is null)
{
// Error objects cannot be produced, but a custom .NET-level failure would still
// resolve its script location from the last syntax element.
context.LastSyntaxElement = _expression;
return ((IConstructor) jsValue).Construct(Arguments.Empty, jsValue);
}

if (context.IsSuspended())
{
return jsValue;
Expand All @@ -36,7 +63,7 @@ protected override object EvaluateInternal(EvaluationContext context)
return jsValue;
}

if (!jsValue.IsConstructor)
if (!isCachedConstructor && !jsValue.IsConstructor)
{
Throw.TypeError(engine.Realm, $"{_calleeExpression.SourceText} is not a constructor");
}
Expand All @@ -49,6 +76,12 @@ protected override object EvaluateInternal(EvaluationContext context)
engine._jsValueArrayPool.ReturnArray(arguments);
}

if (!isCachedConstructor && jsValue is Function function)
{
_cachedConstructor = function;
_cachedLeafZeroArg = _zeroArgs && function.IsZeroArgLeafConstructor && function is IConstructor;
}

return instance;
}
}
Loading