diff --git a/Jint.Tests/Runtime/NewExpressionCacheTests.cs b/Jint.Tests/Runtime/NewExpressionCacheTests.cs
new file mode 100644
index 000000000..2968cf386
--- /dev/null
+++ b/Jint.Tests/Runtime/NewExpressionCacheTests.cs
@@ -0,0 +1,139 @@
+using Jint.Runtime;
+
+namespace Jint.Tests.Runtime;
+
+///
+/// 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.
+///
+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(() => 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;
+ }
+ }
+}
diff --git a/Jint/Native/Date/DateConstructor.cs b/Jint/Native/Date/DateConstructor.cs
index c5fd8f47e..b4525c424 100644
--- a/Jint/Native/Date/DateConstructor.cs
+++ b/Jint/Native/Date/DateConstructor.cs
@@ -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,
@@ -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();
diff --git a/Jint/Native/Function/Function.cs b/Jint/Native/Function/Function.cs
index f0641f021..e7ef5833c 100644
--- a/Jint/Native/Function/Function.cs
+++ b/Jint/Native/Function/Function.cs
@@ -86,6 +86,14 @@ internal Function(
internal override bool IsConstructor => this is IConstructor;
+ ///
+ /// 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.
+ ///
+ internal virtual bool IsZeroArgLeafConstructor => false;
+
public override IEnumerable> GetOwnProperties()
{
if (_prototypeDescriptor != null)
diff --git a/Jint/Runtime/Interpreter/Expressions/JintNewExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintNewExpression.cs
index 1dfae2d16..4a6a4f81e 100644
--- a/Jint/Runtime/Interpreter/Expressions/JintNewExpression.cs
+++ b/Jint/Runtime/Interpreter/Expressions/JintNewExpression.cs
@@ -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)
@@ -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;
@@ -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");
}
@@ -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;
}
}