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
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()
{