diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json index 9a84905da8..cf93a04109 100644 --- a/Jint.Tests.Test262/Test262Harness.settings.json +++ b/Jint.Tests.Test262/Test262Harness.settings.json @@ -8,7 +8,6 @@ "Array.fromAsync", "async-iteration", "Atomics", - "generators", "import-assertions", "iterator-helpers", "regexp-duplicate-named-groups", diff --git a/Jint.Tests/Runtime/GeneratorTests.cs b/Jint.Tests/Runtime/GeneratorTests.cs new file mode 100644 index 0000000000..fa6b996f35 --- /dev/null +++ b/Jint.Tests/Runtime/GeneratorTests.cs @@ -0,0 +1,256 @@ +namespace Jint.Tests.Runtime; + +public class GeneratorTests +{ + [Fact] + public void LoopYield() + { + const string Script = """ + const foo = function*() { + yield 'a'; + yield 'b'; + yield 'c'; + }; + + let str = ''; + for (const val of foo()) { + str += val; + } + return str; + """; + + var engine = new Engine(); + Assert.Equal("abc", engine.Evaluate(Script)); + } + + [Fact] + public void ReturnDuringYield() + { + const string Script = """ + const foo = function*() { + yield 'a'; + return; + yield 'c'; + }; + + let str = ''; + for (const val of foo()) { + str += val; + } + return str; + """; + + var engine = new Engine(); + Assert.Equal("a", engine.Evaluate(Script)); + } + + [Fact] + public void LoneReturnInYield() + { + const string Script = """ + const foo = function*() { + return; + }; + + let str = ''; + for (const val of foo()) { + str += val; + } + return str; + """; + + var engine = new Engine(); + Assert.Equal("", engine.Evaluate(Script)); + } + + [Fact] + public void LoneReturnValueInYield() + { + const string Script = """ + const foo = function*() { + return 'a'; + }; + + let str = ''; + for (const val of foo()) { + str += val; + } + return str; + """; + + var engine = new Engine(); + Assert.Equal("", engine.Evaluate(Script)); + } + + [Fact] + public void YieldUndefined() + { + const string Script = """ + const foo = function*() { + yield undefined; + }; + + let str = ''; + for (const val of foo()) { + str += val; + } + return str; + """; + + var engine = new Engine(); + Assert.Equal("undefined", engine.Evaluate(Script)); + } + + [Fact] + public void ReturnUndefined() + { + const string Script = """ + const foo = function*() { + return undefined; + }; + + let str = ''; + for (const val of foo()) { + str += val; + } + return str; + """; + + var engine = new Engine(); + Assert.Equal("", engine.Evaluate(Script)); + } + + [Fact] + public void Basic() + { + var engine = new Engine(); + engine.Execute("function * generator() { yield 5; yield 6; };"); + engine.Execute("var iterator = generator(); var item = iterator.next();"); + Assert.Equal(5, engine.Evaluate("item.value")); + Assert.False(engine.Evaluate("item.done").AsBoolean()); + engine.Execute("item = iterator.next();"); + Assert.Equal(6, engine.Evaluate("item.value")); + Assert.False(engine.Evaluate("item.done").AsBoolean()); + engine.Execute("item = iterator.next();"); + Assert.True(engine.Evaluate("item.value === void undefined").AsBoolean()); + Assert.True(engine.Evaluate("item.done").AsBoolean()); + } + + [Fact] + public void FunctionExpressions() + { + var engine = new Engine(); + engine.Execute("var generator = function * () { yield 5; yield 6; };"); + engine.Execute("var iterator = generator(); var item = iterator.next();"); + Assert.Equal(5, engine.Evaluate("item.value")); + Assert.False(engine.Evaluate("item.done").AsBoolean()); + engine.Execute("item = iterator.next();"); + Assert.Equal(6, engine.Evaluate("item.value")); + Assert.False(engine.Evaluate("item.done").AsBoolean()); + engine.Execute("item = iterator.next();"); + Assert.True(engine.Evaluate("item.value === void undefined").AsBoolean()); + Assert.True(engine.Evaluate("item.done").AsBoolean()); + } + + [Fact] + public void CorrectThisBinding() + { + var engine = new Engine(); + engine.Execute("var generator = function * () { yield 5; yield 6; };"); + engine.Execute("var iterator = { g: generator, x: 5, y: 6 }.g(); var item = iterator.next();"); + Assert.Equal(5, engine.Evaluate("item.value")); + Assert.False(engine.Evaluate("item.done").AsBoolean()); + engine.Execute("item = iterator.next();"); + Assert.Equal(6, engine.Evaluate("item.value")); + Assert.False(engine.Evaluate("item.done").AsBoolean()); + engine.Execute("item = iterator.next();"); + Assert.True(engine.Evaluate("item.value === void undefined").AsBoolean()); + Assert.True(engine.Evaluate("item.done").AsBoolean()); + } + + [Fact(Skip = "TODO es6-generators")] + public void Sending() + { + const string Script = """ + var sent; + function * generator() { + sent = [yield 5, yield 6]; + }; + var iterator = generator(); + iterator.next(); + iterator.next("foo"); + iterator.next("bar"); + """; + + var engine = new Engine(); + engine.Execute(Script); + + Assert.Equal("foo", engine.Evaluate("sent[0]")); + Assert.Equal("bar", engine.Evaluate("sent[1]")); + } + + [Fact(Skip = "TODO es6-generators")] + public void Sending2() + { + const string Script = """ + function* counter(value) { + while (true) { + const step = yield value++; + + if (step) { + value += step; + } + } + } + + const generatorFunc = counter(0); + """; + + var engine = new Engine(); + engine.Execute(Script); + + Assert.Equal(0, engine.Evaluate("generatorFunc.next().value")); // 0 + Assert.Equal(1, engine.Evaluate("generatorFunc.next().value")); // 1 + Assert.Equal(2, engine.Evaluate("generatorFunc.next().value")); // 2 + Assert.Equal(3, engine.Evaluate("generatorFunc.next().value")); // 3 + Assert.Equal(14, engine.Evaluate("generatorFunc.next(10).value")); // 14 + Assert.Equal(15, engine.Evaluate("generatorFunc.next().value")); // 15 + Assert.Equal(26, engine.Evaluate("generatorFunc.next(10).value")); // 26 + } + + [Fact(Skip = "TODO es6-generators")] + public void Fibonacci() + { + const string Script = """ + function* fibonacci() { + let current = 0; + let next = 1; + while (true) { + const reset = yield current; + [current, next] = [next, next + current]; + if (reset) { + current = 0; + next = 1; + } + } + } + + const sequence = fibonacci(); + """; + + var engine = new Engine(); + engine.Execute(Script); + + Assert.Equal(0, engine.Evaluate("sequence.next().value")); + Assert.Equal(1, engine.Evaluate("sequence.next().value")); + Assert.Equal(1, engine.Evaluate("sequence.next().value")); + Assert.Equal(2, engine.Evaluate("sequence.next().value")); + Assert.Equal(3, engine.Evaluate("sequence.next().value")); + Assert.Equal(5, engine.Evaluate("sequence.next().value")); + Assert.Equal(9, engine.Evaluate("sequence.next().value")); + Assert.Equal(0, engine.Evaluate("sequence.next(true).value")); + Assert.Equal(1, engine.Evaluate("sequence.next().value)")); + Assert.Equal(1, engine.Evaluate("sequence.next().value)")); + Assert.Equal(2, engine.Evaluate("sequence.next().value)")); + } +} diff --git a/Jint/Runtime/Interpreter/Expressions/JintExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintExpression.cs index 2329635f1d..cbd8978df7 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintExpression.cs +++ b/Jint/Runtime/Interpreter/Expressions/JintExpression.cs @@ -132,6 +132,7 @@ protected internal static JintExpression Build(Expression expression) ? new JintCallExpression((CallExpression) ((ChainExpression) expression).Expression) : new JintMemberExpression((MemberExpression) ((ChainExpression) expression).Expression), Nodes.AwaitExpression => new JintAwaitExpression((AwaitExpression) expression), + Nodes.YieldExpression => new JintYieldExpression((YieldExpression) expression), _ => null }; diff --git a/Jint/Runtime/Interpreter/Expressions/JintYieldExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintYieldExpression.cs new file mode 100644 index 0000000000..ca977c864f --- /dev/null +++ b/Jint/Runtime/Interpreter/Expressions/JintYieldExpression.cs @@ -0,0 +1,234 @@ +using Esprima.Ast; +using Jint.Native; +using Jint.Native.Generator; +using Jint.Native.Iterator; +using Jint.Native.Object; + +namespace Jint.Runtime.Interpreter.Expressions; + +internal sealed class JintYieldExpression : JintExpression +{ + public JintYieldExpression(YieldExpression expression) : base(expression) + { + } + + protected override object EvaluateInternal(EvaluationContext context) + { + var expression = (YieldExpression) _expression; + + JsValue value; + if (context.Engine.ExecutionContext.Generator?._nextValue is not null) + { + value = context.Engine.ExecutionContext.Generator._nextValue; + } + else if (expression.Argument is not null) + { + value = Build(expression.Argument).GetValue(context); + } + else + { + value = JsValue.Undefined; + } + + if (expression.Delegate) + { + value = YieldDelegate(context, value); + } + + return Yield(context, value); + } + + /// + /// https://tc39.es/ecma262/#sec-generator-function-definitions-runtime-semantics-evaluation + /// + private JsValue YieldDelegate(EvaluationContext context, JsValue value) + { + var engine = context.Engine; + var generatorKind = engine.ExecutionContext.GetGeneratorKind(); + var iterator = value.GetIterator(engine.Realm, generatorKind); + var iteratorRecord = iterator; + var received = new Completion(CompletionType.Normal, JsValue.Undefined, _expression); + while (true) + { + if (received.Type == CompletionType.Normal) + { + iterator.TryIteratorStep(out var innerResult); + if (generatorKind == GeneratorKind.Async) + { + innerResult = Await(innerResult); + } + + if (innerResult is not IteratorResult oi) + { + ExceptionHelper.ThrowTypeError(engine.Realm); + } + + var done = IteratorComplete(innerResult); + if (done) + { + return IteratorValue(innerResult); + } + + if (generatorKind == GeneratorKind.Async) + { + received = AsyncGeneratorYield(IteratorValue(innerResult)); + } + else + { + received = GeneratorYield(innerResult); + } + + } + else if (received.Type == CompletionType.Throw) + { + var throwMethod = iterator.GetMethod("throw"); + if (throwMethod is not null) + { + var innerResult = throwMethod.Call(iterator, new[]{ received.Value }); + if (generatorKind == GeneratorKind.Async) + { + innerResult = Await(innerResult); + } + // NOTE: Exceptions from the inner iterator throw method are propagated. + // Normal completions from an inner throw method are processed similarly to an inner next. + if (innerResult is not ObjectInstance oi) + { + ExceptionHelper.ThrowTypeError(engine.Realm); + } + + var done = IteratorComplete(innerResult); + if (done) + { + IteratorValue(innerResult); + } + + if (generatorKind == GeneratorKind.Async) + { + received = AsyncGeneratorYield(IteratorValue(innerResult)); + } + else + { + received = GeneratorYield(innerResult); + } + } + else + { + // NOTE: If iterator does not have a throw method, this throw is going to terminate the yield* loop. + // But first we need to give iterator a chance to clean up. + var closeCompletion = new Completion(CompletionType.Normal, null!, _expression); + if (generatorKind == GeneratorKind.Async) + { + AsyncIteratorClose(iteratorRecord, CompletionType.Normal); + } + else + { + iteratorRecord.Close(CompletionType.Normal); + } + + ExceptionHelper.ThrowTypeError(engine.Realm, "Iterator does not have close method"); + } + } + else + { + var returnMethod = iterator.GetMethod("return"); + if (returnMethod is null) + { + var temp = received.Value; + if (generatorKind == GeneratorKind.Async) + { + temp = Await(received.Value); + } + + return temp; + } + + var innerReturnResult = returnMethod.Call(iterator, new[] { received.Value }); + if (generatorKind == GeneratorKind.Async) + { + innerReturnResult = Await(innerReturnResult); + } + + if (innerReturnResult is not ObjectInstance oi) + { + ExceptionHelper.ThrowTypeError(engine.Realm); + } + + var done = IteratorComplete(innerReturnResult); + if (done) + { + var val = IteratorValue(innerReturnResult); + return val; + } + + if (generatorKind == GeneratorKind.Async) + { + received = AsyncGeneratorYield(IteratorValue(innerReturnResult)); + } + else + { + received = GeneratorYield(innerReturnResult); + } + } + } + } + + private Completion GeneratorYield(JsValue innerResult) + { + throw new System.NotImplementedException(); + } + + private static bool IteratorComplete(JsValue iterResult) + { + return TypeConverter.ToBoolean(iterResult.Get(CommonProperties.Done)); + } + + private static JsValue IteratorValue(JsValue iterResult) + { + return iterResult.Get(CommonProperties.Value); + } + + private static void AsyncIteratorClose(object iteratorRecord, CompletionType closeCompletion) + { + ExceptionHelper.ThrowNotImplementedException("async"); + } + + /// + /// https://tc39.es/ecma262/#sec-asyncgeneratoryield + /// + private static Completion AsyncGeneratorYield(object iteratorValue) + { + ExceptionHelper.ThrowNotImplementedException("async"); + return default; + } + + /// + /// https://tc39.es/ecma262/#await + /// + private static ObjectInstance Await(JsValue innerResult) + { + ExceptionHelper.ThrowNotImplementedException("await"); + return null; + } + + /// + /// https://tc39.es/ecma262/#sec-yield + /// + private static JsValue Yield(EvaluationContext context, JsValue iterNextObj) + { + var engine = context.Engine; + var generatorKind = engine.ExecutionContext.GetGeneratorKind(); + if (generatorKind == GeneratorKind.Async) + { + // TODO return ? AsyncGeneratorYield(undefined); + ExceptionHelper.ThrowNotImplementedException("async not implemented"); + } + + // https://tc39.es/ecma262/#sec-generatoryield + var genContext = engine.ExecutionContext; + var generator = genContext.Generator; + generator!._generatorState = GeneratorState.SuspendedYield; + //_engine.LeaveExecutionContext(); + + return iterNextObj; + } +}