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 @@ +