diff --git a/src/SharpTS/Compilation/AsyncGeneratorStateAnalyzer.Statements.cs b/src/SharpTS/Compilation/AsyncGeneratorStateAnalyzer.Statements.cs index eec530081..a8cb2150a 100644 --- a/src/SharpTS/Compilation/AsyncGeneratorStateAnalyzer.Statements.cs +++ b/src/SharpTS/Compilation/AsyncGeneratorStateAnalyzer.Statements.cs @@ -109,7 +109,21 @@ protected override void VisitTryCatch(Stmt.TryCatch stmt) // Don't traverse into nested declarations - they don't affect our analysis protected override void VisitFunction(Stmt.Function stmt) { } - protected override void VisitClass(Stmt.Class stmt) { } + protected override void VisitClass(Stmt.Class stmt) + { + string name = StorageName(stmt, stmt.Name.Lexeme); + _declaredVariables.Add(name); + if (!_seenSuspension) + _variablesDeclaredBeforeSuspension.Add(name); + foreach (var expression in ClassDefinitionExpressions.Enumerate(stmt)) + Visit(expression); + } + protected override void VisitClassExpr(Expr.ClassExpr expr) + { + foreach (var expression in ClassDefinitionExpressions.Enumerate(expr)) + Visit(expression); + } + protected override void VisitInterface(Stmt.Interface stmt) { } protected override void VisitTypeAlias(Stmt.TypeAlias stmt) { } protected override void VisitEnum(Stmt.Enum stmt) { } diff --git a/src/SharpTS/Compilation/AsyncStateAnalyzer.Statements.cs b/src/SharpTS/Compilation/AsyncStateAnalyzer.Statements.cs index 56867989c..03836bcb6 100644 --- a/src/SharpTS/Compilation/AsyncStateAnalyzer.Statements.cs +++ b/src/SharpTS/Compilation/AsyncStateAnalyzer.Statements.cs @@ -181,7 +181,20 @@ protected override void VisitTryCatch(Stmt.TryCatch stmt) // Don't traverse into nested declarations - they don't affect our analysis protected override void VisitFunction(Stmt.Function stmt) { } - protected override void VisitClass(Stmt.Class stmt) { } + protected override void VisitClass(Stmt.Class stmt) + { + // The class body has its own scope, but its binding belongs to this + // function and must survive a suspension when subsequently referenced. + _declaredVariables.Add(StorageName(stmt, stmt.Name.Lexeme)); + foreach (var expression in ClassDefinitionExpressions.Enumerate(stmt)) + Visit(expression); + } + protected override void VisitClassExpr(Expr.ClassExpr expr) + { + foreach (var expression in ClassDefinitionExpressions.Enumerate(expr)) + Visit(expression); + } + protected override void VisitInterface(Stmt.Interface stmt) { } protected override void VisitTypeAlias(Stmt.TypeAlias stmt) { } protected override void VisitEnum(Stmt.Enum stmt) { } diff --git a/src/SharpTS/Compilation/ClassDefinitionExpressions.cs b/src/SharpTS/Compilation/ClassDefinitionExpressions.cs new file mode 100644 index 000000000..81ccfe03d --- /dev/null +++ b/src/SharpTS/Compilation/ClassDefinitionExpressions.cs @@ -0,0 +1,31 @@ +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +/// Expressions evaluated in the enclosing scope when defining a class. +internal static class ClassDefinitionExpressions +{ + internal static IEnumerable Enumerate(Stmt.Class declaration) => + Enumerate(declaration.SuperclassExpr, declaration.Fields, declaration.Methods, declaration.Accessors); + + internal static IEnumerable Enumerate(Expr.ClassExpr expression) => + Enumerate(expression.SuperclassExpr, expression.Fields, expression.Methods, expression.Accessors); + + private static IEnumerable Enumerate(Expr? superclass, List fields, + List methods, List? accessors) + { + if (superclass != null) + yield return superclass; + foreach (var key in MemberKeys(fields, methods, accessors)) + yield return key; + } + + internal static IEnumerable MemberKeys(IEnumerable fields, + IEnumerable methods, IEnumerable? accessors) => + fields.Select(field => (field.Name.Start, field.ComputedKey)) + .Concat(methods.Select(method => (method.Name.Start, method.ComputedKey))) + .Concat((accessors ?? []).Select(accessor => (accessor.Name.Start, accessor.ComputedKey))) + .Where(member => member.ComputedKey != null) + .OrderBy(member => member.Start) + .Select(member => member.ComputedKey!); +} diff --git a/src/SharpTS/Compilation/ClosureAnalyzer.cs b/src/SharpTS/Compilation/ClosureAnalyzer.cs index 60a473159..b59f33ced 100644 --- a/src/SharpTS/Compilation/ClosureAnalyzer.cs +++ b/src/SharpTS/Compilation/ClosureAnalyzer.cs @@ -662,6 +662,8 @@ protected override void VisitFunction(Stmt.Function stmt) protected override void VisitClass(Stmt.Class stmt) { DeclareVariable(stmt.Name.Lexeme); + foreach (var expression in ClassDefinitionExpressions.Enumerate(stmt)) + Visit(expression); foreach (var method in stmt.Methods) { // Skip overload signatures (no body) @@ -672,6 +674,8 @@ protected override void VisitClass(Stmt.Class stmt) protected override void VisitClassExpr(Expr.ClassExpr expr) { + foreach (var expression in ClassDefinitionExpressions.Enumerate(expr)) + Visit(expression); // Class expressions don't declare the class name in the outer scope // (unlike class declarations), but we still need to analyze all bodies diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs index 0b3447fe2..129099014 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs @@ -1333,8 +1333,9 @@ protected static bool AnyContainsSuspension(IEnumerable exprs) Expr.Satisfies sa => ExprContainsSuspension(sa.Expression), Expr.NonNullAssertion nn => ExprContainsSuspension(nn.Expression), Expr.DynamicImport di => ExprContainsSuspension(di.PathExpression), - // Leaves (Literal/Variable/This/Super/ImportMeta/RegexLiteral) and lambda/class - // boundaries (ArrowFunction/ClassExpr) cannot surface a suspension to the current frame. + Expr.ClassExpr ce => AnyContainsSuspension(ClassDefinitionExpressions.Enumerate(ce)), + // Leaves and nested function bodies cannot suspend the current frame. + // Class heritage and computed names execute in the current frame. _ => false }; @@ -1365,6 +1366,7 @@ protected static bool AnyStmtContainsSuspension(IEnumerable statements) /// protected static bool StmtContainsSuspension(Stmt stmt) => stmt switch { + Stmt.Class c => AnyContainsSuspension(ClassDefinitionExpressions.Enumerate(c)), Stmt.Expression e => ExprContainsSuspension(e.Expr), Stmt.Var v => v.Initializer != null && ExprContainsSuspension(v.Initializer), Stmt.Const c => ExprContainsSuspension(c.Initializer), diff --git a/src/SharpTS/Compilation/GeneratorBlockScopeRenamer.cs b/src/SharpTS/Compilation/GeneratorBlockScopeRenamer.cs index 02c224f2c..297dc41f9 100644 --- a/src/SharpTS/Compilation/GeneratorBlockScopeRenamer.cs +++ b/src/SharpTS/Compilation/GeneratorBlockScopeRenamer.cs @@ -41,7 +41,7 @@ internal sealed record BlockScopeRenameResult( } /// -/// Computes per-binding storage names for block-scoped (let/const) declarations inside a +/// Computes per-binding storage names for block-scoped (let/const/class) declarations inside a /// suspension state-machine body that shadow an enclosing binding of the same name. /// /// @@ -73,7 +73,7 @@ internal sealed record BlockScopeRenameResult( /// /// Restrictions that keep the rewrite sound: /// -/// Only let/const (, with +/// Only class and let/const (, with /// IsVar == false) declarations are renamed. for/for-of/for-in/catch /// introduce scopes (so shadowing is detected accurately) but their loop-variable / catch-parameter /// bindings are left alone. @@ -391,8 +391,20 @@ protected override void VisitArrowFunction(Expr.ArrowFunction expr) // capture pivot does not cover, so a binding they read is already OFF-LIMITS (CaptureClassifier). // Their interiors contribute no renames or pivots; do not descend. protected override void VisitFunction(Stmt.Function stmt) { } - protected override void VisitClass(Stmt.Class stmt) { } - protected override void VisitClassExpr(Expr.ClassExpr expr) { } + protected override void VisitClass(Stmt.Class stmt) + { + if (_arrowDepth == 0) + DeclareBlockScoped(stmt, stmt.Name.Lexeme); + else + CurrentScope[stmt.Name.Lexeme] = stmt.Name.Lexeme; + foreach (var expression in ClassDefinitionExpressions.Enumerate(stmt)) + Visit(expression); + } + protected override void VisitClassExpr(Expr.ClassExpr expr) + { + foreach (var expression in ClassDefinitionExpressions.Enumerate(expr)) + Visit(expression); + } #endregion diff --git a/src/SharpTS/Compilation/GeneratorStateAnalyzer.cs b/src/SharpTS/Compilation/GeneratorStateAnalyzer.cs index e45fcfc78..d2fdb9461 100644 --- a/src/SharpTS/Compilation/GeneratorStateAnalyzer.cs +++ b/src/SharpTS/Compilation/GeneratorStateAnalyzer.cs @@ -275,40 +275,21 @@ protected override void VisitTryCatch(Stmt.TryCatch stmt) protected override void VisitFunction(Stmt.Function stmt) { } protected override void VisitClass(Stmt.Class stmt) { - _declaredVariables.Add(stmt.Name.Lexeme); + string name = StorageName(stmt, stmt.Name.Lexeme); + _declaredVariables.Add(name); if (!_seenYield) - _variablesDeclaredBeforeYield.Add(stmt.Name.Lexeme); + _variablesDeclaredBeforeYield.Add(name); // Class bodies are nested callable/initializer scopes, but heritage and // computed element names are evaluated eagerly in the enclosing generator. - if (stmt.SuperclassExpr != null) - Visit(stmt.SuperclassExpr); - foreach (var field in stmt.Fields) - if (field.ComputedKey != null) - Visit(field.ComputedKey); - foreach (var method in stmt.Methods) - if (method.ComputedKey != null) - Visit(method.ComputedKey); - if (stmt.Accessors != null) - foreach (var accessor in stmt.Accessors) - if (accessor.ComputedKey != null) - Visit(accessor.ComputedKey); + foreach (var expression in ClassDefinitionExpressions.Enumerate(stmt)) + Visit(expression); } protected override void VisitClassExpr(Expr.ClassExpr expr) { - if (expr.SuperclassExpr != null) - Visit(expr.SuperclassExpr); - foreach (var field in expr.Fields) - if (field.ComputedKey != null) - Visit(field.ComputedKey); - foreach (var method in expr.Methods) - if (method.ComputedKey != null) - Visit(method.ComputedKey); - if (expr.Accessors != null) - foreach (var accessor in expr.Accessors) - if (accessor.ComputedKey != null) - Visit(accessor.ComputedKey); + foreach (var expression in ClassDefinitionExpressions.Enumerate(expr)) + Visit(expression); } protected override void VisitInterface(Stmt.Interface stmt) { } protected override void VisitTypeAlias(Stmt.TypeAlias stmt) { } diff --git a/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs b/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs index 906d0e066..bbb12b209 100644 --- a/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs +++ b/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs @@ -591,6 +591,8 @@ private void CollectArrowsFromStmt(Stmt stmt) } break; case Stmt.Class c: + foreach (var expression in ClassDefinitionExpressions.Enumerate(c)) + CollectArrowsFromExpr(expression); var previousClassName = _currentCollectClassName; _currentCollectClassName = c.Name.Lexeme; foreach (var method in c.Methods) @@ -604,8 +606,6 @@ private void CollectArrowsFromStmt(Stmt stmt) // get MethodBuilders instead of EmitArrowFunction's null fallback. foreach (var field in c.Fields) { - if (field.ComputedKey != null) - CollectArrowsFromExpr(field.ComputedKey); if (field.Initializer != null) CollectArrowsFromExpr(field.Initializer); } @@ -1080,6 +1080,8 @@ or Stmt.Class CollectArrowsFromExpr(sp2.Value); break; case Expr.ClassExpr ce: + foreach (var expression in ClassDefinitionExpressions.Enumerate(ce)) + CollectArrowsFromExpr(expression); // Collect the class expression for later definition CollectClassExpression(ce); var previousClassNameCE = _currentCollectClassName; diff --git a/src/SharpTS/Compilation/ILCompiler.Async.cs b/src/SharpTS/Compilation/ILCompiler.Async.cs index 56760236a..d57a8a6b6 100644 --- a/src/SharpTS/Compilation/ILCompiler.Async.cs +++ b/src/SharpTS/Compilation/ILCompiler.Async.cs @@ -862,6 +862,16 @@ private void AnalyzeArrowStmtForAwaits(Stmt stmt, ref int awaitCount, ref bool s { switch (stmt) { + case Stmt.Class classStmt: + { + var name = ArrowStorageName(classStmt, classStmt.Name.Lexeme); + declaredVariables.Add(name); + if (!seenAwait) + declaredBeforeAwait.Add(name); + foreach (var expression in ClassDefinitionExpressions.Enumerate(classStmt)) + AnalyzeArrowExprForAwaits(expression, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); + break; + } case Stmt.Var v: { var name = ArrowStorageName(v, v.Name.Lexeme); // #766: per-binding storage name @@ -972,6 +982,10 @@ private void AnalyzeArrowExprForAwaits(Expr expr, ref int awaitCount, ref bool s { switch (expr) { + case Expr.ClassExpr classExpr: + foreach (var expression in ClassDefinitionExpressions.Enumerate(classExpr)) + AnalyzeArrowExprForAwaits(expression, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); + break; case Expr.Await a: awaitCount++; seenAwait = true; @@ -1009,6 +1023,10 @@ private void AnalyzeArrowExprForAwaits(Expr expr, ref int awaitCount, ref bool s case Expr.Grouping g: AnalyzeArrowExprForAwaits(g.Expression, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); break; + case Expr.Comma comma: + AnalyzeArrowExprForAwaits(comma.Left, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); + AnalyzeArrowExprForAwaits(comma.Right, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); + break; case Expr.Call c: AnalyzeArrowExprForAwaits(c.Callee, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); foreach (var arg in c.Arguments) @@ -1242,8 +1260,7 @@ private void EmitSuspensionFreePrimitiveAsyncFunctionBodies() coreParameters[index].ParameterType); } var coreEmitter = new SuspensionFreeAsyncCoreEmitter(coreContext); - foreach (Stmt statement in function.Body!) - coreEmitter.EmitStatement(statement); + coreEmitter.EmitStatements(function.Body!); MethodBuilder stubMethod = _functions.Builders[functionName]; ILGenerator il = stubMethod.GetILGenerator(); diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs index d8e2d1702..116fbad16 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs @@ -428,9 +428,13 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) // by this class's generated name. It is registered in the class- // expression .cctor and dispatched through the $Runtime symbol- // accessor registry, mirroring the class-declaration path (#266). - if (accessor.ComputedKey != null) + if (accessor.ComputedKey != null || accessor.IsStatic) { - DefineSymbolAccessorMethod(typeBuilder, accessor); + // Static accessors use the runtime constructor registry rather + // than the instance CLR property table, including literal names. + DefineSymbolAccessorMethod(typeBuilder, accessor.ComputedKey != null + ? accessor + : accessor with { ComputedKey = new Expr.Literal(accessor.Name.Lexeme) }); continue; } string accessorName = accessor.Name.Lexeme; @@ -461,6 +465,9 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) _classExprs.Setters[classExpr][pascalName] = methodBuilder; } } + + if (DefineDeferredComputedMethodKeyRegistrar(typeBuilder, classExpr.Fields) is { } deferred) + _classExprs.DeferredComputedKeys[classExpr] = deferred; } /// @@ -521,7 +528,7 @@ private void EmitClassExpressionBody(Expr.ClassExpr classExpr) { // Symbol-keyed accessors are emitted below from _classes.SymbolAccessors // (their synthetic methods aren't in _classExprs.Getters/Setters). - if (!accessor.IsAbstract && accessor.ComputedKey == null) + if (!accessor.IsAbstract && accessor.ComputedKey == null && !accessor.IsStatic) { EmitClassExpressionAccessor(classExpr, typeBuilder, accessor, fieldsField); } @@ -621,6 +628,15 @@ private void EmitClassExpressionStaticConstructor(Expr.ClassExpr classExpr, Type EmitClassPrototypeRegistration( il, typeBuilder, GetClassConstructorLength(classExpr.Methods)); + if (_classes.DeferredClassDefinitions.TryGetValue(typeBuilder.Name, out var deferredDefinition)) + { + il.Emit(OpCodes.Ret); + il = deferredDefinition.Initializer.GetILGenerator(); + ctx = CreateClassExpressionContext(il, classExpr, typeBuilder, null, deferredDefinition.Initializer); + ctx.IsStaticConstructorContext = true; + emitter = new ILEmitter(ctx); + } + // Process StaticInitializers if available (preserves declaration order) if (hasStaticInitializers) { @@ -628,6 +644,11 @@ private void EmitClassExpressionStaticConstructor(Expr.ClassExpr classExpr, Type { switch (initializer) { + case Stmt.Field field when field.IsStatic && field.ComputedKey != null: + _classes.ComputedFieldKeys.TryGetValue(field, out var computedKey); + EmitComputedStaticFieldInitializer(emitter, il, typeBuilder, field, computedKey); + break; + case Stmt.Field field when field.IsStatic && field.Initializer != null: var staticField = _classExprs.StaticFields[classExpr][field.Name.Lexeme]; emitter.EmitExpression(field.Initializer); @@ -819,8 +840,24 @@ private void EmitClassExpressionConstructor(Expr.ClassExpr classExpr, TypeBuilde void EmitInstanceFieldInitializers() { - foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && f.Initializer != null)) + foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsDeclare && (f.Initializer != null || f.ComputedKey != null))) { + if (field.ComputedKey != null) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldsfld, _classes.ComputedFieldKeys[field]); + if (field.Initializer != null) + { + emitter.EmitExpression(field.Initializer); + emitter.EmitBoxIfNeeded(field.Initializer); + } + else + il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + il.Emit(OpCodes.Call, _runtime.ObjectWrite.Index); + continue; + } + if (field.Initializer == null) + continue; string fieldName = field.Name.Lexeme; string pascalName = NamingConventions.ToPascalCase(fieldName); @@ -1177,7 +1214,7 @@ private void DefineClassExpressionSymbolMethods(Expr.ClassExpr classExpr, TypeBu // 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) }; + var renamed = method with { Name = new Token(TokenType.IDENTIFIER, uniqueName, null, method.Name.Line, method.Name.Start) }; // Class-expression methods use all-object parameter slots (computed iterator methods are // typically parameterless anyway). Async generator FIRST since it sets both flags. @@ -1203,8 +1240,6 @@ private void DefineClassExpressionSymbolMethods(Expr.ClassExpr classExpr, TypeBu list.Add((renamed, method.ComputedKey!, mb)); } _classes.SymbolMethods[className] = list; - if (DefineDeferredComputedMethodKeyRegistrar(typeBuilder) is { } deferred) - _classExprs.DeferredComputedKeys[classExpr] = deferred; } /// diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs index 1876a97bc..952cbf00a 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs @@ -276,9 +276,8 @@ private void EmitConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt, Fiel // Computed keys use dynamic SetIndex to support Symbol keys // Stack: this il.Emit(OpCodes.Ldarg_0); - // Evaluate computed key expression (e.g., the Symbol) - initEmitter.EmitExpression(field.ComputedKey); - initEmitter.EmitBoxIfNeeded(field.ComputedKey); + // Load the key captured when the class definition was evaluated. + il.Emit(OpCodes.Ldsfld, _classes.ComputedFieldKeys[field]); // Emit initializer value; a field with no initializer is still an own // property whose value is undefined. if (field.Initializer != null) diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs b/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs index 5d79dbccc..7038cb552 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs @@ -504,6 +504,9 @@ private void EmitGetPropertyBody( } } + if (classStmt.Accessors?.Any(accessor => accessor.ComputedKey != null && !accessor.IsStatic) == true) + EmitComputedInstanceGetterFallback(il); + // 3b. #791: non-symbol (string/number) computed method keys. Their bodies are emitted as // $symmethod_N and registered in the symbol-method registry under the property-key string // (RegisterSymbolMethod), but named/string-index access (obj.dyn, obj["dyn"]) funnels here @@ -866,6 +869,9 @@ private void EmitGetPropertyBodyForClassExpr( } } + if (classExpr.Accessors?.Any(accessor => accessor.ComputedKey != null && !accessor.IsStatic) == true) + EmitComputedInstanceGetterFallback(il); + // Computed non-symbol methods use synthetic CLR names and are // registered under their evaluated JavaScript property-key string. // Class declarations already consult this registry; class expressions @@ -1114,4 +1120,24 @@ private void EmitSetPropertyBodyForClassExpr( il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "set_Item", [_types.String, _types.Object])!); il.Emit(OpCodes.Ret); } + private void EmitComputedInstanceGetterFallback(ILGenerator il) + { + var missing = il.DefineLabel(); + var getter = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, _runtime.SymbolAccessors.FindGetter); + il.Emit(OpCodes.Stloc, getter); + il.Emit(OpCodes.Ldloc, getter); + il.Emit(OpCodes.Brfalse, missing); + il.Emit(OpCodes.Ldloc, getter); + il.Emit(OpCodes.Castclass, _types.MethodBase); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.MethodBase, "Invoke", _types.Object, _types.ObjectArray)); + il.Emit(OpCodes.Ret); + il.MarkLabel(missing); + } + } diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs index bb60a41ce..94b972d6d 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs @@ -402,6 +402,9 @@ private void DefineClassMethodsOnly(Stmt.Class classStmt) // Create PropertyBuilders for explicit accessors CreateExplicitAccessorProperties(typeBuilder, className); } + + if (DefineDeferredComputedMethodKeyRegistrar(typeBuilder, classStmt.Fields) is { } deferred) + _classes.DeferredComputedClassKeys[classStmt] = deferred; } private bool TryResolveTypedPrimitiveMethodCoreReturnType( @@ -938,7 +941,7 @@ private void DefineSymbolMethods(TypeBuilder typeBuilder, Stmt.Class classStmt, // Unique, deterministic name: multiple computed methods must not collide, and the synthetic // `` lexeme is not a dispatchable name. string uniqueName = $"$symmethod_{i}"; - var renamed = method with { Name = new Token(TokenType.IDENTIFIER, uniqueName, null, method.Name.Line) }; + var renamed = method with { Name = new Token(TokenType.IDENTIFIER, uniqueName, null, method.Name.Line, method.Name.Start) }; // Display-class analysis/registration ran against the original computed-method AST. // Body emission uses the renamed copy so it can resolve the synthetic MethodBuilder; @@ -989,8 +992,6 @@ private void DefineSymbolMethods(TypeBuilder typeBuilder, Stmt.Class classStmt, list.Add((renamed, method.ComputedKey!, mb)); } _classes.SymbolMethods[className] = list; - if (DefineDeferredComputedMethodKeyRegistrar(typeBuilder) is { } deferred) - _classes.DeferredComputedClassKeys[classStmt] = deferred; } /// diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs index eef47cae8..8290d9a61 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs @@ -123,6 +123,19 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt il.Emit(OpCodes.Stsfld, privateFieldStorage); } + if (_classes.DeferredClassDefinitions.TryGetValue(typeBuilder.Name, out var deferredDefinition)) + { + il.Emit(OpCodes.Ret); + il = deferredDefinition.Initializer.GetILGenerator(); + ctx = CreateModuleMemberContext(il, deferredDefinition.Initializer); + ctx.CurrentClassBuilder = typeBuilder; + ctx.EmittingTypeBuilder = typeBuilder; + ctx.CurrentClassName = qualifiedClassName; + ctx.IsStaticConstructorContext = true; + ApplyCapturedTopLevelVariableAccess(ctx); + emitter = new ILEmitter(ctx); + } + // Use StaticInitializers for proper declaration order if available if (hasStaticInitializers) { @@ -136,6 +149,12 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt switch (initializer) { case Stmt.Field field when field.IsStatic: + if (field.ComputedKey != null) + { + _classes.ComputedFieldKeys.TryGetValue(field, out var computedKey); + EmitComputedStaticFieldInitializer(emitter, il, typeBuilder, field, computedKey); + break; + } if (field.Initializer != null) { emitter.EmitExpression(field.Initializer); @@ -232,6 +251,8 @@ private void EmitSymbolAccessorRegistrations(ILEmitter emitter, ILGenerator il, foreach (var (accessor, method) in list) { + if (_classes.DeferredClassDefinitions.ContainsKey(typeBuilder.Name)) + continue; bool isGetter = accessor.Kind.Type == TokenType.GET; // owner: typeof(ThisClass) @@ -275,7 +296,7 @@ private void EmitSymbolMethodRegistrations(ILEmitter emitter, ILGenerator il, Ty foreach (var (method, key, builder) in list) { - if (ExpressionContainsYield(key)) + if (_classes.DeferredClassDefinitions.ContainsKey(typeBuilder.Name)) continue; // owner: typeof(ThisClass) il.Emit(OpCodes.Ldtoken, typeBuilder); @@ -295,17 +316,32 @@ private void EmitSymbolMethodRegistrations(ILEmitter emitter, ILGenerator il, Ty } } - private (MethodBuilder Method, IReadOnlyList Keys)? DefineDeferredComputedMethodKeyRegistrar(TypeBuilder typeBuilder) + private (MethodBuilder Method, IReadOnlyList Keys)? DefineDeferredComputedMethodKeyRegistrar(TypeBuilder typeBuilder, IReadOnlyList fields) { - if (!_classes.SymbolMethods.TryGetValue(typeBuilder.Name, out var methods)) + if (_classes.DeferredClassDefinitions.TryGetValue(typeBuilder.Name, out var existing)) + return (existing.Registrar, existing.Keys); + var deferred = new List<(Expr Key, MethodBuilder? Builder, bool IsStatic, bool? IsGetter, int Position, Stmt.Field? Field)>(); + if (_classes.SymbolMethods.TryGetValue(typeBuilder.Name, out var methods)) + foreach (var (method, key, builder) in methods) + deferred.Add((key, builder, method.IsStatic, null, method.Name.Start, null)); + if (_classes.SymbolAccessors.TryGetValue(typeBuilder.Name, out var accessors)) + foreach (var (accessor, builder) in accessors) + deferred.Add((accessor.ComputedKey!, builder, accessor.IsStatic, accessor.Kind.Type == TokenType.GET, accessor.Name.Start, null)); + foreach (var field in fields.Where(field => field.ComputedKey != null && !field.IsDeclare)) + deferred.Add((field.ComputedKey!, null, field.IsStatic, null, field.Name.Start, field)); + // Field names are evaluated with the definition, even when no key suspends. + if (!deferred.Any(entry => entry.Field != null || ExpressionContainsSuspension(entry.Key))) return null; - var deferred = methods.Where(entry => ExpressionContainsYield(entry.Key)).ToList(); - if (deferred.Count == 0) - return null; + deferred = deferred.OrderBy(entry => entry.Position).ToList(); + var initializer = typeBuilder.DefineMethod("$initializeDeferredClass", + MethodAttributes.Assembly | MethodAttributes.Static, _types.Void, Type.EmptyTypes); - var registrar = typeBuilder.DefineMethod( - "$registerDeferredComputedKeys", + // TypeScript type arguments do not create separate class definitions. Keep + // captured keys outside generic CLR instantiations so all instances share them. + var keyOwner = typeBuilder.IsGenericTypeDefinition ? _programType : typeBuilder; + var registrar = keyOwner.DefineMethod( + typeBuilder.IsGenericTypeDefinition ? $"$registerDeferredComputedKeys_{typeBuilder.Name}" : "$registerDeferredComputedKeys", MethodAttributes.Assembly | MethodAttributes.Static, _types.Void, [_types.ObjectArray]); @@ -314,32 +350,96 @@ private void EmitSymbolMethodRegistrations(ILEmitter emitter, ILGenerator il, Ty for (int i = 0; i < deferred.Count; i++) { - var (method, _key, builder) = deferred[i]; + var (_key, builder, isStatic, isGetter, _position, field) = deferred[i]; + if (field != null) + { + var keyField = keyOwner.DefineField($"$computedFieldKey_{typeBuilder.Name}_{i}", _types.Object, + FieldAttributes.Assembly | FieldAttributes.Static); + _classes.ComputedFieldKeys.Add(field, keyField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Stsfld, keyField); + continue; + } il.Emit(OpCodes.Ldtoken, typeBuilder); il.Emit(OpCodes.Call, getTypeFromHandle); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldc_I4, i); il.Emit(OpCodes.Ldelem_Ref); - EmitMethodInfoLiteral(il, builder, typeBuilder); - il.Emit(method.IsStatic ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Call, _runtime.SymbolAccessors.RegisterMethod); + if (isGetter == false) + il.Emit(OpCodes.Ldnull); + EmitMethodInfoLiteral(il, builder!, typeBuilder); + if (isGetter == true) + il.Emit(OpCodes.Ldnull); + il.Emit(isStatic ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Call, isGetter.HasValue + ? _runtime.SymbolAccessors.RegisterAccessor + : _runtime.SymbolAccessors.RegisterMethod); } + il.Emit(OpCodes.Call, typeBuilder.IsGenericTypeDefinition + ? EmitterTypeHelpers.ResolveMethod( + EmitGenerics.MakeGenericType(typeBuilder, typeBuilder.GetGenericArguments().Select(_ => _types.Object).ToArray()), initializer) + : initializer); il.Emit(OpCodes.Ret); - return (registrar, deferred.Select(entry => entry.Key).ToArray()); + var keys = deferred.Select(entry => entry.Key).ToArray(); + _classes.DeferredClassDefinitions.Add(typeBuilder.Name, (initializer, registrar, keys)); + return (registrar, keys); } - private static bool ExpressionContainsYield(Expr expression) + private void EmitComputedStaticFieldInitializer(ILEmitter emitter, ILGenerator il, + TypeBuilder owner, Stmt.Field field, FieldBuilder? key) { - var visitor = new YieldPresenceVisitor(); + il.Emit(OpCodes.Ldtoken, owner); + il.Emit(OpCodes.Call, _types.TypeGetTypeFromHandle); + if (key != null) + il.Emit(OpCodes.Ldsfld, key); + else + { + emitter.EmitExpression(field.ComputedKey!); + emitter.EmitBoxIfNeeded(field.ComputedKey!); + } + if (field.Initializer != null) + { + emitter.EmitExpression(field.Initializer); + emitter.EmitBoxIfNeeded(field.Initializer); + } + else + il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + il.Emit(OpCodes.Call, _runtime.ObjectWrite.Index); + } + + private static bool ExpressionContainsSuspension(Expr expression) + { + var visitor = new SuspensionPresenceVisitor(); visitor.Visit(expression); return visitor.Found; } - private sealed class YieldPresenceVisitor : Parsing.Visitors.AstVisitorBase + private sealed class SuspensionPresenceVisitor : Parsing.Visitors.AstVisitorBase { public bool Found { get; private set; } + protected override void VisitArrowFunction(Expr.ArrowFunction expr) { } + protected override void VisitFunction(Stmt.Function stmt) { } + protected override void VisitClass(Stmt.Class stmt) + { + foreach (var expression in ClassDefinitionExpressions.Enumerate(stmt)) + Visit(expression); + } + protected override void VisitClassExpr(Expr.ClassExpr expr) + { + foreach (var expression in ClassDefinitionExpressions.Enumerate(expr)) + Visit(expression); + } + + protected override void VisitAwait(Expr.Await expr) + { + Found = true; + ShouldContinue = false; + } + protected override void VisitYield(Expr.Yield expr) { Found = true; diff --git a/src/SharpTS/Compilation/ILCompiler.CommonJs.cs b/src/SharpTS/Compilation/ILCompiler.CommonJs.cs index 103c13db8..9c096d50b 100644 --- a/src/SharpTS/Compilation/ILCompiler.CommonJs.cs +++ b/src/SharpTS/Compilation/ILCompiler.CommonJs.cs @@ -215,8 +215,8 @@ private void EmitCommonJsModuleInit(ParsedModule module) foreach (var stmt in module.Statements) { - // Skip class/function/interface/type-alias/enum declarations — compiled separately. - if (stmt is Stmt.Class or Stmt.Function or Stmt.Interface or Stmt.TypeAlias or Stmt.Enum) + // Class definitions execute here; other declarations are compiled separately. + if (stmt is Stmt.Function or Stmt.Interface or Stmt.TypeAlias or Stmt.Enum) { continue; } diff --git a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs index 99c08f8a0..8e9500368 100644 --- a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs +++ b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs @@ -268,6 +268,8 @@ private CompilationContext CreateNestedAsyncArrowContext(ILGenerator il, Compila ClassExprBuilders = parentCtx.ClassExprBuilders, ClassExprCaptureFields = parentCtx.ClassExprCaptureFields, BlockScopedClassBuilders = parentCtx.BlockScopedClassBuilders, + DeferredComputedClassKeys = parentCtx.DeferredComputedClassKeys, + DeferredComputedClassExprKeys = parentCtx.DeferredComputedClassExprKeys, IsStrictMode = parentCtx.IsStrictMode, // ES2022 Private Class Elements support - inherit from parent context CurrentClassName = parentCtx.CurrentClassName, diff --git a/src/SharpTS/Compilation/ILCompiler.Modules.cs b/src/SharpTS/Compilation/ILCompiler.Modules.cs index 84a783bb9..1b26cbb48 100644 --- a/src/SharpTS/Compilation/ILCompiler.Modules.cs +++ b/src/SharpTS/Compilation/ILCompiler.Modules.cs @@ -694,9 +694,9 @@ private void EmitModuleInit(ParsedModule module) foreach (var stmt in module.Statements) { - // Skip class, function, interface, type alias, and enum declarations - // (they are compiled separately in earlier phases) - if (stmt is Stmt.Class or Stmt.Function or Stmt.Interface or Stmt.TypeAlias or Stmt.Enum) + // Class definitions still execute at their source position. Other + // declarations are compiled separately in earlier phases. + if (stmt is Stmt.Function or Stmt.Interface or Stmt.TypeAlias or Stmt.Enum) { continue; } @@ -773,9 +773,9 @@ private void EmitScriptInit(ParsedModule script) foreach (var stmt in script.Statements) { - // Skip class, function, interface, type alias, and enum declarations - // (they are compiled separately in earlier phases) - if (stmt is Stmt.Class or Stmt.Function or Stmt.Interface or Stmt.TypeAlias or Stmt.Enum) + // Class definitions still execute at their source position. Other + // declarations are compiled separately in earlier phases. + if (stmt is Stmt.Function or Stmt.Interface or Stmt.TypeAlias or Stmt.Enum) { continue; } @@ -1195,8 +1195,9 @@ private void DefineHostedModuleRunner(ParsedModule module) string? defaultBinding, string? exportAssignmentBinding) => statement switch { - // Declarations are defined in the compiler's normal declaration phases. - Stmt.Class or Stmt.Function or Stmt.Interface or Stmt.TypeAlias or Stmt.Enum or + // Class declarations retain their executable definition work. + // Other declarations are defined in the normal declaration phases. + Stmt.Function or Stmt.Interface or Stmt.TypeAlias or Stmt.Enum or Stmt.Namespace or Stmt.DeclareModule => null, // Module imports are emitted in a synchronous prelude using the normal diff --git a/src/SharpTS/Compilation/ILCompiler.State.cs b/src/SharpTS/Compilation/ILCompiler.State.cs index d24ae435b..38a709a0b 100644 --- a/src/SharpTS/Compilation/ILCompiler.State.cs +++ b/src/SharpTS/Compilation/ILCompiler.State.cs @@ -75,6 +75,8 @@ private sealed class ClassCompilationState // RegisterSymbolMethod call. public Dictionary> SymbolMethods { get; } = []; public Dictionary Keys)> DeferredComputedClassKeys { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary Keys)> DeferredClassDefinitions { get; } = []; + public Dictionary ComputedFieldKeys { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary InstanceFieldsField { get; } = []; public HashSet CompactStorageClasses { get; } = new(ReferenceEqualityComparer.Instance); diff --git a/src/SharpTS/Compilation/ILEmitter.Expressions.cs b/src/SharpTS/Compilation/ILEmitter.Expressions.cs index bb6452233..bda7808e4 100644 --- a/src/SharpTS/Compilation/ILEmitter.Expressions.cs +++ b/src/SharpTS/Compilation/ILEmitter.Expressions.cs @@ -1129,6 +1129,8 @@ protected override void EmitClassExpression(Expr.ClassExpr ce) IL.Emit(OpCodes.Ldtoken, typeBuilder); IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); IL.Emit(OpCodes.Call, _ctx.Runtime!.ClassInitialization.RunDefinition); + if (_ctx.DeferredComputedClassExprKeys?.TryGetValue(ce, out var deferred) == true) + EmitDeferredComputedKeys(deferred.Method, deferred.Keys); // Load the Type object using ldtoken + GetTypeFromHandle IL.Emit(OpCodes.Ldtoken, typeBuilder); diff --git a/src/SharpTS/Compilation/ILEmitter.Statements.cs b/src/SharpTS/Compilation/ILEmitter.Statements.cs index 30ccf6afc..c51d0af9a 100644 --- a/src/SharpTS/Compilation/ILEmitter.Statements.cs +++ b/src/SharpTS/Compilation/ILEmitter.Statements.cs @@ -3286,7 +3286,8 @@ private void EmitBlockScopedClassDeclaration(Stmt.Class classStmt) if (!isBlockScoped) { string qualifiedName = _ctx.GetQualifiedClassName(classStmt.Name.Lexeme); - if (!_ctx.Classes.TryGetValue(qualifiedName, out var topLevelBuilder)) + if (!_ctx.Classes.TryGetValue(qualifiedName, out var topLevelBuilder) + && !_ctx.Classes.TryGetValue(_ctx.ResolveClassName(classStmt.Name.Lexeme), out topLevelBuilder)) return; builder = topLevelBuilder; } @@ -3306,6 +3307,8 @@ private void EmitBlockScopedClassDeclaration(Stmt.Class classStmt) IL.Emit(OpCodes.Call, _ctx.Types.GetMethod( _ctx.Types.Type, "GetTypeFromHandle", _ctx.Types.RuntimeTypeHandle)); IL.Emit(OpCodes.Call, _ctx.Runtime!.ClassInitialization.RunDefinition); + if (_ctx.DeferredComputedClassKeys?.TryGetValue(classStmt, out var deferred) == true) + EmitDeferredComputedKeys(deferred.Method, deferred.Keys); // Top-level classes are lexical declarations, so they may also be present // in BlockScopedClassBuilders. Regardless of that implementation detail, diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs index 77590ab0b..2a17b1198 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs @@ -2175,6 +2175,26 @@ void EmitErrorTypeBranch(Type ctorType, MethodBuilder populate, FieldBuilder pro il.Emit(OpCodes.Ret); il.MarkLabel(noTypePdsLabel); + // Computed static accessors also use synthetic CLR names. Resolve + // their evaluated string keys after own descriptors, just as the + // symbol-index path resolves the same accessor registry. + var noComputedStaticGetterLabel = il.DefineLabel(); + var computedStaticGetterLocal = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, inputs.SymbolAccessors.FindGetter); + il.Emit(OpCodes.Stloc, computedStaticGetterLocal); + il.Emit(OpCodes.Ldloc, computedStaticGetterLocal); + il.Emit(OpCodes.Brfalse, noComputedStaticGetterLabel); + il.Emit(OpCodes.Ldloc, computedStaticGetterLocal); + il.Emit(OpCodes.Castclass, _types.MethodBase); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.MethodBase, "Invoke", _types.Object, _types.ObjectArray)); + il.Emit(OpCodes.Ret); + il.MarkLabel(noComputedStaticGetterLabel); + // Computed static methods are emitted under synthetic CLR names // and registered by their evaluated JavaScript property key. They // therefore cannot be found by the ordinary name-based reflection diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs index 17e53bcaf..e97237e6d 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs @@ -63,6 +63,7 @@ private readonly record struct SetPropertyInputs( bool ProxySelected, EmittedReflectAssignment? ReflectAssignment, EmittedRegExpRuntime RegExps, + EmittedSymbolAccessorRuntime SymbolAccessors, MethodBuilder TSFunctionInvokeWithThis, TypeBuilder TSFunctionType, Type UndefinedType @@ -1201,6 +1202,33 @@ void EmitTypeSetSkipName(string n) il.Emit(OpCodes.Ret); il.MarkLabel(newTypeDescriptorLabel); + var noComputedSetter = il.DefineLabel(); + var computedSetter = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, inputs.SymbolAccessors.FindSetter); + il.Emit(OpCodes.Stloc, computedSetter); + il.Emit(OpCodes.Ldloc, computedSetter); + il.Emit(OpCodes.Brfalse, noComputedSetter); + il.Emit(OpCodes.Ldloc, computedSetter); + il.Emit(OpCodes.Castclass, _types.MethodBase); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.MethodBase, "Invoke", _types.Object, _types.ObjectArray)); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ret); + il.MarkLabel(noComputedSetter); + // A registered getter without a setter rejects ordinary assignment, + // just like other getter-only properties; do not create a shadow. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, inputs.SymbolAccessors.FindGetter); + il.Emit(OpCodes.Brtrue, typeSetSkipLabel); var newTypeDescriptorLocal = il.DeclareLocal(inputs.DescriptorStorage.DescriptorType); var newTypeEnumerableLocal = il.DeclareLocal(_types.Boolean); il.Emit(OpCodes.Ldc_I4_1); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs index 0636fbbc1..d2cd2ffe9 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs @@ -1340,6 +1340,7 @@ void EmitLinkProto(FieldBuilder child) _features.UsesProxy, runtime.Reflect.Assignment, runtime.RegExps, + runtime.SymbolAccessors, runtime.FunctionValues.InvokeWithThis, runtime.FunctionValues.Type, runtime.Sentinels.UndefinedType diff --git a/src/SharpTS/Compilation/RuntimeFeatureDetector.cs b/src/SharpTS/Compilation/RuntimeFeatureDetector.cs index b6569a6c6..8c331ef62 100644 --- a/src/SharpTS/Compilation/RuntimeFeatureDetector.cs +++ b/src/SharpTS/Compilation/RuntimeFeatureDetector.cs @@ -766,7 +766,8 @@ private void VisitStmt(Stmt? stmt) break; case Stmt.Class cls: - if (cls.SuperclassExpr is not null) VisitExpr(cls.SuperclassExpr); + foreach (var expression in ClassDefinitionExpressions.Enumerate(cls)) + VisitExpr(expression); foreach (var m in cls.Methods) { // Class methods can be `async *foo()` — async generators. @@ -1694,7 +1695,8 @@ prop.Key is not Expr.ComputedKey && VisitExpr(nn.Expression); break; case Expr.ClassExpr ce: - if (ce.SuperclassExpr is not null) VisitExpr(ce.SuperclassExpr); + foreach (var expression in ClassDefinitionExpressions.Enumerate(ce)) + VisitExpr(expression); foreach (var m in ce.Methods) { if (m.IsAsync) diff --git a/src/SharpTS/Compilation/StateMachineExitRoutingEmitter.Variables.cs b/src/SharpTS/Compilation/StateMachineExitRoutingEmitter.Variables.cs index 2eec431d1..5ca7165e2 100644 --- a/src/SharpTS/Compilation/StateMachineExitRoutingEmitter.Variables.cs +++ b/src/SharpTS/Compilation/StateMachineExitRoutingEmitter.Variables.cs @@ -106,6 +106,10 @@ protected void EmitDefaultParameters(List parameters) /// protected abstract IReadOnlyDictionary BlockScopeRenames { get; } + protected override string GetClassStorageName(Stmt.Class classStmt) => + BlockScopeRenames.TryGetValue(classStmt, out var renamed) + ? renamed : classStmt.Name.Lexeme; + /// Shared empty map for analyses built without the renamer. protected static readonly IReadOnlyDictionary NoRenames = new Dictionary(); diff --git a/src/SharpTS/Compilation/StatementEmitterBase.cs b/src/SharpTS/Compilation/StatementEmitterBase.cs index 1238f15f3..11629fca0 100644 --- a/src/SharpTS/Compilation/StatementEmitterBase.cs +++ b/src/SharpTS/Compilation/StatementEmitterBase.cs @@ -1813,13 +1813,20 @@ protected virtual void EmitStoreLoopVariable(LocalBuilder? local, string name, A #region Class Expressions + /// + /// Selects the storage name for a class binding. Overrides may select a + /// suspension-safe binding slot, but must preserve the original declaration + /// node used to look up its class builder. + /// + protected virtual string GetClassStorageName(Stmt.Class classStmt) => classStmt.Name.Lexeme; + private void EmitStateMachineClassDeclaration(Stmt.Class classStmt) { TypeBuilder? builder = null; if (Ctx.BlockScopedClassBuilders?.TryGetValue(classStmt, out var scopedBuilder) == true) builder = scopedBuilder; - else - Ctx.Classes.TryGetValue(Ctx.GetQualifiedClassName(classStmt.Name.Lexeme), out builder); + else if (!Ctx.Classes.TryGetValue(Ctx.GetQualifiedClassName(classStmt.Name.Lexeme), out builder)) + Ctx.Classes.TryGetValue(Ctx.ResolveClassName(classStmt.Name.Lexeme), out builder); if (builder == null) return; @@ -1831,7 +1838,8 @@ private void EmitStateMachineClassDeclaration(Stmt.Class classStmt) if (Ctx.DeferredComputedClassKeys?.TryGetValue(classStmt, out var deferred) == true) EmitDeferredComputedKeys(deferred.Method, deferred.Keys); - var field = GetHoistedVariableField(classStmt.Name.Lexeme); + string storageName = GetClassStorageName(classStmt); + var field = GetHoistedVariableField(storageName); if (field != null) { IL.Emit(OpCodes.Ldarg_0); @@ -1841,20 +1849,32 @@ private void EmitStateMachineClassDeclaration(Stmt.Class classStmt) return; } - var local = Ctx.Locals.GetLocal(classStmt.Name.Lexeme) - ?? Ctx.Locals.DeclareLocal(classStmt.Name.Lexeme, Types.Object, classStmt); + var local = Ctx.Locals.GetLocal(storageName) + ?? Ctx.Locals.DeclareLocal(storageName, Types.Object, classStmt); IL.Emit(OpCodes.Ldtoken, builder); IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); IL.Emit(OpCodes.Stloc, local); } - private void EmitDeferredComputedKeys(MethodBuilder method, IReadOnlyList keys) + /// + /// Evaluates class keys in order, preserving Symbol keys and coercing all + /// other keys through Ctx.Runtime.StringCoercion.ToJsString. Spills each + /// resulting value before evaluating the next key, then passes the collected + /// values to as an object array. + /// + protected void EmitDeferredComputedKeys(MethodBuilder method, IReadOnlyList keys) { var values = new List(keys.Count); foreach (var key in keys) { EmitExpression(key); EnsureBoxed(); + var isSymbol = IL.DefineLabel(); + IL.Emit(OpCodes.Dup); + IL.Emit(OpCodes.Isinst, Ctx.Runtime!.Symbols.Type); + IL.Emit(OpCodes.Brtrue, isSymbol); + IL.Emit(OpCodes.Call, Ctx.Runtime.StringCoercion.ToJsString); + IL.MarkLabel(isSymbol); values.Add(_helpers.SpillStoreObject()); } diff --git a/src/SharpTS/Execution/Interpreter.Dispatch.cs b/src/SharpTS/Execution/Interpreter.Dispatch.cs index 21b704b0e..802750f9e 100644 --- a/src/SharpTS/Execution/Interpreter.Dispatch.cs +++ b/src/SharpTS/Execution/Interpreter.Dispatch.cs @@ -189,6 +189,7 @@ public partial class Interpreter /// internal ValueTask DispatchStmtAsync(Stmt stmt) => stmt switch { + Stmt.Class s => VisitClassCore(_asyncContext, s), Stmt.Block s => ExecuteBlockAsyncVT(s), Stmt.Sequence s => ExecuteSequenceAsyncVT(s), Stmt.Expression s => ExecuteExpressionAsyncVT(s), diff --git a/src/SharpTS/Execution/Interpreter.Expressions.cs b/src/SharpTS/Execution/Interpreter.Expressions.cs index d243e0c6c..12e5c6556 100644 --- a/src/SharpTS/Execution/Interpreter.Expressions.cs +++ b/src/SharpTS/Execution/Interpreter.Expressions.cs @@ -181,7 +181,7 @@ internal async ValueTask VisitDestructuringAssignAsync(Expr.Destru internal ValueTask VisitImportMetaAsync(Expr.ImportMeta im) => new(EvaluateImportMeta(im)); internal ValueTask VisitYieldAsync(Expr.Yield yieldExpr) => new(EvaluateYieldAsync(yieldExpr)); internal ValueTask VisitRegexLiteralAsync(Expr.RegexLiteral regex) => new(RuntimeValue.FromObject(new SharpTSRegExp(regex.Pattern, regex.Flags))); - internal ValueTask VisitClassExprAsync(Expr.ClassExpr classExpr) => new(EvaluateClassExpression(classExpr)); + internal ValueTask VisitClassExprAsync(Expr.ClassExpr classExpr) => EvaluateClassExpressionCore(_asyncContext, classExpr); /// /// Evaluates a yield expression, throwing YieldException for control flow. @@ -2040,7 +2040,10 @@ private RuntimeValue EvaluateImportMeta(Expr.ImportMeta im) /// Evaluates a class expression and returns the SharpTSClass object. /// Unlike class declarations, the class is not added to the environment. /// - private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) + private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) => + EvaluateClassExpressionCore(_syncContext, classExpr).GetAwaiter().GetResult(); + + private async ValueTask EvaluateClassExpressionCore(IEvaluationContext ctx, Expr.ClassExpr classExpr) { // Generate name for anonymous classes string className = classExpr.Name?.Lexeme ?? $"$ClassExpr_{++_classExprCounter}"; @@ -2049,7 +2052,7 @@ private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) object? superclass = null; if (classExpr.SuperclassExpr != null) { - superclass = Evaluate(classExpr.SuperclassExpr); + superclass = (await ctx.EvaluateExprAsync(classExpr.SuperclassExpr)).ToObject(); // `extends Array` (#233): substitute the SharpTSArrayClass bridge, // mirroring VisitClass. if (superclass is SharpTSArrayGlobal) @@ -2099,6 +2102,15 @@ private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) bool hasStaticInitializers = classExpr.StaticInitializers != null && classExpr.StaticInitializers.Count > 0; // Process fields + // Evaluate every computed key exactly once, in source order, before + // applying static values or symbol-keyed methods and accessors. + var computedMemberKeys = new Dictionary(System.Collections.Generic.ReferenceEqualityComparer.Instance); + foreach (var key in Compilation.ClassDefinitionExpressions.MemberKeys(classExpr.Fields, classExpr.Methods, classExpr.Accessors)) + { + object? value = (await ctx.EvaluateExprAsync(key)).ToObject(); + computedMemberKeys[key] = value is SharpTSSymbol ? value : PropertyKeyConverter.ToPropertyKeyString(value); + } + foreach (Stmt.Field field in classExpr.Fields) { if (field.IsStatic) @@ -2115,7 +2127,8 @@ private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) } else { - instanceFields.Add(field); + instanceFields.Add(field.ComputedKey == null ? field + : field with { ComputedKey = new Expr.Literal(computedMemberKeys[field.ComputedKey]) }); } } @@ -2140,7 +2153,7 @@ private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) // table; other keys fold to a string-named method (parallels the class-declaration path). if (method.ComputedKey != null) { - object? key = Evaluate(method.ComputedKey); + object? key = computedMemberKeys[method.ComputedKey]; if (key is SharpTSSymbol symbolKey) { (symbolMethods ??= []).Add((symbolKey, func, method.IsStatic)); @@ -2164,6 +2177,8 @@ private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) // Create accessor functions Dictionary getters = []; Dictionary setters = []; + Dictionary staticGetters = []; + Dictionary staticSetters = []; List<(SharpTSSymbol Symbol, SharpTSFunction Func, bool IsStatic, bool IsGetter)>? symbolAccessors = null; if (classExpr.Accessors != null) @@ -2186,7 +2201,7 @@ private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) // (mirrors VisitClass). if (accessor.ComputedKey != null) { - object? key = Evaluate(accessor.ComputedKey); + object? key = computedMemberKeys[accessor.ComputedKey]; if (key is SharpTSSymbol symbolKey) { (symbolAccessors ??= []).Add((symbolKey, func, accessor.IsStatic, isGetter)); @@ -2197,11 +2212,11 @@ private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) if (isGetter) { - getters[nameKey] = func; + (accessor.IsStatic ? staticGetters : getters)[nameKey] = func; } else { - setters[nameKey] = func; + (accessor.IsStatic ? staticSetters : setters)[nameKey] = func; } } } @@ -2218,7 +2233,9 @@ private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) getters, setters, classExpr.IsAbstract, - instanceFields) + instanceFields, + staticGetters: staticGetters.Count > 0 ? staticGetters : null, + staticSetters: staticSetters.Count > 0 ? staticSetters : null) : superclass is SharpTSArrayClass arraySuper ? new SharpTSArrayClass( className, @@ -2229,7 +2246,9 @@ private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) getters, setters, classExpr.IsAbstract, - instanceFields) + instanceFields, + staticGetters: staticGetters.Count > 0 ? staticGetters : null, + staticSetters: staticSetters.Count > 0 ? staticSetters : null) : superclass is SharpTSPromiseClass promiseSuper ? new SharpTSPromiseClass( className, @@ -2240,7 +2259,9 @@ private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) getters, setters, classExpr.IsAbstract, - instanceFields) + instanceFields, + staticGetters: staticGetters.Count > 0 ? staticGetters : null, + staticSetters: staticSetters.Count > 0 ? staticSetters : null) : new SharpTSClass( className, (SharpTSClass?)superclass, @@ -2250,7 +2271,9 @@ private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) getters, setters, classExpr.IsAbstract, - instanceFields); + instanceFields, + staticGetters: staticGetters.Count > 0 ? staticGetters : null, + staticSetters: staticSetters.Count > 0 ? staticSetters : null); if (symbolAccessors != null) { @@ -2289,8 +2312,12 @@ private RuntimeValue EvaluateClassExpression(Expr.ClassExpr classExpr) case Stmt.Field field when field.IsStatic: object? fieldValue = field.Initializer != null ? Evaluate(field.Initializer) - : null; - klass.SetStaticProperty(field.Name.Lexeme, fieldValue); + : field.ComputedKey != null ? SharpTSUndefined.Instance : null; + if (field.ComputedKey != null && computedMemberKeys[field.ComputedKey] is SharpTSSymbol symbol) + klass.SetStaticBySymbol(symbol, fieldValue); + else + klass.SetStaticProperty(field.ComputedKey != null + ? (string)computedMemberKeys[field.ComputedKey]! : field.Name.Lexeme, fieldValue); break; case Stmt.StaticBlock block: diff --git a/src/SharpTS/Execution/Interpreter.Statements.cs b/src/SharpTS/Execution/Interpreter.Statements.cs index 6437b599a..931411d35 100644 --- a/src/SharpTS/Execution/Interpreter.Statements.cs +++ b/src/SharpTS/Execution/Interpreter.Statements.cs @@ -1986,7 +1986,10 @@ internal ExecutionResult VisitFunction(Stmt.Function functionStmt) return ExecutionResult.Success(); } - internal ExecutionResult VisitClass(Stmt.Class classStmt) + internal ExecutionResult VisitClass(Stmt.Class classStmt) => + VisitClassCore(_syncContext, classStmt).GetAwaiter().GetResult(); + + private async ValueTask VisitClassCore(IEvaluationContext ctx, Stmt.Class classStmt) { // @DotNetType declare class: bind a DotNet wrapper into the environment // instead of creating an empty SharpTSClass. Non-DotNet declare classes still @@ -1999,7 +2002,7 @@ internal ExecutionResult VisitClass(Stmt.Class classStmt) object? superclass = null; if (classStmt.SuperclassExpr != null) { - superclass = Evaluate(classStmt.SuperclassExpr); + superclass = (await ctx.EvaluateExprAsync(classStmt.SuperclassExpr)).ToObject(); // `extends Array` (#233): the Array global is a constructor // singleton, not a SharpTSClass — substitute the SharpTSArrayClass @@ -2054,6 +2057,15 @@ internal ExecutionResult VisitClass(Stmt.Class classStmt) // so they'll be added with null/undefined values and can be set externally later. bool hasStaticInitializers = classStmt.StaticInitializers != null && classStmt.StaticInitializers.Count > 0; + // Evaluate every computed key exactly once, in source order, before + // applying static values or symbol-keyed methods and accessors. + var computedMemberKeys = new Dictionary(System.Collections.Generic.ReferenceEqualityComparer.Instance); + foreach (var key in Compilation.ClassDefinitionExpressions.MemberKeys(classStmt.Fields, classStmt.Methods, classStmt.Accessors)) + { + object? value = (await ctx.EvaluateExprAsync(key)).ToObject(); + computedMemberKeys[key] = value is SharpTSSymbol ? value : PropertyKeyConverter.ToPropertyKeyString(value); + } + foreach (Stmt.Field field in classStmt.Fields) { if (field.IsPrivate) @@ -2092,7 +2104,8 @@ internal ExecutionResult VisitClass(Stmt.Class classStmt) else { // Collect instance fields - they'll be initialized when instances are created - instanceFields.Add(field); + instanceFields.Add(field.ComputedKey == null ? field + : field with { ComputedKey = new Expr.Literal(computedMemberKeys[field.ComputedKey]) }); } } @@ -2119,7 +2132,7 @@ internal ExecutionResult VisitClass(Stmt.Class classStmt) // in the symbol-method table; other keys fold to a string-named method. if (method.ComputedKey != null) { - object? key = Evaluate(method.ComputedKey); + object? key = computedMemberKeys[method.ComputedKey]; if (key is SharpTSSymbol symbolKey) { (symbolMethods ??= []).Add((symbolKey, func, method.IsStatic)); @@ -2183,7 +2196,7 @@ internal ExecutionResult VisitClass(Stmt.Class classStmt) // class-definition time, like computed field keys. if (accessor.ComputedKey != null) { - object? key = Evaluate(accessor.ComputedKey); + object? key = computedMemberKeys[accessor.ComputedKey]; if (key is SharpTSSymbol symbolKey) { (symbolAccessors ??= []).Add((symbolKey, func, accessor.IsStatic, isGetter)); @@ -2351,11 +2364,14 @@ internal ExecutionResult VisitClass(Stmt.Class classStmt) case Stmt.Field field when field.IsStatic: object? fieldValue = field.Initializer != null ? Evaluate(field.Initializer) - : null; + : field.ComputedKey != null ? SharpTSUndefined.Instance : null; if (field.IsPrivate) klass.SetStaticPrivateField(field.Name.Lexeme, fieldValue); + else if (field.ComputedKey != null && computedMemberKeys[field.ComputedKey] is SharpTSSymbol symbol) + klass.SetStaticBySymbol(symbol, fieldValue); else - klass.SetStaticProperty(field.Name.Lexeme, fieldValue); + klass.SetStaticProperty(field.ComputedKey != null + ? (string)computedMemberKeys[field.ComputedKey]! : field.Name.Lexeme, fieldValue); break; case Stmt.StaticBlock block: diff --git a/src/SharpTS/Parsing/Parser.Classes.cs b/src/SharpTS/Parsing/Parser.Classes.cs index ba271a6ca..2e7a70543 100644 --- a/src/SharpTS/Parsing/Parser.Classes.cs +++ b/src/SharpTS/Parsing/Parser.Classes.cs @@ -550,7 +550,7 @@ private void ParseComputedClassMember( Consume(TokenType.RIGHT_BRACKET, "Expect ']' after computed property name."); // Synthetic name token for error reporting; the real key lives in ComputedKey. - Token syntheticName = new Token(TokenType.IDENTIFIER, "", null, Previous().Line); + Token syntheticName = new Token(TokenType.IDENTIFIER, "", null, Previous().Line, Previous().Start); // A '(' or '<' after ']' marks a method ([Symbol.iterator]() {} / generic form); // anything else is a computed-name field ([expr]: T = v). diff --git a/src/SharpTS/Parsing/Parser.cs b/src/SharpTS/Parsing/Parser.cs index 4082fb773..3236a50ee 100644 --- a/src/SharpTS/Parsing/Parser.cs +++ b/src/SharpTS/Parsing/Parser.cs @@ -443,7 +443,7 @@ private Token ConsumePropertyNameOrLiteral(string message) return (new Token(TokenType.IDENTIFIER, d.ToString(System.Globalization.CultureInfo.InvariantCulture), null, Previous().Line), null); } - return (new Token(TokenType.IDENTIFIER, "", null, Previous().Line), computedKey); + return (new Token(TokenType.IDENTIFIER, "", null, Previous().Line, Previous().Start), computedKey); } return (ConsumePropertyNameOrLiteral("Expect property name after 'get'/'set'."), null); } diff --git a/src/SharpTS/Runtime/Types/SharpTSClass.cs b/src/SharpTS/Runtime/Types/SharpTSClass.cs index 6b0e3bcb1..961774bd6 100644 --- a/src/SharpTS/Runtime/Types/SharpTSClass.cs +++ b/src/SharpTS/Runtime/Types/SharpTSClass.cs @@ -184,7 +184,7 @@ protected void InitializeInstanceFields(Interpreter interpreter, SharpTSInstance { object? value = field.Initializer != null ? interpreter.Evaluate(field.Initializer) - : null; + : field.ComputedKey != null ? SharpTSUndefined.Instance : null; // Check if this is a computed property name if (field.ComputedKey != null) diff --git a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs index 77550b606..2694067bf 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs @@ -1147,7 +1147,10 @@ private bool CheckClassAccessorBodies( // Promise, Array). Create a placeholder class so super() // calls and constructor validation type-check correctly // (accept any number of args). - var leafName = Expr.GetSuperclassLeafName(classStmt.SuperclassExpr)!; + // A dynamic heritage expression (for example an awaited promise) + // has no lexical leaf name. Its permissive placeholder must not + // masquerade as a named built-in or dereference a missing name. + var leafName = Expr.GetSuperclassLeafName(classStmt.SuperclassExpr) ?? ""; var placeholder = new TypeInfo.MutableClass(leafName); var permissiveConstructor = new TypeInfo.Function( [TypeInfo.Any.Shared], TypeInfo.Void.Shared, RequiredParams: 0, HasRestParam: true); diff --git a/tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs b/tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs index 5a68eed37..6e5f99bea 100644 --- a/tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs +++ b/tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs @@ -47,6 +47,7 @@ public class EmitterSyncTests // storage; these overrides retoken the operator node so the read/write land on the // shadow's slot. Byte-identical across all four state machines, so they live here. --- "EmitConstDeclaration", // Route a shadowing const declaration to its own slot + "GetClassStorageName", // Use analyzed per-binding class storage across suspension without changing the AST node used for class-builder lookup "EmitCompoundAssign", // Route a shadowing compound assignment to its own slot "EmitLogicalAssign", // Route a shadowing logical assignment to its own slot "EmitPrefixIncrement", // Route a shadowing prefix ++/-- to its own slot diff --git a/tests/SharpTS.Tests/CompilerTests/AsyncLocalClassDeclarationTests.cs b/tests/SharpTS.Tests/CompilerTests/AsyncLocalClassDeclarationTests.cs new file mode 100644 index 000000000..af46acf4c --- /dev/null +++ b/tests/SharpTS.Tests/CompilerTests/AsyncLocalClassDeclarationTests.cs @@ -0,0 +1,442 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.CompilerTests; + +public sealed class AsyncLocalClassDeclarationTests +{ + [Theory, ModeData] + public void GenericComputedFieldKeys_AreSharedAcrossTypeArguments(ExecutionMode mode) + { + const string source = """ + let counter = 0; + class Box { [counter++] = 5; } + console.log(counter); + const numberBox: any = new Box(); + const stringBox: any = new Box(); + console.log(counter, numberBox[0], stringBox[0], stringBox[1]); + """; + Assert.Equal("1\n1 5 5 undefined\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void ComputedFieldKeysWithoutAwait_AreCapturedAtDefinition(ExecutionMode mode) + { + const string source = """ + async function run() { + let counter = 0; + const C = class { [counter++] = 5; }; + console.log(counter); + const a: any = new C(); + const b: any = new C(); + console.log(counter, a[0], b[0], b[1]); + class D { [counter++] = 7; } + console.log(counter); + const d: any = new D(); + const e: any = new D(); + console.log(counter, d[1], e[1], e[2]); + } + run().then(() => {}, e => console.log("rejected", e.message)); + """; + Assert.Equal("1\n1 5 5 undefined\n2\n2 7 7 undefined\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void ClassExpressionComputedStaticAccessors_StayOnConstructor(ExecutionMode mode) + { + const string source = """ + async function run() { + const C = class { + static get [await new Promise(r => setTimeout(() => r("value"), 1))]() { return 5; } + static set ["value"](v) { console.log("static", v); } + get ["value"]() { return 7; } + }; + const cls: any = C; + const instance: any = new C(); + console.log(cls.value, instance.value); + cls.value = 9; + } + run().then(() => {}, e => console.log("rejected", e.message)); + """; + Assert.Equal("5 7\nstatic 9\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void ComputedFieldsWithoutSuspension_UseRuntimeKeys(ExecutionMode mode) + { + const string source = """ + const key = "value"; + class Declaration { static [key] = 5; } + const Expression = class { static [key] = 7; [key] = 9; }; + const d: any = Declaration; + const e: any = Expression; + const instance: any = new Expression(); + console.log(d.value, e.value, instance.value); + """; + Assert.Equal("5 7 9\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void ComputedFieldsWithoutInitializers_StoreUndefined(ExecutionMode mode) + { + const string source = """ + const key = "empty"; + class Declaration { static [key]; } + const Expression = class { static [key]; [key]; }; + const d: any = Declaration; + const e: any = Expression; + const instance: any = new Expression(); + console.log(d.empty, e.empty, instance.empty); + """; + Assert.Equal("undefined undefined undefined\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AwaitedStaticGetterOnlyKey_AssignmentPreservesGetter(ExecutionMode mode) + { + const string source = """ + async function run() { + class C { static get [await new Promise(r => setTimeout(() => r("value"), 1))]() { return 5; } } + const cls: any = C; + cls.value = 9; + console.log(cls.value); + } + run().then(() => {}, e => console.log("rejected", e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AwaitedInstanceGetterKey_IsReadableByName(ExecutionMode mode) + { + const string source = """ + async function run() { + class C { get [await new Promise(r => setTimeout(() => r("value"), 1))]() { return 5; } } + const c: any = new C(); + console.log(c.value); + } + run().then(() => {}, e => console.log("rejected", e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AwaitedStaticSetterKey_IsWritableByName(ExecutionMode mode) + { + const string source = """ + async function run() { + class C { static set [await new Promise(r => setTimeout(() => r("value"), 1))](v) { console.log(v); } } + const cls: any = C; + cls.value = 5; + } + run().then(() => {}, e => console.log("rejected", e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void RejectedFieldKey_IsCaughtWithoutRunningStaticInitializer(ExecutionMode mode) + { + const string source = """ + async function run() { + try { + class C { + static [await new Promise((r, j) => setTimeout(() => j(new Error("key")), 1))] = console.log("bad"); + } + } catch (e) { console.log(e.message); } + class D { static value = 5; } + console.log(D.value); + } + run().then(() => {}, e => console.log("outer", e.message)); + """; + Assert.Equal("key\n5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AwaitedHeritageExpression_PreservesSideEffectsAndBaseMembers(ExecutionMode mode) + { + const string source = """ + class B { static value = 5; } + async function run() { + class C extends (await new Promise(r => setTimeout(() => { console.log("base"); r(0); }, 1)), B) {} + const cls: any = C; + return cls.value; + } + run().then(v => console.log(v), e => console.log("rejected", e.message)); + """; + Assert.Equal("base\n5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AsyncArrowClassExpression_AwaitsHeritageBeforeConstruction(ExecutionMode mode) + { + const string source = """ + class B { value() { return 5; } } + const run = async () => { + const C = class extends (await new Promise(r => setTimeout(() => { console.log("base"); r(0); }, 1)), B) {}; + const c: any = new C(); + return c.value(); + }; + run().then(v => console.log(v), e => console.log("rejected", e.message)); + """; + Assert.Equal("base\n5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AwaitedStaticFieldKey_IsEvaluatedAtDefinition(ExecutionMode mode) + { + const string source = """ + async function run() { + class C { static [await new Promise(r => setTimeout(() => r("value"), 1))] = 5; } + const cls: any = C; + return cls.value; + } + run().then(v => console.log(v), e => console.log("rejected", e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AwaitedInstanceFieldKey_IsEvaluatedOnceBeforeConstruction(ExecutionMode mode) + { + const string source = """ + async function run() { + let calls = 0; + class C { + [await new Promise(r => setTimeout(() => { calls++; r("value"); }, 1))] = 5; + } + const a: any = new C(); + const b: any = new C(); + console.log(calls, a.value, b.value); + } + run().then(() => {}, e => console.log("rejected", e.message)); + """; + Assert.Equal("1 5 5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AwaitedClassExpressionFieldKey_PreservesSymbolIdentity(ExecutionMode mode) + { + const string source = """ + async function run() { + const key = Symbol("key"); + const C = class { static [await new Promise(r => setTimeout(() => r(key), 1))] = 5; }; + const cls: any = C; + console.log(cls[key]); + } + run().then(() => {}, e => console.log("rejected", e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AwaitedAccessorAndMethodKeys_PreserveSourceOrder(ExecutionMode mode) + { + const string source = """ + async function run() { + class C { + static get [await new Promise(r => setTimeout(() => { console.log("first"); r("a"); }, 1))]() { return 2; } + static [await new Promise(r => setTimeout(() => { console.log("second"); r("b"); }, 1))]() { return 3; } + } + const cls: any = C; + return cls.a + cls.b(); + } + run().then(v => console.log(v), e => console.log("rejected", e.message)); + """; + Assert.Equal("first\nsecond\n5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AwaitedAccessorKey_IsEvaluatedInAsyncFunction(ExecutionMode mode) + { + const string source = """ + async function run() { + class C { static get [await new Promise(r => setTimeout(() => r("value"), 1))]() { return 5; } } + const cls: any = C; + return cls.value; + } + run().then(v => console.log(v), e => console.log("rejected", e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AwaitedMethodKey_IsEvaluatedInAsyncGenerator(ExecutionMode mode) + { + const string source = """ + async function* run() { + class C { static [await new Promise(r => setTimeout(() => r("value"), 1))]() { return 5; } } + const cls: any = C; + yield cls.value(); + } + run().next().then(v => console.log(v.value), e => console.log("rejected", e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void MultipleAwaitedMethodKeys_PreserveEarlierKeys(ExecutionMode mode) + { + const string source = """ + async function run() { + class C { + static [await new Promise(r => setTimeout(() => r("a"), 1))]() { return 2; } + static [await new Promise(r => setTimeout(() => r("b"), 1))]() { return 3; } + } + const cls: any = C; + return cls.a() + cls.b(); + } + run().then(v => console.log(v), e => console.log("rejected", e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AwaitedMethodKey_IsEvaluatedInAsyncFunction(ExecutionMode mode) + { + const string source = """ + async function run() { + class C { static [await new Promise(r => setTimeout(() => r("value"), 1))]() { return 5; } } + const cls: any = C; + return cls.value(); + } + run().then(v => console.log(v), e => console.log("rejected", e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void AwaitedClassExpressionMethodKey_IsEvaluatedInAsyncArrow(ExecutionMode mode) + { + const string source = """ + const run = async () => { + const C = class { static [await new Promise(r => setTimeout(() => r("value"), 1))]() { return 5; } }; + const cls: any = C; + return cls.value(); + }; + run().then(v => console.log(v), e => console.log("rejected", e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void ArrowDeclarationBeforeSuspension_PreservesClassBinding(ExecutionMode mode) + { + const string source = """ + const run = async () => { + class C { static value = 5; } + await new Promise(r => setTimeout(r, 1)); + return C.value; + }; + run().then(v => console.log(v), e => console.log("rejected", e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void ShadowedAsyncClasses_KeepSeparateBindings(ExecutionMode mode) + { + const string source = """ + async function run() { + class C { static value = 5; } + { + class C { static value = 7; } + await new Promise(r => setTimeout(r, 1)); + console.log(C.value); + } + console.log(C.value); + } + run(); + """; + Assert.Equal("7\n5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void ShadowedGeneratorClasses_KeepSeparateBindings(ExecutionMode mode) + { + const string source = """ + function* run() { + class C { static value = 5; } + { + class C { static value = 7; } + yield 0; + console.log(C.value); + } + console.log(C.value); + } + const iterator = run(); + iterator.next(); + iterator.next(); + """; + Assert.Equal("7\n5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void ShadowedAsyncGeneratorClasses_KeepSeparateBindings(ExecutionMode mode) + { + const string source = """ + async function* run() { + class C { static value = 5; } + { + class C { static value = 7; } + yield 0; + console.log(C.value); + } + console.log(C.value); + } + const iterator = run(); + iterator.next().then(() => iterator.next()); + """; + Assert.Equal("7\n5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void DeclarationBeforeSuspension_PreservesClassBinding(ExecutionMode mode) + { + const string source = """ + async function run(){class C{static value=5;}await new Promise(r=>setTimeout(r,1));return C.value;} + run().then(v=>console.log(v),e=>console.log("rejected",e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void DeclarationAfterSuspension_ResolvesClassBinding(ExecutionMode mode) + { + const string source = """ + async function run(){await new Promise(r=>setTimeout(r,1));class C{static value=5;}return C.value;} + run().then(v=>console.log(v),e=>console.log("rejected",e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void DeclarationAfterIntrinsicAwait_ResolvesLocalClass(ExecutionMode mode) + { + const string source = """ + async function run(){await Promise.resolve(0);class C{static value=5;}return C.value;} + run().then(v=>console.log(v)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void DeclarationAfterIntrinsicAwait_DoesNotReject(ExecutionMode mode) + { + const string source = """ + async function run(){await Promise.resolve(0);class C{static value=5;}return C.value;} + run().then(v=>console.log(v),e=>console.log("rejected",e.message)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void DeclarationBeforeIntrinsicAwait_ResolvesLocalClass(ExecutionMode mode) + { + const string source = """ + async function run(){class C{static value=5;}const value=C.value;await Promise.resolve(0);return value;} + run().then(v=>console.log(v)); + """; + Assert.Equal("5\n", TestHarness.Run(source, mode)); + } +} diff --git a/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.cs b/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.cs index 68e8a2eb6..e342501a8 100644 --- a/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.cs +++ b/tests/SharpTS.Tests/CompilerTests/StandaloneDllTests.cs @@ -18,6 +18,33 @@ namespace SharpTS.Tests.CompilerTests; /// public class StandaloneDllTests { + [Theory] + [InlineData("main.ts")] + [InlineData("main.cts")] + [InlineData("main.mts")] + public void Isolated_ComputedClassFieldKeys_AreCapturedAtDefinition(string entryPoint) + { + using var tempDir = IntegrationTests.CliTestHelper.CreateTempDirectory(); + tempDir.CreateFile(entryPoint, """ + let counter = 0; + class Box { [counter++] = 5; } + const key = "value"; + class Static { static [key] = 7; } + console.log(counter); + const a: any = new Box(); + const b: any = new Box(); + const cls: any = Static; + console.log(counter, a[0], b[0], b[1], cls.value); + """); + var dllPath = tempDir.GetPath("computed_class_keys.dll"); + var compile = IntegrationTests.CliTestHelper.RunCli( + $"--no-tsconfig --compile \"{tempDir.GetPath(entryPoint)}\" -o \"{dllPath}\" --verify --standalone", tempDir.Path); + Assert.True(compile.ExitCode == 0, compile.StandardOutput + compile.StandardError); + Assert.DoesNotContain("SharpTS", GetAssemblyReferences(dllPath)); + Assert.Equal("1\n1 5 5 undefined 7\n", ExecuteCompiledDllIsolated(dllPath, timeoutMs: 15000, + verifyStandardError: error => Assert.Empty(error), standardInput: "")); + } + // Allowlist of files that contain intentional SharpTS late-binding patterns. // These use graceful fallback: emit tries emitted types first, falls back to // interpreter types via Type.GetType() if available. This allows compiled code diff --git a/tests/SharpTS.Tests/CompilerTests/SuspensionFreePrimitiveAsyncTests.cs b/tests/SharpTS.Tests/CompilerTests/SuspensionFreePrimitiveAsyncTests.cs index 792ec4bd5..cbee76c4a 100644 --- a/tests/SharpTS.Tests/CompilerTests/SuspensionFreePrimitiveAsyncTests.cs +++ b/tests/SharpTS.Tests/CompilerTests/SuspensionFreePrimitiveAsyncTests.cs @@ -473,6 +473,23 @@ async function run(value: number): Promise { Assert.Equal("12\n", TestHarness.RunCompiledStandalone(source)); } + [Fact] + public void LocalClassBinding_PreservesTypedCoreAndStandaloneOutput() + { + const string source = """ + async function run(){await Promise.resolve(0);class C{static value=5;}return C.value;} + run().then(v=>console.log(v)); + """; + Assembly assembly = Compile(source); + Assert.Contains(assembly.GetType("$Program")!.GetMethods( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static), + method => method.Name.Contains("$asyncCore$run", StringComparison.Ordinal)); + Assert.DoesNotContain(assembly.GetTypes(), type => + type.Name.Contains("d__", StringComparison.Ordinal)); + Assert.Empty(TestHarness.CompileAndVerifyOnly(source)); + Assert.Equal("5\n", TestHarness.RunCompiledStandalone(source)); + } + private static Assembly Compile(string source) { var statements = new Parser(new Lexer(source).ScanTokens()).ParseOrThrow(); diff --git a/tests/SharpTS.Tests/Hosting/HostedInterpreterRuntimeTests.cs b/tests/SharpTS.Tests/Hosting/HostedInterpreterRuntimeTests.cs index 0af4b98e4..fac4d8013 100644 --- a/tests/SharpTS.Tests/Hosting/HostedInterpreterRuntimeTests.cs +++ b/tests/SharpTS.Tests/Hosting/HostedInterpreterRuntimeTests.cs @@ -15,6 +15,30 @@ namespace SharpTS.Tests.Hosting; [Collection("ProcessLifecycleTests")] public sealed class HostedInterpreterRuntimeTests { + [Theory] + [InlineData("await Promise.resolve(0);class C{static value=5;}return C.value;")] + [InlineData("class C{static value=5;}await new Promise(r=>setTimeout(r,1));return C.value;")] + public void CompiledAsyncLocalClass_ResolvesInHostedRuntime(string body) + { + SharpTSProgram program = CreateProgram( + "async function run(){" + body + "}" + + "run().then(v=>console.log(v),e=>console.log('rejected',e.message));export {};"); + var compiler = new ILCompiler($"hosted_async_class_{Guid.NewGuid():N}"); + compiler.EnableHostedOutput(); + compiler.CompileModules(program.RuntimeModules.ToList(), program.Resolver, program.TypeMap); + var dispatcher = new DeterministicHostDispatcher(); + var errors = new RecordingErrorSink(); + using var output = Infrastructure.AsyncLocalConsoleRedirector.Capture(); + using ISharpTSHostedRuntime runtime = SharpTSHostedAssembly.CreateRuntime( + System.Reflection.Assembly.Load(compiler.SaveToBytes()), + dispatcher, new RecordingLifetime(), errors); + Task initialization = runtime.InitializeAsync(); + dispatcher.RunUntil(() => initialization.IsCompleted && output.GetOutput().Length > 0); + initialization.GetAwaiter().GetResult(); + Assert.Equal("5\n", output.GetOutput().Replace("\r\n", "\n")); + Assert.Empty(errors.Errors); + } + [Fact] public void SynchronousLoopsDoNotPumpHostedTimersOrMicrotasks() {