diff --git a/Jurassic/Compiler/Emit/OptimizationInfo.cs b/Jurassic/Compiler/Emit/OptimizationInfo.cs
index 48456f34..e27263fb 100644
--- a/Jurassic/Compiler/Emit/OptimizationInfo.cs
+++ b/Jurassic/Compiler/Emit/OptimizationInfo.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Reflection;
namespace Jurassic.Compiler
{
@@ -409,6 +410,21 @@ public int LongJumpStackSizeThreshold
set;
}
+ public Action EmitOnLoopIteration { get; set; }
+
+ public void TryEmitOnLoopIteration(ILGenerator generator)
+ {
+ if (EmitOnLoopIteration == null)
+ return;
+
+ if (EmitOnLoopIteration.Target != null)
+ {
+ generator.LoadArgument(0);
+ generator.LoadField(typeof(ScriptEngine).GetField(nameof(ScriptEngine.OnLoopIterationCallTarget)));
+ }
+ generator.Call(EmitOnLoopIteration.Method);
+ }
+
///
/// Emits code to branch between statements, even if code generation is within a finally
/// block (where unconditional branches are not allowed).
@@ -417,6 +433,7 @@ public int LongJumpStackSizeThreshold
/// The label to jump to.
public void EmitLongJump(ILGenerator generator, ILLabel targetLabel)
{
+ TryEmitOnLoopIteration(generator);
if (this.LongJumpCallback == null)
{
// Code generation is not inside a finally block.
diff --git a/Jurassic/Compiler/MethodGenerator/CompilerOptions.cs b/Jurassic/Compiler/MethodGenerator/CompilerOptions.cs
index c7895a96..be9e583b 100644
--- a/Jurassic/Compiler/MethodGenerator/CompilerOptions.cs
+++ b/Jurassic/Compiler/MethodGenerator/CompilerOptions.cs
@@ -38,6 +38,8 @@ public CompilerOptions()
///
public bool EnableILAnalysis { get; set; }
+ public Action EmitOnLoopIteration { get; set; }
+
///
/// Performs a shallow clone of this instance.
///
diff --git a/Jurassic/Compiler/MethodGenerator/MethodGenerator.cs b/Jurassic/Compiler/MethodGenerator/MethodGenerator.cs
index 6d837c80..934185ce 100644
--- a/Jurassic/Compiler/MethodGenerator/MethodGenerator.cs
+++ b/Jurassic/Compiler/MethodGenerator/MethodGenerator.cs
@@ -177,6 +177,8 @@ public void GenerateCode()
optimizationInfo.MethodOptimizationHints = this.MethodOptimizationHints;
optimizationInfo.FunctionName = this.GetStackName();
optimizationInfo.Source = this.Source;
+ optimizationInfo.EmitOnLoopIteration = Options.EmitOnLoopIteration;
+
ILGenerator generator;
if (this.Options.EnableDebugging == false)
diff --git a/Jurassic/Compiler/Statements/ForInStatement.cs b/Jurassic/Compiler/Statements/ForInStatement.cs
index ba2ce6fa..632681e8 100644
--- a/Jurassic/Compiler/Statements/ForInStatement.cs
+++ b/Jurassic/Compiler/Statements/ForInStatement.cs
@@ -124,6 +124,7 @@ public override void GenerateCode(ILGenerator generator, OptimizationInfo optimi
this.Body.GenerateCode(generator, optimizationInfo);
optimizationInfo.PopBreakOrContinueInfo();
+ optimizationInfo.TryEmitOnLoopIteration(generator);
generator.Branch(continueTarget);
generator.DefineLabelPosition(breakTarget);
diff --git a/Jurassic/Compiler/Statements/ForOfStatement.cs b/Jurassic/Compiler/Statements/ForOfStatement.cs
index 12ececc3..1e21dfac 100644
--- a/Jurassic/Compiler/Statements/ForOfStatement.cs
+++ b/Jurassic/Compiler/Statements/ForOfStatement.cs
@@ -127,6 +127,7 @@ public override void GenerateCode(ILGenerator generator, OptimizationInfo optimi
this.Body.GenerateCode(generator, optimizationInfo);
optimizationInfo.PopBreakOrContinueInfo();
+ optimizationInfo.TryEmitOnLoopIteration(generator);
generator.Branch(continueTarget);
generator.DefineLabelPosition(breakTarget);
diff --git a/Jurassic/Compiler/Statements/LoopStatement.cs b/Jurassic/Compiler/Statements/LoopStatement.cs
index cb1b5109..8732b55c 100644
--- a/Jurassic/Compiler/Statements/LoopStatement.cs
+++ b/Jurassic/Compiler/Statements/LoopStatement.cs
@@ -225,6 +225,7 @@ public override void GenerateCode(ILGenerator generator, OptimizationInfo optimi
if (this.IncrementStatement != null)
this.IncrementStatement.GenerateCode(generator, optimizationInfo);
+ optimizationInfo.TryEmitOnLoopIteration(generator);
// Unconditionally branch back to the start of the loop.
generator.Branch(startOfLoop);
diff --git a/Jurassic/Core/ScriptEngine.cs b/Jurassic/Core/ScriptEngine.cs
index 6a4b20ba..c6a665b4 100644
--- a/Jurassic/Core/ScriptEngine.cs
+++ b/Jurassic/Core/ScriptEngine.cs
@@ -881,6 +881,7 @@ private CompilerOptions CreateOptions()
EnableDebugging = this.EnableDebugging,
CompatibilityMode = this.CompatibilityMode,
EnableILAnalysis = this.EnableILAnalysis,
+ EmitOnLoopIteration = this.OnLoopIterationCall
};
}
@@ -1368,5 +1369,19 @@ internal Dictionary StaticTypeWrapperCache
return this.staticTypeWrapperCache;
}
}
+
+ // Emit on loop iteration (before loop's branch call)
+ //_________________________________________________________________________________________
+ private Action _onLoopIterationCall;
+ public Action OnLoopIterationCall
+ {
+ get { return _onLoopIterationCall; }
+ set
+ {
+ _onLoopIterationCall = value;
+ OnLoopIterationCallTarget = OnLoopIterationCall?.Target;
+ }
+ }
+ public object OnLoopIterationCallTarget;
}
}
diff --git a/Unit Tests/Library/EmitUserCodeOnLoopIterationTests.cs b/Unit Tests/Library/EmitUserCodeOnLoopIterationTests.cs
new file mode 100644
index 00000000..4dac95e2
--- /dev/null
+++ b/Unit Tests/Library/EmitUserCodeOnLoopIterationTests.cs
@@ -0,0 +1,207 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Jurassic;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace UnitTests.Library
+{
+
+ public class ClassWithCallCounters
+ {
+ private readonly int _iterationsLimit;
+ public int InstanceCounter = 0;
+ public static int StaticCounter = 0;
+
+ public ClassWithCallCounters(int iterationsLimit=100)
+ {
+ _iterationsLimit = iterationsLimit;
+ }
+ public void InstanceMethod()
+ {
+ InstanceCounter++;
+
+ if (InstanceCounter > _iterationsLimit)
+ throw new InvalidOperationException($"Reached a maximum of "+_iterationsLimit +" iterations");
+
+
+ }
+
+ public static void StaticMetod()
+ {
+ StaticCounter++;
+ }
+ }
+
+ [TestClass]
+ public class EmitUserCodeOnLoopIterationTests
+ {
+ [TestMethod]
+ public void OnLoopCallWithInstanceMethod()
+ {
+ var engine = new ScriptEngine();
+ var classWithCallCounters = new ClassWithCallCounters();
+ engine.OnLoopIterationCall = classWithCallCounters.InstanceMethod;
+ var loopScript = @"
+function Test(){
+
+for (var i=0; i< 20; i++){
+
+}
+}";
+ engine.Evaluate(loopScript);
+ engine.CallGlobalFunction("Test");
+ Assert.AreEqual(19, classWithCallCounters.InstanceCounter);
+ }
+
+ [TestMethod]
+ public void OnLoopCallWithInstanceMethodInDebug()
+ {
+ var engine = new ScriptEngine();
+ engine.EnableDebugging = true;
+ var classWithCallCounters = new ClassWithCallCounters();
+ engine.OnLoopIterationCall = classWithCallCounters.InstanceMethod;
+ var loopScript = @"
+function Test(){
+
+for (var i=0; i< 20; i++){
+
+}
+}";
+ engine.Evaluate(loopScript);
+ engine.CallGlobalFunction("Test");
+ Assert.AreEqual(19, classWithCallCounters.InstanceCounter);
+ }
+
+ [TestMethod]
+ public void OnLoopCallWithStaticMethod()
+ {
+ var engine = new ScriptEngine();
+
+ engine.OnLoopIterationCall = ClassWithCallCounters.StaticMetod;
+ var staticCounterBefore = ClassWithCallCounters.StaticCounter;
+ var loopScript = @"
+function Test(){
+
+for (var i=0; i< 20; i++){
+
+}
+}";
+ engine.Evaluate(loopScript);
+ engine.CallGlobalFunction("Test");
+ Assert.AreEqual(19, ClassWithCallCounters.StaticCounter - staticCounterBefore);
+ }
+
+
+ [TestMethod]
+ public void OnLoopTerationAndTrivialContinueStatement()
+ {
+ var engine = new ScriptEngine();
+ var classWithCallCounters = new ClassWithCallCounters();
+ engine.OnLoopIterationCall = classWithCallCounters.InstanceMethod;
+ var loopScript = @"
+function Test(){
+for ( var i=0; i< 10; i++){
+continue;
+}
+}";
+ engine.Evaluate(loopScript);
+ engine.CallGlobalFunction("Test");
+ Assert.AreEqual(19, classWithCallCounters.InstanceCounter);
+ }
+
+ [TestMethod]
+ public void OnLoopTerationAndLabeledContinueStatement()
+ {
+ var engine = new ScriptEngine();
+ var classWithCallCounters = new ClassWithCallCounters();
+ engine.OnLoopIterationCall = classWithCallCounters.InstanceMethod;
+ var loopScript = @"
+function Test(){
+label1:
+for ( var i=0; i< 10; i++){
+continue label1;
+}
+}";
+ engine.Evaluate(loopScript);
+ engine.CallGlobalFunction("Test");
+ Assert.AreEqual(19, classWithCallCounters.InstanceCounter);
+ }
+
+ [TestMethod]
+ public void OnNestedLoop()
+ {
+ var engine = new ScriptEngine();
+ var classWithCallCounters = new ClassWithCallCounters();
+ engine.OnLoopIterationCall = classWithCallCounters.InstanceMethod;
+ var loopScript = @"
+function Test(){
+
+for ( var i=0; i< 10; i++){
+for (var j=0; j< 10; j++){
+}
+
+}
+}";
+ engine.Evaluate(loopScript);
+
+ engine.CallGlobalFunction("Test");
+
+ Assert.AreEqual(99, classWithCallCounters.InstanceCounter);
+
+ }
+
+ [TestMethod]
+ public void OnLoopTerationAndNestedLabeledContinueStatement()
+ {
+ var engine = new ScriptEngine();
+ var classWithCallCounters = new ClassWithCallCounters();
+ engine.OnLoopIterationCall = classWithCallCounters.InstanceMethod;
+ var loopScript = @"
+function Test(){
+label1:
+for ( var i=0; i< 10; i++){
+label2:
+for (var j=0; j< 10; j++){
+continue label1;
+}
+
+}
+}";
+ engine.Evaluate(loopScript);
+
+ engine.CallGlobalFunction("Test");
+
+ Assert.AreEqual(19, classWithCallCounters.InstanceCounter);
+ }
+
+ [TestMethod]
+ public void ThrowRightExceptionOnALotOfIterations()
+ {
+ var engine = new ScriptEngine();
+ var classWithCallCounters = new ClassWithCallCounters(5);
+ engine.OnLoopIterationCall = classWithCallCounters.InstanceMethod;
+ var loopScript = @"
+function Test(){
+
+for ( var i=0; i< 10; i++){
+
+}
+}";
+ engine.Evaluate(loopScript);
+
+ Exception e = null;
+ try
+ {
+ engine.CallGlobalFunction("Test");
+ }
+ catch (Exception ex)
+ {
+ e = ex;
+ }
+
+ Assert.IsInstanceOfType(e, typeof(InvalidOperationException));
+ }
+ }
+}
diff --git a/Unit Tests/Unit Tests.csproj b/Unit Tests/Unit Tests.csproj
index e5c0e850..f210eff2 100644
--- a/Unit Tests/Unit Tests.csproj
+++ b/Unit Tests/Unit Tests.csproj
@@ -72,6 +72,7 @@
+