diff --git a/Jint.Benchmark/ClassBenchmark.cs b/Jint.Benchmark/ClassBenchmark.cs
index 2359b319c..87b387947 100644
--- a/Jint.Benchmark/ClassBenchmark.cs
+++ b/Jint.Benchmark/ClassBenchmark.cs
@@ -51,4 +51,38 @@ public void GetSet()
_engine.Evaluate(script);
}
}
+
+ [Benchmark]
+ public void ReEvaluateClassDeclarations()
+ {
+ // the dom.js / class-factory shape: one engine re-evaluates the same prepared script that
+ // declares classes; member definitions come from the per-engine cache while class
+ // identities, prototypes, private state and static-block effects stay per-evaluation
+ var script = Engine.PrepareScript("""
+ (function () {
+ class Node {
+ #tag = 'node';
+ static VERSION = 1;
+ static { this.registry = []; }
+ constructor(name) { this.name = name; this.children = []; }
+ appendChild(child) { this.children.push(child); return child; }
+ get childCount() { return this.children.length; }
+ set alias(value) { this.name = value; }
+ describe() { return this.#tag + ':' + this.name; }
+ static create(name) { return new Node(name); }
+ }
+ class Element extends Node {
+ describe() { return 'element:' + super.describe(); }
+ }
+ const e = new Element('div');
+ e.appendChild(Node.create('span'));
+ e.alias = 'main';
+ return e.describe() + '/' + e.childCount + '/' + Element.VERSION;
+ })();
+ """);
+ for (var i = 0; i < 40_000; ++i)
+ {
+ _engine.Evaluate(script);
+ }
+ }
}
diff --git a/Jint.Tests/Runtime/ClassReEvaluationTests.cs b/Jint.Tests/Runtime/ClassReEvaluationTests.cs
new file mode 100644
index 000000000..748f0bb15
--- /dev/null
+++ b/Jint.Tests/Runtime/ClassReEvaluationTests.cs
@@ -0,0 +1,80 @@
+namespace Jint.Tests.Runtime;
+
+///
+/// Pins the semantics of the per-engine function-definition cache applied to class evaluation:
+/// re-evaluating the same class AST (factory functions, re-run prepared scripts) reuses the
+/// members' interpreter definitions, while closures, home objects, environments and private
+/// state stay strictly per evaluation.
+///
+public class ClassReEvaluationTests
+{
+ [Fact]
+ public void ClassFactoriesProduceIndependentClasses()
+ {
+ var engine = new Engine();
+ var result = engine.Evaluate("""
+ (function () {
+ function make(tag) {
+ return class {
+ static origin = tag;
+ #secret = tag + '!';
+ constructor() { this.made = tag; }
+ get secret() { return this.#secret; }
+ describe() { return tag + ':' + this.made; }
+ static { this.stamped = tag.toUpperCase(); }
+ };
+ }
+ const A = make('a'), B = make('b');
+ const a = new A(), b = new B();
+ return [
+ A !== B, A.prototype !== B.prototype,
+ A.prototype.describe !== B.prototype.describe,
+ a.describe(), b.describe(),
+ a.secret, b.secret,
+ A.origin, B.origin, A.stamped, B.stamped,
+ a instanceof A, a instanceof B
+ ].join(',');
+ })()
+ """).AsString();
+
+ Assert.Equal("true,true,true,a:a,b:b,a!,b!,a,b,A,B,true,false", result);
+ }
+
+ [Fact]
+ public void SuperAndHomeObjectStayPerEvaluation()
+ {
+ var engine = new Engine();
+ var result = engine.Evaluate("""
+ (function () {
+ function make(base) {
+ return class extends base { hello() { return 'sub-' + super.hello(); } };
+ }
+ class B1 { hello() { return 'one'; } }
+ class B2 { hello() { return 'two'; } }
+ const S1 = make(B1), S2 = make(B2);
+ return new S1().hello() + '|' + new S2().hello();
+ })()
+ """).AsString();
+
+ Assert.Equal("sub-one|sub-two", result);
+ }
+
+ [Fact]
+ public void RepeatedPreparedScriptEvaluationStaysCorrect()
+ {
+ var engine = new Engine();
+ // top-level `class` is a lexical declaration and correctly throws on re-declaration, so the
+ // re-evaluation pattern wraps it — the class AST (and its cached member definitions) still
+ // repeats across evaluations
+ var prepared = Engine.PrepareScript("""
+ (function () {
+ class Point { #x; constructor(x) { this.#x = x; } get x() { return this.#x; } double() { return this.x * 2; } }
+ return new Point(21).double();
+ })();
+ """);
+
+ Assert.Equal(42, engine.Evaluate(prepared).AsNumber());
+ Assert.Equal(42, engine.Evaluate(prepared).AsNumber());
+ Assert.Equal(42, engine.Evaluate(prepared).AsNumber());
+ }
+}
diff --git a/Jint/AstExtensions.cs b/Jint/AstExtensions.cs
index 5bf213203..2f601e44f 100644
--- a/Jint/AstExtensions.cs
+++ b/Jint/AstExtensions.cs
@@ -329,7 +329,14 @@ internal static Record DefineMethod(this T m, ObjectInstance obj, ObjectInsta
Throw.SyntaxError(engine.Realm);
}
- var definition = new JintFunctionDefinition(function, sourceTextNode);
+ // re-evaluating the same class (factory functions, re-run prepared scripts) reuses the
+ // method's interpreter definition and its warm body handler tree; the closure, home object
+ // and environments stay per-evaluation
+ if (!engine.TryGetFunctionDefinition((Node) function, out var definition))
+ {
+ engine.CacheFunctionDefinition((Node) function, definition = new JintFunctionDefinition(function, sourceTextNode));
+ }
+
var closure = intrinsics.Function.OrdinaryFunctionCreate(prototype, definition, definition.ThisMode, env, privateEnv);
closure.MakeMethod(obj);
diff --git a/Jint/Engine.cs b/Jint/Engine.cs
index e31c2fc85..d60191566 100644
--- a/Jint/Engine.cs
+++ b/Jint/Engine.cs
@@ -54,10 +54,11 @@ public sealed partial class Engine : IDisposable
internal EvaluationContext? _activeEvaluationContext;
internal ErrorDispatchInfo? _error;
- // Per-engine cache of function declarations' interpreter definitions, keyed on the AST node.
- // The definition owns the lazily-built body handler tree — and through it every per-node inline
- // cache — so reusing it across evaluations and calls keeps a prepared script's functions warm
- // instead of rebuilding the subtree each time declaration instantiation runs.
+ // Per-engine cache of interpreter function definitions — hoisted declarations, class methods,
+ // class field initializers and static blocks — keyed on the stable AST node. The definition
+ // owns the lazily-built body handler tree — and through it every per-node inline cache — so
+ // reusing it across evaluations and calls keeps a prepared script's functions warm instead of
+ // rebuilding the subtree each time declaration instantiation or class evaluation runs.
//
// Lifetime notes. Engine-owned rather than shared via the AST's UserData State because handler
// trees accumulate engine-affine cache entries, and sharing them across engines retains dead
@@ -68,7 +69,7 @@ public sealed partial class Engine : IDisposable
// failure the weak table was meant to avoid. The strong form instead retains definitions for
// scripts this engine evaluated until the engine dies, mirroring Realm._templateMap's existing
// behavior.
- private readonly Dictionary _hoistedFunctionDefinitions = new();
+ private readonly Dictionary _functionDefinitions = new();
// Scripts this engine has run global declaration instantiation for, so the definition cache
// above only engages on RE-evaluation (see GlobalDeclarationInstantiation).
@@ -76,22 +77,30 @@ public sealed partial class Engine : IDisposable
internal JintFunctionDefinition GetOrCreateFunctionDefinition(FunctionDeclaration declaration)
{
- if (!_hoistedFunctionDefinitions.TryGetValue(declaration, out var definition))
+ if (!TryGetFunctionDefinition(declaration, out var definition))
{
- // backstop for hosts streaming endless distinct sources (unique eval/script texts)
- // through one engine: reset rather than grow without bound - steady-state reuse of a
- // sane number of scripts never reaches this
- if (_hoistedFunctionDefinitions.Count >= 2048)
- {
- _hoistedFunctionDefinitions.Clear();
- }
-
- _hoistedFunctionDefinitions[declaration] = definition = new JintFunctionDefinition(declaration);
+ CacheFunctionDefinition(declaration, definition = new JintFunctionDefinition(declaration));
}
return definition;
}
+ internal bool TryGetFunctionDefinition(Node key, [NotNullWhen(true)] out JintFunctionDefinition? definition)
+ => _functionDefinitions.TryGetValue(key, out definition);
+
+ internal void CacheFunctionDefinition(Node key, JintFunctionDefinition definition)
+ {
+ // backstop for hosts streaming endless distinct sources (unique eval/script texts)
+ // through one engine: reset rather than grow without bound - steady-state reuse of a
+ // sane number of scripts never reaches this
+ if (_functionDefinitions.Count >= 2048)
+ {
+ _functionDefinitions.Clear();
+ }
+
+ _functionDefinitions[key] = definition;
+ }
+
private readonly EventLoop _eventLoop = new();
internal EventLoop EventLoop => _eventLoop;
diff --git a/Jint/Native/Function/ClassDefinition.cs b/Jint/Native/Function/ClassDefinition.cs
index a59542d08..e44e89c39 100644
--- a/Jint/Native/Function/ClassDefinition.cs
+++ b/Jint/Native/Function/ClassDefinition.cs
@@ -613,7 +613,11 @@ private static AutoAccessorResult AutoAccessorDefinitionEvaluation(
var env = engine.ExecutionContext.LexicalEnvironment;
var privEnv = engine.ExecutionContext.PrivateEnvironment;
- var definition = new JintFunctionDefinition(new ClassFieldFunction(accessorProperty.Value));
+ if (!engine.TryGetFunctionDefinition(accessorProperty.Value, out var definition))
+ {
+ engine.CacheFunctionDefinition(accessorProperty.Value, definition = new JintFunctionDefinition(new ClassFieldFunction(accessorProperty.Value)));
+ }
+
initializer = intrinsics.Function.OrdinaryFunctionCreate(intrinsics.Function.PrototypeObject, definition, FunctionThisMode.Global, env, privEnv);
initializer.MakeMethod(target);
initializer._classFieldInitializerName = name;
@@ -658,7 +662,11 @@ private static AutoAccessorResult AutoAccessorDefinitionEvaluation(
var env = engine.ExecutionContext.LexicalEnvironment;
var privEnv = engine.ExecutionContext.PrivateEnvironment;
- var definition = new JintFunctionDefinition(new ClassFieldFunction(accessorProperty.Value));
+ if (!engine.TryGetFunctionDefinition(accessorProperty.Value, out var definition))
+ {
+ engine.CacheFunctionDefinition(accessorProperty.Value, definition = new JintFunctionDefinition(new ClassFieldFunction(accessorProperty.Value)));
+ }
+
initializer = intrinsics.Function.OrdinaryFunctionCreate(intrinsics.Function.PrototypeObject, definition, FunctionThisMode.Global, env, privEnv);
initializer.MakeMethod(target);
initializer._classFieldInitializerName = name;
@@ -689,7 +697,11 @@ private static ClassFieldDefinition ClassFieldDefinitionEvaluation(Engine engine
var env = engine.ExecutionContext.LexicalEnvironment;
var privateEnv = engine.ExecutionContext.PrivateEnvironment;
- var definition = new JintFunctionDefinition(new ClassFieldFunction(fieldDefinition.Value));
+ if (!engine.TryGetFunctionDefinition(fieldDefinition.Value, out var definition))
+ {
+ engine.CacheFunctionDefinition(fieldDefinition.Value, definition = new JintFunctionDefinition(new ClassFieldFunction(fieldDefinition.Value)));
+ }
+
initializer = intrinsics.Function.OrdinaryFunctionCreate(intrinsics.Function.PrototypeObject, definition, FunctionThisMode.Global, env, privateEnv);
initializer.MakeMethod(homeObject);
@@ -729,7 +741,10 @@ private static ClassStaticBlockDefinition ClassStaticBlockDefinitionEvaluation(E
{
var intrinsics = engine.Realm.Intrinsics;
- var definition = new JintFunctionDefinition(new ClassStaticBlockFunction(o));
+ if (!engine.TryGetFunctionDefinition(o, out var definition))
+ {
+ engine.CacheFunctionDefinition(o, definition = new JintFunctionDefinition(new ClassStaticBlockFunction(o)));
+ }
var lex = engine.ExecutionContext.LexicalEnvironment;
var privateEnv = engine.ExecutionContext.PrivateEnvironment;
@@ -799,7 +814,11 @@ public ClassStaticBlockFunction(StaticBlock staticBlock) : base(NodeType.StaticB
return DefineMethodProperty(obj, methodDef.Key, methodDef.Closure, enumerable);
}
- var definition = new JintFunctionDefinition(function, sourceTextNode: method);
+ if (!engine.TryGetFunctionDefinition((Node) function, out var definition))
+ {
+ engine.CacheFunctionDefinition((Node) function, definition = new JintFunctionDefinition(function, sourceTextNode: method));
+ }
+
var intrinsics = engine.Realm.Intrinsics;
var value = method.TryGetKey(engine);