From af892ad26808e55059076e2a7221406cc2518cfd Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Tue, 16 Jun 2026 11:49:05 -0700 Subject: [PATCH] Compiled: static async generators (#778), class-expression generators (#765), symbol-method binding + class-expr symbol methods (#755 s1/s2) Route the remaining compiled-mode generator/symbol-method cases through the existing state-machine and symbol-registry infrastructure (no new machinery); all IL-verified and identical across both back ends. #778: generalize EmitAsyncGeneratorMethodBody with an isInstanceMethod flag + a static stub (no `this`); EmitStaticMethodBody dispatches `static async *m()` there first. (Sync `static *m()` was already #692.) #765: class-expression instance/static method emitters give generator / async-generator methods the right return type and dispatch them to the same state-machine emitters as class declarations. #755 s1: the symbol-method bracket-get returns `new $TSFunction(obj, method)` (receiver-bound) instead of a raw MethodInfo, so `obj[Symbol.iterator]()` keeps `this`, mirroring the string-key path. #755 s2: class expressions wire computed symbol-keyed methods through the same synthetic-method + SymbolMethods registry + .cctor registration as declarations. Follow-ups filed: #789 (class-expr generator arrow write-capture DC), #791 (#755 s3 non-symbol keys), #793 (class-expr inferred method return inference). Full SharpTS.Tests suite green. --- Compilation/ILCompiler.AsyncGenerators.cs | 68 +++++++- .../ILCompiler.Classes.ClassExpressions.cs | 152 ++++++++++++++++-- Compilation/ILCompiler.Classes.Static.cs | 14 +- Compilation/RuntimeEmitter.Objects.Index.cs | 14 +- .../SharedTests/ClassExpressionTests.cs | 85 ++++++++++ .../SharedTests/ComputedSymbolMethodTests.cs | 38 +++-- .../SharedTests/StaticGeneratorMethodTests.cs | 58 ++++++- 7 files changed, 387 insertions(+), 42 deletions(-) diff --git a/Compilation/ILCompiler.AsyncGenerators.cs b/Compilation/ILCompiler.AsyncGenerators.cs index 4a9cab79..9d6bee0e 100644 --- a/Compilation/ILCompiler.AsyncGenerators.cs +++ b/Compilation/ILCompiler.AsyncGenerators.cs @@ -222,19 +222,21 @@ private void EmitAsyncGeneratorMoveNextAsyncBody(AsyncGeneratorStateMachineBuild /// Emits the body of an instance async generator method using a state machine. /// Called for class methods marked with both IsAsync and IsGenerator = true. /// - private void EmitAsyncGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Function method, FieldInfo fieldsField, - string? currentClassName = null) + private void EmitAsyncGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Function method, FieldInfo? fieldsField, + bool isInstanceMethod = true, string? currentClassName = null) { // Analyze async generator function to determine yield/await points and hoisted variables var analysis = _asyncGenerators.Analyzer.Analyze(method); - // Build state machine type for instance method. Use the MethodBuilder's (mangled) name rather - // than method.Name.Lexeme so a private async generator's `#p` lexeme doesn't put a `#` in the name. + // Build state machine type. A static async generator method (#778) has no `this`/instance fields, + // so it is set up like a free function (isInstanceMethod: false, static stub). Use the + // MethodBuilder's (mangled) name rather than method.Name.Lexeme so a private async generator's + // `#p` lexeme doesn't put a `#` in the name. var smBuilder = new AsyncGeneratorStateMachineBuilder(_moduleBuilder, _types, _asyncGenerators.StateMachineCounter++); smBuilder.DefineStateMachine( $"{methodBuilder.DeclaringType!.Name}_{methodBuilder.Name}", analysis, - isInstanceMethod: true, // This is an instance method + isInstanceMethod: isInstanceMethod, runtime: _runtime ); @@ -246,15 +248,21 @@ private void EmitAsyncGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Func if (methodDCKey != null && _closures.FunctionDisplayClasses.TryGetValue(methodDCKey, out var methodFuncDC)) smBuilder.DefineFunctionDisplayClassField(methodFuncDC); - // Emit stub method body (creates state machine and returns it) - EmitAsyncGeneratorInstanceStubMethod(methodBuilder, smBuilder, method, methodDCKey); + // Emit stub method body (creates state machine and returns it). A static async generator method + // (#778) has no `this` and no function-DC write-capture support (it is not registered in + // RegisterGeneratorMethodFunctionDisplayClasses, so methodDCKey is null), mirroring the sync + // static generator (#692). + if (isInstanceMethod) + EmitAsyncGeneratorInstanceStubMethod(methodBuilder, smBuilder, method, methodDCKey); + else + EmitAsyncGeneratorStaticStubMethod(methodBuilder, smBuilder, method.Parameters); // Create context for MoveNextAsync emission var il = smBuilder.MoveNextAsyncMethod.GetILGenerator(); var ctx = new CompilationContext(il, _typeMapper, _functions.Builders, _classes.Builders, _namespaceFields, _namespaceVarFields, _types) { FieldsField = fieldsField, - IsInstanceMethod = true, + IsInstanceMethod = isInstanceMethod, ClosureAnalyzer = _closures.Analyzer, ArrowMethods = _closures.ArrowMethods, ConstArrowBindings = _closures.ConstArrowBindings, @@ -378,4 +386,48 @@ private void EmitAsyncGeneratorInstanceStubMethod( // Return the state machine (which implements IAsyncEnumerable) il.Emit(OpCodes.Ret); } + + /// + /// Emits the stub that creates the async generator state machine for a STATIC method (#778): like + /// the instance stub but with no this and parameters starting at arg 0 (no receiver slot). + /// Value-type parameters are boxed into the object-typed state-machine fields. Mirrors + /// (the sync analogue, #692). + /// + private void EmitAsyncGeneratorStaticStubMethod( + MethodBuilder methodBuilder, + AsyncGeneratorStateMachineBuilder smBuilder, + List parameters) + { + var il = methodBuilder.GetILGenerator(); + + // Create new instance of the state machine + il.Emit(OpCodes.Newobj, smBuilder.Constructor); + + // Box value types since state machine fields are object-typed. Decide from the method's ACTUAL + // IL signature (methodBuilder.GetParameters()), not the AST-resolved types: a private static + // method's parameters are all `object` slots, so boxing the AST-resolved value type would + // mismatch the `object` argument actually loaded (StackUnexpected). Mirrors EmitAsyncStubMethod. + var paramTypes = methodBuilder.GetParameters(); + + // Copy parameters to state machine fields (static methods start params at index 0). + for (int i = 0; i < parameters.Count; i++) + { + var field = smBuilder.GetVariableField(parameters[i].Name.Lexeme); + if (field != null) + { + il.Emit(OpCodes.Dup); // Keep state machine reference on stack + il.Emit(OpCodes.Ldarg, i); + + if (i < paramTypes.Length && paramTypes[i].ParameterType.IsValueType) + { + il.Emit(OpCodes.Box, paramTypes[i].ParameterType); + } + + il.Emit(OpCodes.Stfld, field); + } + } + + // Return the state machine (which implements IAsyncEnumerable) + il.Emit(OpCodes.Ret); + } } diff --git a/Compilation/ILCompiler.Classes.ClassExpressions.cs b/Compilation/ILCompiler.Classes.ClassExpressions.cs index ab7dc4bb..1b2a6f92 100644 --- a/Compilation/ILCompiler.Classes.ClassExpressions.cs +++ b/Compilation/ILCompiler.Classes.ClassExpressions.cs @@ -319,11 +319,17 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) _classExprs.Constructors[classExpr] = ctorBuilder; _classes.Constructors[className] = ctorBuilder; - // Define static methods - foreach (var method in classExpr.Methods.Where(m => m.Body != null && m.IsStatic && m.Name.Lexeme != "constructor")) + // Define static methods (computed symbol-keyed methods are handled by + // DefineClassExpressionSymbolMethods below, like the class-declaration path). + foreach (var method in classExpr.Methods.Where(m => m.Body != null && m.IsStatic && m.Name.Lexeme != "constructor" && m.ComputedKey == null)) { var paramTypes = method.Parameters.Select(_ => typeof(object)).ToArray(); - Type returnType = method.IsAsync ? _types.TaskOfObject : typeof(object); + // Match the method kind to its state machine's return type (#765), mirroring + // DefineClassMethodsOnly. Async generator FIRST since it has both flags set. + Type returnType = (method.IsAsync && method.IsGenerator) ? _types.IAsyncEnumerableOfObject : + method.IsAsync ? _types.TaskOfObject : + method.IsGenerator ? _types.IEnumerableOfObject : + typeof(object); var methodBuilder = typeBuilder.DefineMethod( method.Name.Lexeme, @@ -334,8 +340,9 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) _classExprs.StaticMethods[classExpr][method.Name.Lexeme] = methodBuilder; } - // Define instance methods - foreach (var method in classExpr.Methods.Where(m => m.Body != null && !m.IsStatic && m.Name.Lexeme != "constructor")) + // Define instance methods (computed symbol-keyed methods are handled by + // DefineClassExpressionSymbolMethods below, like the class-declaration path). + foreach (var method in classExpr.Methods.Where(m => m.Body != null && !m.IsStatic && m.Name.Lexeme != "constructor" && m.ComputedKey == null)) { var paramTypes = method.Parameters.Select(_ => typeof(object)).ToArray(); @@ -343,7 +350,12 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) if (method.IsAbstract) methodAttrs |= MethodAttributes.Abstract; - Type returnType = method.IsAsync ? typeof(Task) : typeof(object); + // Match the method kind to its state machine's return type (#765), mirroring + // DefineClassMethodsOnly. Async generator FIRST since it has both flags set. + Type returnType = (method.IsAsync && method.IsGenerator) ? _types.IAsyncEnumerableOfObject : + method.IsAsync ? typeof(Task) : + method.IsGenerator ? _types.IEnumerableOfObject : + typeof(object); var methodBuilder = typeBuilder.DefineMethod( method.Name.Lexeme, @@ -354,6 +366,10 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) _classExprs.InstanceMethods[classExpr][method.Name.Lexeme] = methodBuilder; } + // Computed symbol-keyed methods (`*[Symbol.iterator]()` etc.) get synthetic uniquely-named + // builders plus runtime symbol-method registration, mirroring the class-declaration path (#755). + DefineClassExpressionSymbolMethods(classExpr, typeBuilder); + // Define user-defined accessors (overrides property accessors) if (classExpr.Accessors != null) { @@ -428,18 +444,22 @@ private void EmitClassExpressionBody(Expr.ClassExpr classExpr) // Emit instance constructor EmitClassExpressionConstructor(classExpr, typeBuilder, fieldsField); - // Emit instance method bodies - foreach (var method in classExpr.Methods.Where(m => m.Body != null && !m.IsStatic && m.Name.Lexeme != "constructor")) + // Emit instance method bodies (computed symbol-keyed methods are emitted below). + foreach (var method in classExpr.Methods.Where(m => m.Body != null && !m.IsStatic && m.Name.Lexeme != "constructor" && m.ComputedKey == null)) { EmitClassExpressionMethod(classExpr, typeBuilder, method, fieldsField); } - // Emit static method bodies - foreach (var method in classExpr.Methods.Where(m => m.Body != null && m.IsStatic)) + // Emit static method bodies (computed symbol-keyed methods are emitted below). + foreach (var method in classExpr.Methods.Where(m => m.Body != null && m.IsStatic && m.ComputedKey == null)) { EmitClassExpressionStaticMethodBody(classExpr, method); } + // Emit computed symbol-keyed method bodies (#755), then their runtime registration runs in + // the class-expression .cctor (EmitClassExpressionStaticConstructor → EmitSymbolMethodRegistrations). + EmitClassExpressionSymbolMethods(classExpr, typeBuilder, fieldsField); + // Emit user-defined accessor bodies if (classExpr.Accessors != null) { @@ -547,11 +567,12 @@ private void EmitClassExpressionStaticConstructor(Expr.ClassExpr classExpr, Type { bool hasStaticFields = classExpr.Fields.Any(f => f.IsStatic && f.Initializer != null); bool hasStaticInitializers = classExpr.StaticInitializers?.Count > 0; - // Symbol-keyed computed accessors (#281) register in the .cctor, keyed by - // this class's generated name (mirrors the class-declaration path #266). + // Symbol-keyed computed accessors (#281) and methods (#755) register in the .cctor, keyed by + // this class's generated name (mirrors the class-declaration path #266/#647). bool hasSymbolAccessors = _classes.SymbolAccessors.ContainsKey(typeBuilder.Name); + bool hasSymbolMethods = _classes.SymbolMethods.ContainsKey(typeBuilder.Name); - if (!hasStaticFields && !hasStaticInitializers && !hasSymbolAccessors) return; + if (!hasStaticFields && !hasStaticInitializers && !hasSymbolAccessors && !hasSymbolMethods) return; var cctor = typeBuilder.DefineConstructor( MethodAttributes.Static | MethodAttributes.Private | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, @@ -599,9 +620,10 @@ private void EmitClassExpressionStaticConstructor(Expr.ClassExpr classExpr, Type } } - // Register symbol-keyed computed accessors (#281) in the runtime registry, - // keyed by this class's Type so dynamic bracket get/set can dispatch them. + // Register symbol-keyed computed accessors (#281) and methods (#755) in the runtime registry, + // keyed by this class's Type so dynamic bracket get / for...of dispatch can find them. EmitSymbolAccessorRegistrations(emitter, il, typeBuilder); + EmitSymbolMethodRegistrations(emitter, il, typeBuilder); il.Emit(OpCodes.Ret); } @@ -806,6 +828,14 @@ private void EmitClassExpressionMethod( // args with the `undefined` sentinel on the value-call path (covers sync + async). MarkPadsUndefined(methodBuilder); + // Generator methods route through the same state-machine emitters as class declarations (#765). + // Async generator FIRST since `async *m()` has both IsAsync and IsGenerator set. + if (method.IsAsync && method.IsGenerator) + { + EmitAsyncGeneratorMethodBody(methodBuilder, method, fieldsField); + return; + } + // Handle async methods via state machine if (method.IsAsync) { @@ -813,6 +843,13 @@ private void EmitClassExpressionMethod( return; } + // Generator methods use the generator state machine (#765). + if (method.IsGenerator) + { + EmitGeneratorMethodBody(methodBuilder, method, fieldsField); + return; + } + var il = methodBuilder.GetILGenerator(); var ctx = CreateClassExpressionContext(il, classExpr, typeBuilder, fieldsField); ctx.IsInstanceMethod = true; @@ -864,12 +901,26 @@ private void EmitClassExpressionStaticMethodBody(Expr.ClassExpr classExpr, Stmt. var typeBuilder = _classExprs.Builders[classExpr]; + // Static generator methods route like a free function (no `this`), mirroring the class- + // declaration static path (#765/#778). Async generator FIRST since it has both flags set. + if (method.IsAsync && method.IsGenerator) + { + EmitAsyncGeneratorMethodBody(methodBuilder, method, fieldsField: null, isInstanceMethod: false); + return; + } + if (method.IsAsync) { EmitClassExpressionStaticAsyncMethod(classExpr, typeBuilder, method, methodBuilder); return; } + if (method.IsGenerator) + { + EmitGeneratorMethodBody(methodBuilder, method, fieldsField: null, isInstanceMethod: false); + return; + } + var il = methodBuilder.GetILGenerator(); var ctx = CreateClassExpressionContext(il, classExpr, typeBuilder, null); ctx.IsInstanceMethod = false; @@ -955,6 +1006,77 @@ private void EmitClassExpressionSymbolAccessors( } } + /// + /// Pre-defines a uniquely-named .NET method for each computed symbol-keyed method of a class + /// EXPRESSION (*[Symbol.iterator]() {…} and the async/generator forms), mirroring the + /// class-declaration (#755). Recorded in the shared + /// registry (keyed by the generated type name) so the + /// bodies emit through the normal class-expression per-method emitters and the .cctor registers + /// them in the runtime symbol-method registry. + /// + private void DefineClassExpressionSymbolMethods(Expr.ClassExpr classExpr, TypeBuilder typeBuilder) + { + string className = typeBuilder.Name; + if (_classes.SymbolMethods.ContainsKey(className)) + return; // already defined (idempotent across multi-module pre-define/emit passes) + + var computed = classExpr.Methods.Where(m => m.ComputedKey != null && m.Body != null).ToList(); + if (computed.Count == 0) + return; + + var list = new List<(Stmt.Function, Expr, MethodBuilder)>(); + for (int i = 0; i < computed.Count; i++) + { + var method = computed[i]; + // Unique, deterministic name so multiple computed methods don't collide and the synthetic + // `` lexeme (not a dispatchable name) is replaced by a real IL name. + string uniqueName = $"$symmethod_{i}"; + var renamed = method with { Name = new Token(TokenType.IDENTIFIER, uniqueName, null, method.Name.Line) }; + + // Class-expression methods use all-object parameter slots (computed iterator methods are + // typically parameterless anyway). Async generator FIRST since it sets both flags. + var paramTypes = method.Parameters.Select(_ => typeof(object)).ToArray(); + Type returnType = (method.IsAsync && method.IsGenerator) ? _types.IAsyncEnumerableOfObject : + method.IsAsync ? _types.TaskOfObject : + method.IsGenerator ? _types.IEnumerableOfObject : + typeof(object); + + // Non-virtual (like symbol accessors): the registry holds the exact MethodInfo. + MethodAttributes attrs = MethodAttributes.Public | MethodAttributes.HideBySig; + if (method.IsStatic) + attrs |= MethodAttributes.Static; + + var mb = typeBuilder.DefineMethod(uniqueName, attrs, returnType, paramTypes); + + // Register under the unique name so EmitClassExpression(Static)MethodBody resolves the builder. + if (method.IsStatic) + _classExprs.StaticMethods[classExpr][uniqueName] = mb; + else + _classExprs.InstanceMethods[classExpr][uniqueName] = mb; + + list.Add((renamed, method.ComputedKey!, mb)); + } + _classes.SymbolMethods[className] = list; + } + + /// + /// Emits the bodies of the computed symbol-keyed methods recorded by + /// , reusing the class-expression per-method + /// emitters so the generator/async state machines compose (#755). + /// + private void EmitClassExpressionSymbolMethods(Expr.ClassExpr classExpr, TypeBuilder typeBuilder, FieldInfo fieldsField) + { + if (!_classes.SymbolMethods.TryGetValue(typeBuilder.Name, out var list)) + return; + foreach (var (method, _key, _builder) in list) + { + if (method.IsStatic) + EmitClassExpressionStaticMethodBody(classExpr, method); + else + EmitClassExpressionMethod(classExpr, typeBuilder, method, fieldsField); + } + } + private void EmitClassExpressionAccessor( Expr.ClassExpr classExpr, TypeBuilder typeBuilder, diff --git a/Compilation/ILCompiler.Classes.Static.cs b/Compilation/ILCompiler.Classes.Static.cs index 45421b26..8580552b 100644 --- a/Compilation/ILCompiler.Classes.Static.cs +++ b/Compilation/ILCompiler.Classes.Static.cs @@ -341,12 +341,20 @@ private void EmitStaticMethodBody(string className, Stmt.Function method) // covers sync, async, and generator (#692) static methods (same builder). MarkPadsUndefined(_classes.StaticMethods[className][method.Name.Lexeme]); + // Static async generator methods (#778) use the async generator state machine, set up like a + // free function (no `this`). Checked FIRST since a `static async *m()` has both IsAsync and + // IsGenerator set, so it must not fall into the sync-generator or plain-async branch below. + if (method.IsGenerator && method.IsAsync) + { + var agMethodBuilder = _classes.StaticMethods[className][method.Name.Lexeme]; + EmitAsyncGeneratorMethodBody(agMethodBuilder, method, fieldsField: null, isInstanceMethod: false); + return; + } + // Static generator methods (#692) use the generator state machine, set up like a free - // function (no `this`). Checked before the plain-async branch so `static async *m()` isn't - // mis-emitted as a non-generator async method (its full support is tracked separately). + // function (no `this`). if (method.IsGenerator && !method.IsAsync) { - var genTypeBuilder = _classes.Builders[className]; var genMethodBuilder = _classes.StaticMethods[className][method.Name.Lexeme]; EmitGeneratorMethodBody(genMethodBuilder, method, fieldsField: null, isInstanceMethod: false); return; diff --git a/Compilation/RuntimeEmitter.Objects.Index.cs b/Compilation/RuntimeEmitter.Objects.Index.cs index 8b9b9982..659a5b29 100644 --- a/Compilation/RuntimeEmitter.Objects.Index.cs +++ b/Compilation/RuntimeEmitter.Objects.Index.cs @@ -199,9 +199,12 @@ private void EmitGetIndex(TypeBuilder typeBuilder, EmittedRuntime runtime) il.MarkLabel(noSymGetterLabel); } - // #647: computed symbol-keyed method (`[Symbol.iterator]() {...}`). Reading the member - // returns the callable itself — the found MethodInfo, invoked later via InvokeMethodValue's - // MethodBase arm — unlike a getter (above), which is invoked here. + // #647/#755: computed symbol-keyed method (`[Symbol.iterator]() {...}`). Reading the member + // returns a receiver-bound callable — `new $TSFunction(obj, method)` — so a standalone + // `obj[Symbol.iterator]()` keeps `this`. InvokeWithThis uses the bound `_target` for an + // instance method (and null for a static one), mirroring the string-key method path + // (GetFieldsProperty). for...of / spread / for-await pass the receiver themselves, so they + // worked even when this returned the raw MethodInfo. { var noSymMethodLabel = il.DefineLabel(); var symMethodLocal = il.DeclareLocal(_types.Object); @@ -211,7 +214,10 @@ private void EmitGetIndex(TypeBuilder typeBuilder, EmittedRuntime runtime) il.Emit(OpCodes.Stloc, symMethodLocal); il.Emit(OpCodes.Ldloc, symMethodLocal); il.Emit(OpCodes.Brfalse, noSymMethodLabel); - il.Emit(OpCodes.Ldloc, symMethodLocal); + il.Emit(OpCodes.Ldarg_0); // obj — receiver to bind + il.Emit(OpCodes.Ldloc, symMethodLocal); // found MethodInfo (typed object) + il.Emit(OpCodes.Castclass, _types.MethodInfo); + il.Emit(OpCodes.Newobj, runtime.TSFunctionCtor); // new $TSFunction(obj, method) il.Emit(OpCodes.Ret); il.MarkLabel(noSymMethodLabel); } diff --git a/SharpTS.Tests/SharedTests/ClassExpressionTests.cs b/SharpTS.Tests/SharedTests/ClassExpressionTests.cs index e4d498b8..1abd4e1f 100644 --- a/SharpTS.Tests/SharedTests/ClassExpressionTests.cs +++ b/SharpTS.Tests/SharedTests/ClassExpressionTests.cs @@ -426,4 +426,89 @@ public void GenericClassExpression_ExplicitTypeArg(ExecutionMode mode) } #endregion + + #region Generator Methods (#765) + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ClassExpression_GeneratorMethod_Works(ExecutionMode mode) + { + // #765: a generator method in a class expression was emitted on the linear path and its + // `yield` hit "Yield not supported in this context". It now routes through the generator + // state machine like a class-declaration method. + var source = """ + const C = class { *gen() { yield 1; yield 2; } }; + console.log([...new C().gen()].join(",")); + """; + + Assert.Equal("1,2\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ClassExpression_GeneratorMethod_CapturesThis(ExecutionMode mode) + { + // Exercises `this`/constructor-parameter access and a loop inside a class-expression generator. + var source = """ + const Range = class { + constructor(public start: number, public end: number) {} + *gen(): Generator { for (let i = this.start; i < this.end; i++) yield i; } + }; + console.log([...new Range(1, 5).gen()].join(",")); + """; + + Assert.Equal("1,2,3,4\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ClassExpression_GeneratorMethod_YieldStar(ExecutionMode mode) + { + var source = """ + const C = class { *gen() { yield* [1, 2, 3]; } }; + console.log([...new C().gen()].join(",")); + """; + + Assert.Equal("1,2,3\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ClassExpression_StaticGeneratorMethod_Works(ExecutionMode mode) + { + var source = """ + const C = class { static *sg() { yield 7; yield 8; } }; + console.log([...C.sg()].join(",")); + """; + + Assert.Equal("7,8\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ClassExpression_AsyncGeneratorMethod_Works(ExecutionMode mode) + { + var source = """ + const C = class { async *ag() { yield 1; yield 2; } }; + async function main() { for await (const x of new C().ag()) console.log(x); } + main(); + """; + + Assert.Equal("1\n2\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ClassExpression_StaticAsyncGeneratorMethod_Works(ExecutionMode mode) + { + var source = """ + const C = class { static async *sag() { yield 5; yield 6; } }; + async function main() { for await (const x of C.sag()) console.log(x); } + main(); + """; + + Assert.Equal("5\n6\n", TestHarness.Run(source, mode)); + } + + #endregion } diff --git a/SharpTS.Tests/SharedTests/ComputedSymbolMethodTests.cs b/SharpTS.Tests/SharedTests/ComputedSymbolMethodTests.cs index 769bf26c..1e98a6ed 100644 --- a/SharpTS.Tests/SharedTests/ComputedSymbolMethodTests.cs +++ b/SharpTS.Tests/SharedTests/ComputedSymbolMethodTests.cs @@ -79,7 +79,7 @@ class Range { *[Symbol.iterator](): Iterator { yield 1; yield 2; yield 3 // Compiled mode: a non-symbol computed key (`[KEY]()` with KEY a string) would need a // dynamically-named .NET method; the symbol-method registry only backs symbol keys, and named - // access doesn't consult it. Interpreter-only; tracked as a follow-up. + // access doesn't consult it. Interpreter-only; tracked as a follow-up in #791. [Theory] [MemberData(nameof(ExecutionModes.InterpretedOnly), MemberType = typeof(ExecutionModes))] public void NonSymbolComputedMethodKey_FoldsToNamedMethod(ExecutionMode mode) @@ -140,12 +140,13 @@ class Derived extends Base {} Assert.Equal("2\n1\n2\n", TestHarness.Run(source, mode)); } - // Compiled mode: reading a symbol method as a value (`obj[Symbol.iterator]`) returns the raw - // MethodInfo rather than a receiver-bound callable, so a standalone `obj[Symbol.iterator]()` call - // loses `this`. for...of / spread / for-await (which pass the receiver themselves) are unaffected - // and run in both back ends. Tracked as a follow-up. + // Reading a symbol method as a value (`obj[Symbol.iterator]`) returns a receiver-bound callable in + // both back ends, so a standalone `obj[Symbol.iterator]()` call keeps `this` (#755 sub-case 1). In + // compiled mode the bracket-get wraps the method in `$TSFunction(obj, method)`, mirroring the + // string-key method path. for...of / spread / for-await (which pass the receiver themselves) were + // already unaffected. [Theory] - [MemberData(nameof(ExecutionModes.InterpretedOnly), MemberType = typeof(ExecutionModes))] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] public void DirectSymbolMethodAccess_ReturnsCallable(ExecutionMode mode) { var source = """ @@ -157,10 +158,11 @@ class R { *[Symbol.iterator]() { yield 5; } } Assert.Equal("5\n", TestHarness.Run(source, mode)); } - // Compiled mode: class *expressions* go through a separate emit path that doesn't yet wire - // computed symbol-keyed methods (class declarations do). Tracked as a follow-up. + // Class *expressions* now wire computed symbol-keyed methods through the same synthetic-method + + // registry path as class declarations, including the generator state machine (#755 sub-case 2, + // building on the class-expression generator routing in #765). [Theory] - [MemberData(nameof(ExecutionModes.InterpretedOnly), MemberType = typeof(ExecutionModes))] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] public void ClassExpression_ComputedSymbolMethod_Works(ExecutionMode mode) { var source = """ @@ -171,6 +173,24 @@ public void ClassExpression_ComputedSymbolMethod_Works(ExecutionMode mode) Assert.Equal("7\n8\n", TestHarness.Run(source, mode)); } + // Class expression carrying an async computed symbol method (`async *[Symbol.asyncIterator]()`), + // consumed with for-await — exercises the class-expression async-generator + symbol-registry + // paths together (#755 sub-case 2). + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void ClassExpression_AsyncComputedSymbolMethod_Works(ExecutionMode mode) + { + var source = """ + const A = class { async *[Symbol.asyncIterator]() { yield 10; yield 20; } }; + async function main() { + for await (const x of new A()) console.log(x); + } + main(); + """; + + Assert.Equal("10\n20\n", TestHarness.Run(source, mode)); + } + [Fact] public void PlainClass_StillNotIterable_ReportsError() { diff --git a/SharpTS.Tests/SharedTests/StaticGeneratorMethodTests.cs b/SharpTS.Tests/SharedTests/StaticGeneratorMethodTests.cs index 399911d3..3d1e6c84 100644 --- a/SharpTS.Tests/SharedTests/StaticGeneratorMethodTests.cs +++ b/SharpTS.Tests/SharedTests/StaticGeneratorMethodTests.cs @@ -4,9 +4,9 @@ namespace SharpTS.Tests.SharedTests; /// -/// Tests for static generator methods — `class C { static *gen() { yield … } }` (#692). The instance -/// form already compiled; this pins the static form, whose state machine is set up like a free -/// function (no `this`). Runs in both back ends. +/// Tests for static generator methods — `class C { static *gen() { yield … } }` (#692) and the async +/// form `static async *gen()` (#778). The instance forms already compiled; this pins the static forms, +/// whose state machines are set up like a free function (no `this`). Runs in both back ends. /// public class StaticGeneratorMethodTests { @@ -75,4 +75,56 @@ class C { *gen() { yield 7; } } Assert.Equal("7\n", TestHarness.Run(source, mode)); } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void StaticAsyncGenerator_ForAwait_Works(ExecutionMode mode) + { + // #778: a static async generator (static async *m()) was emitted as a plain async method, so + // its `yield` hit "Yield not supported in this context". It now routes through the async + // generator state machine with a static stub (no `this`), like a free async generator. + var source = """ + class C { static async *gen() { yield 1; yield 2; } } + async function main() { for await (const x of C.gen()) console.log(x); } + main(); + """; + + Assert.Equal("1\n2\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void StaticAsyncGenerator_ParameterAndAwait_Works(ExecutionMode mode) + { + // Reads a parameter (in a yield, not a for-condition — see the CompiledOnly test below) and + // suspends on a real await between yields. + var source = """ + class C { static async *gen(a: number) { yield a; await Promise.resolve(0); yield a * 2; } } + async function main() { for await (const x of C.gen(5)) console.log(x); } + main(); + """; + + Assert.Equal("5\n10\n", TestHarness.Run(source, mode)); + } + + [Theory] + [MemberData(nameof(ExecutionModes.All), MemberType = typeof(ExecutionModes))] + public void StaticAsyncGenerator_DefaultParamLoopAndAwait_Works(ExecutionMode mode) + { + // Exercises a default value-type parameter, a C-style for loop whose bound reads that + // parameter, and an await inside the loop — the full #778 shape (the generator + // default-parameter prologue from #737 applies for free). + var source = """ + class C { static async *gen(n: number = 3): AsyncGenerator { + for (let i = 0; i < n; i++) { await Promise.resolve(0); yield i * 10; } + } } + async function main() { + for await (const x of C.gen()) console.log(x); + for await (const x of C.gen(2)) console.log("g:" + x); + } + main(); + """; + + Assert.Equal("0\n10\n20\ng:0\ng:10\n", TestHarness.Run(source, mode)); + } }