From 75854c38e61b07135ea05dcdeeefe7dcb20024c8 Mon Sep 17 00:00:00 2001 From: Nicholas Nassiri Date: Mon, 21 Sep 2026 03:15:02 -0700 Subject: [PATCH] Own deferred class definition metadata and validate emission --- .../CompilationContext.ClassExpressions.cs | 3 +- .../DeferredClassDefinitionRegistry.cs | 113 ++++++++++++++++++ .../ILCompiler.Classes.ClassExpressions.cs | 11 +- .../ILCompiler.Classes.Constructors.cs | 2 +- .../Compilation/ILCompiler.Classes.Methods.cs | 3 +- .../Compilation/ILCompiler.Classes.Static.cs | 28 +++-- .../ILCompiler.ContextFactories.cs | 6 +- src/SharpTS/Compilation/ILCompiler.State.cs | 5 +- src/SharpTS/Compilation/ILCompiler.cs | 2 + .../Compilation/ILEmitter.Expressions.cs | 4 +- .../Compilation/ILEmitter.Statements.cs | 4 +- .../Compilation/StatementEmitterBase.cs | 8 +- .../DeferredClassDefinitionRegistryTests.cs | 95 +++++++++++++++ 13 files changed, 247 insertions(+), 37 deletions(-) create mode 100644 src/SharpTS/Compilation/DeferredClassDefinitionRegistry.cs create mode 100644 tests/SharpTS.Tests/CompilerTests/DeferredClassDefinitionRegistryTests.cs diff --git a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs index 4ad612f52..3e772e310 100644 --- a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs +++ b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs @@ -34,6 +34,5 @@ public partial class CompilationContext // Variable name to class expression mapping (for static member access) public Dictionary? VarToClassExpr { get; set; } - public IReadOnlyDictionary Keys)>? DeferredComputedClassKeys { get; set; } - public IReadOnlyDictionary Keys)>? DeferredComputedClassExprKeys { get; set; } + public DeferredClassDefinitionRegistry? DeferredClassDefinitions { get; set; } } diff --git a/src/SharpTS/Compilation/DeferredClassDefinitionRegistry.cs b/src/SharpTS/Compilation/DeferredClassDefinitionRegistry.cs new file mode 100644 index 000000000..4d95e2159 --- /dev/null +++ b/src/SharpTS/Compilation/DeferredClassDefinitionRegistry.cs @@ -0,0 +1,113 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +/// +/// Owns deferred class initializers and captured computed keys for one compilation. +/// Declarations remain readable while bodies are emitted, including nested classes; +/// completion closes registration and checks every forward-declared initializer. +/// +public sealed class DeferredClassDefinitionRegistry +{ + private readonly Dictionary _byType = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary _bySource = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary _fieldKeys = new(ReferenceEqualityComparer.Instance); + private readonly HashSet _emitted = new(ReferenceEqualityComparer.Instance); + + public bool IsComplete { get; private set; } + + public bool TryGet(TypeBuilder owner, [NotNullWhen(true)] out DeferredClassDefinition? definition) + => _byType.TryGetValue(owner, out definition); + + public bool TryGet(object source, [NotNullWhen(true)] out DeferredClassDefinition? definition) + => _bySource.TryGetValue(source, out definition); + + internal FieldBuilder RequireFieldKey(Stmt.Field field) + => _fieldKeys.TryGetValue(field, out var key) ? key + : throw new InvalidOperationException("The computed field key has not been declared."); + + internal bool TryGetFieldKey(Stmt.Field field, [NotNullWhen(true)] out FieldBuilder? key) + => _fieldKeys.TryGetValue(field, out key); + + internal DeferredClassDefinition Declare(object source, TypeBuilder owner, MethodBuilder initializer, + MethodBuilder registrar, IEnumerable keys, IReadOnlyDictionary fieldKeys) + { + EnsureMutable(); + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(owner); + ArgumentNullException.ThrowIfNull(initializer); + ArgumentNullException.ThrowIfNull(registrar); + ArgumentNullException.ThrowIfNull(keys); + ArgumentNullException.ThrowIfNull(fieldKeys); + var snapshot = keys.ToArray(); + if (source is not (Stmt.Class or Expr.ClassExpr) + || _bySource.ContainsKey(source) || _byType.ContainsKey(owner) + || initializer.DeclaringType != owner || !initializer.IsStatic || !registrar.IsStatic + || (!owner.IsGenericTypeDefinition && registrar.DeclaringType != owner) + || registrar.Module != owner.Module || ReferenceEquals(initializer, registrar) + || snapshot.Length == 0 || snapshot.Any(key => key is null)) + throw new InvalidOperationException("Invalid or duplicate deferred class declaration."); + var sourceFields = source is Stmt.Class declaration ? declaration.Fields : ((Expr.ClassExpr)source).Fields; + var expected = sourceFields.Where(field => field.ComputedKey != null && !field.IsDeclare).ToArray(); + if (fieldKeys.Count != expected.Length || expected.Any(field => !fieldKeys.ContainsKey(field))) + throw new InvalidOperationException("Deferred class keys must include every computed field."); + foreach (var (field, key) in fieldKeys) + { + if (key is null || _fieldKeys.ContainsKey(field) || !key.IsStatic + || !expected.Any(expectedField => ReferenceEquals(expectedField, field)) + || key.DeclaringType != registrar.DeclaringType + || !snapshot.Any(expression => ReferenceEquals(expression, field.ComputedKey))) + throw new InvalidOperationException("Invalid or duplicate computed field key declaration."); + } + var definition = new DeferredClassDefinition(owner, initializer, registrar, Array.AsReadOnly(snapshot)); + _byType.Add(owner, definition); + _bySource.Add(source, definition); + foreach (var (field, key) in fieldKeys) + _fieldKeys.Add(field, key); + return definition; + } + + internal void MarkInitializerEmitted(DeferredClassDefinition definition) + { + EnsureMutable(); + ArgumentNullException.ThrowIfNull(definition); + if (!_byType.TryGetValue(definition.Owner, out var declared) || !ReferenceEquals(declared, definition) + || definition.Initializer.GetILGenerator().ILOffset == 0 + || definition.Registrar.GetILGenerator().ILOffset == 0 || !_emitted.Add(definition)) + throw new InvalidOperationException("The deferred class initializer is foreign, empty, or already emitted."); + } + + internal void CompleteEmission() + { + EnsureMutable(); + if (_emitted.Count != _byType.Count) + throw new InvalidOperationException("Not all deferred class initializers have been emitted."); + IsComplete = true; + } + + private void EnsureMutable() + { + if (IsComplete) + throw new InvalidOperationException("Deferred class metadata is complete."); + } +} + +/// Immutable forward references shared by declaration and expression emitters. +public sealed class DeferredClassDefinition +{ + internal DeferredClassDefinition(TypeBuilder owner, MethodBuilder initializer, MethodBuilder registrar, + IReadOnlyList keys) + { + Owner = owner; + Initializer = initializer; + Registrar = registrar; + Keys = keys; + } + + public TypeBuilder Owner { get; } + public MethodBuilder Initializer { get; } + public MethodBuilder Registrar { get; } + public IReadOnlyList Keys { get; } +} diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs index 116fbad16..ea6dd11ef 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs @@ -466,8 +466,7 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) } } - if (DefineDeferredComputedMethodKeyRegistrar(typeBuilder, classExpr.Fields) is { } deferred) - _classExprs.DeferredComputedKeys[classExpr] = deferred; + DefineDeferredComputedMethodKeyRegistrar(classExpr, typeBuilder, classExpr.Fields); } /// @@ -628,7 +627,7 @@ private void EmitClassExpressionStaticConstructor(Expr.ClassExpr classExpr, Type EmitClassPrototypeRegistration( il, typeBuilder, GetClassConstructorLength(classExpr.Methods)); - if (_classes.DeferredClassDefinitions.TryGetValue(typeBuilder.Name, out var deferredDefinition)) + if (_classes.DeferredDefinitions.TryGet(typeBuilder, out var deferredDefinition)) { il.Emit(OpCodes.Ret); il = deferredDefinition.Initializer.GetILGenerator(); @@ -645,7 +644,7 @@ 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); + _classes.DeferredDefinitions.TryGetFieldKey(field, out var computedKey); EmitComputedStaticFieldInitializer(emitter, il, typeBuilder, field, computedKey); break; @@ -683,6 +682,8 @@ private void EmitClassExpressionStaticConstructor(Expr.ClassExpr classExpr, Type EmitSymbolMethodRegistrations(emitter, il, typeBuilder); il.Emit(OpCodes.Ret); + if (deferredDefinition != null) + _classes.DeferredDefinitions.MarkInitializerEmitted(deferredDefinition); } /// @@ -845,7 +846,7 @@ void EmitInstanceFieldInitializers() if (field.ComputedKey != null) { il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldsfld, _classes.ComputedFieldKeys[field]); + il.Emit(OpCodes.Ldsfld, _classes.DeferredDefinitions.RequireFieldKey(field)); if (field.Initializer != null) { emitter.EmitExpression(field.Initializer); diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs index 952cbf00a..81ba37324 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs @@ -277,7 +277,7 @@ private void EmitConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt, Fiel // Stack: this il.Emit(OpCodes.Ldarg_0); // Load the key captured when the class definition was evaluated. - il.Emit(OpCodes.Ldsfld, _classes.ComputedFieldKeys[field]); + il.Emit(OpCodes.Ldsfld, _classes.DeferredDefinitions.RequireFieldKey(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.Methods.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs index 94b972d6d..76f2edc3c 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs @@ -403,8 +403,7 @@ private void DefineClassMethodsOnly(Stmt.Class classStmt) CreateExplicitAccessorProperties(typeBuilder, className); } - if (DefineDeferredComputedMethodKeyRegistrar(typeBuilder, classStmt.Fields) is { } deferred) - _classes.DeferredComputedClassKeys[classStmt] = deferred; + DefineDeferredComputedMethodKeyRegistrar(classStmt, typeBuilder, classStmt.Fields); } private bool TryResolveTypedPrimitiveMethodCoreReturnType( diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs index 8290d9a61..58a2b0280 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs @@ -123,7 +123,7 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt il.Emit(OpCodes.Stsfld, privateFieldStorage); } - if (_classes.DeferredClassDefinitions.TryGetValue(typeBuilder.Name, out var deferredDefinition)) + if (_classes.DeferredDefinitions.TryGet(typeBuilder, out var deferredDefinition)) { il.Emit(OpCodes.Ret); il = deferredDefinition.Initializer.GetILGenerator(); @@ -151,7 +151,7 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt case Stmt.Field field when field.IsStatic: if (field.ComputedKey != null) { - _classes.ComputedFieldKeys.TryGetValue(field, out var computedKey); + _classes.DeferredDefinitions.TryGetFieldKey(field, out var computedKey); EmitComputedStaticFieldInitializer(emitter, il, typeBuilder, field, computedKey); break; } @@ -236,6 +236,8 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt EmitSymbolMethodRegistrations(emitter, il, typeBuilder); il.Emit(OpCodes.Ret); + if (deferredDefinition != null) + _classes.DeferredDefinitions.MarkInitializerEmitted(deferredDefinition); } /// @@ -251,7 +253,7 @@ private void EmitSymbolAccessorRegistrations(ILEmitter emitter, ILGenerator il, foreach (var (accessor, method) in list) { - if (_classes.DeferredClassDefinitions.ContainsKey(typeBuilder.Name)) + if (_classes.DeferredDefinitions.TryGet(typeBuilder, out _)) continue; bool isGetter = accessor.Kind.Type == TokenType.GET; @@ -296,7 +298,7 @@ private void EmitSymbolMethodRegistrations(ILEmitter emitter, ILGenerator il, Ty foreach (var (method, key, builder) in list) { - if (_classes.DeferredClassDefinitions.ContainsKey(typeBuilder.Name)) + if (_classes.DeferredDefinitions.TryGet(typeBuilder, out _)) continue; // owner: typeof(ThisClass) il.Emit(OpCodes.Ldtoken, typeBuilder); @@ -316,10 +318,14 @@ private void EmitSymbolMethodRegistrations(ILEmitter emitter, ILGenerator il, Ty } } - private (MethodBuilder Method, IReadOnlyList Keys)? DefineDeferredComputedMethodKeyRegistrar(TypeBuilder typeBuilder, IReadOnlyList fields) + private void DefineDeferredComputedMethodKeyRegistrar(object source, TypeBuilder typeBuilder, IReadOnlyList fields) { - if (_classes.DeferredClassDefinitions.TryGetValue(typeBuilder.Name, out var existing)) - return (existing.Registrar, existing.Keys); + if (_classes.DeferredDefinitions.TryGet(typeBuilder, out var existing)) + { + if (!_classes.DeferredDefinitions.TryGet(source, out var bySource) || !ReferenceEquals(existing, bySource)) + throw new InvalidOperationException("The deferred class type belongs to a different declaration."); + return; + } 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) @@ -331,7 +337,7 @@ private void EmitSymbolMethodRegistrations(ILEmitter emitter, ILGenerator il, Ty 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; + return; deferred = deferred.OrderBy(entry => entry.Position).ToList(); var initializer = typeBuilder.DefineMethod("$initializeDeferredClass", @@ -345,6 +351,7 @@ private void EmitSymbolMethodRegistrations(ILEmitter emitter, ILGenerator il, Ty MethodAttributes.Assembly | MethodAttributes.Static, _types.Void, [_types.ObjectArray]); + var fieldKeys = new Dictionary(ReferenceEqualityComparer.Instance); var il = registrar.GetILGenerator(); var getTypeFromHandle = _types.GetMethod(_types.Type, "GetTypeFromHandle", _types.RuntimeTypeHandle); @@ -355,7 +362,7 @@ private void EmitSymbolMethodRegistrations(ILEmitter emitter, ILGenerator il, Ty { var keyField = keyOwner.DefineField($"$computedFieldKey_{typeBuilder.Name}_{i}", _types.Object, FieldAttributes.Assembly | FieldAttributes.Static); - _classes.ComputedFieldKeys.Add(field, keyField); + fieldKeys.Add(field, keyField); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldc_I4, i); il.Emit(OpCodes.Ldelem_Ref); @@ -384,8 +391,7 @@ private void EmitSymbolMethodRegistrations(ILEmitter emitter, ILGenerator il, Ty : initializer); il.Emit(OpCodes.Ret); var keys = deferred.Select(entry => entry.Key).ToArray(); - _classes.DeferredClassDefinitions.Add(typeBuilder.Name, (initializer, registrar, keys)); - return (registrar, keys); + _classes.DeferredDefinitions.Declare(source, typeBuilder, initializer, registrar, keys, fieldKeys); } private void EmitComputedStaticFieldInitializer(ILEmitter emitter, ILGenerator il, diff --git a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs index 8e9500368..f461c9c72 100644 --- a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs +++ b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs @@ -90,8 +90,7 @@ private CompilationContext CreateBaseCompilationContext(ILGenerator il, MethodBa ClassExprBuilders = _classExprs.Builders, ClassExprStaticMethods = _classExprs.StaticMethods, ClassExprCaptureFields = _classExprs.CaptureFields, - DeferredComputedClassKeys = _classes.DeferredComputedClassKeys, - DeferredComputedClassExprKeys = _classExprs.DeferredComputedKeys, + DeferredClassDefinitions = _classes.DeferredDefinitions, BlockScopedClassBuilders = _classes.BlockScopedBuilders, ClassRegistry = GetClassRegistry(), DotNetNamespace = _modules.CurrentDotNetNamespace, @@ -268,8 +267,7 @@ private CompilationContext CreateNestedAsyncArrowContext(ILGenerator il, Compila ClassExprBuilders = parentCtx.ClassExprBuilders, ClassExprCaptureFields = parentCtx.ClassExprCaptureFields, BlockScopedClassBuilders = parentCtx.BlockScopedClassBuilders, - DeferredComputedClassKeys = parentCtx.DeferredComputedClassKeys, - DeferredComputedClassExprKeys = parentCtx.DeferredComputedClassExprKeys, + DeferredClassDefinitions = parentCtx.DeferredClassDefinitions, IsStrictMode = parentCtx.IsStrictMode, // ES2022 Private Class Elements support - inherit from parent context CurrentClassName = parentCtx.CurrentClassName, diff --git a/src/SharpTS/Compilation/ILCompiler.State.cs b/src/SharpTS/Compilation/ILCompiler.State.cs index 38a709a0b..dc4fbc490 100644 --- a/src/SharpTS/Compilation/ILCompiler.State.cs +++ b/src/SharpTS/Compilation/ILCompiler.State.cs @@ -19,6 +19,7 @@ public partial class ILCompiler /// private sealed class ClassCompilationState { + public DeferredClassDefinitionRegistry DeferredDefinitions { get; } = new(); public HashSet Declarations { get; } = new(ReferenceEqualityComparer.Instance); public HashSet EmittedMethodBodies { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary Builders { get; } = []; @@ -74,9 +75,6 @@ private sealed class ClassCompilationState // machinery (incl. generator/async state machines); the key drives the .cctor // 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); @@ -442,7 +440,6 @@ private sealed class ClassExpressionCompilationState public Dictionary Superclass { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary EnclosingClass { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary> CaptureFields { get; } = new(ReferenceEqualityComparer.Instance); - public Dictionary Keys)> DeferredComputedKeys { get; } = new(ReferenceEqualityComparer.Instance); } /// diff --git a/src/SharpTS/Compilation/ILCompiler.cs b/src/SharpTS/Compilation/ILCompiler.cs index 7c81b7242..6a032a6ff 100644 --- a/src/SharpTS/Compilation/ILCompiler.cs +++ b/src/SharpTS/Compilation/ILCompiler.cs @@ -1094,6 +1094,7 @@ private void DefineHoistedRegexFields(List statements) /// private void Phase9_FinalizeTypes() { + _classes.DeferredDefinitions.CompleteEmission(); _unionGenerator?.FinalizeAllUnionTypes(); // Finalize generated object-literal shape structs (#862) before any type that uses them. @@ -1652,6 +1653,7 @@ private void ModulePhase10_EmitEntryPoint(List modules) /// private void ModulePhase11_FinalizeTypes() { + _classes.DeferredDefinitions.CompleteEmission(); _unionGenerator?.FinalizeAllUnionTypes(); // Finalize generated object-literal shape structs (#862) before any type that uses them. diff --git a/src/SharpTS/Compilation/ILEmitter.Expressions.cs b/src/SharpTS/Compilation/ILEmitter.Expressions.cs index bda7808e4..396255550 100644 --- a/src/SharpTS/Compilation/ILEmitter.Expressions.cs +++ b/src/SharpTS/Compilation/ILEmitter.Expressions.cs @@ -1129,8 +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); + if (_ctx.DeferredClassDefinitions?.TryGet(ce, out var deferred) == true) + EmitDeferredComputedKeys(deferred.Registrar, 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 c51d0af9a..91a32b74b 100644 --- a/src/SharpTS/Compilation/ILEmitter.Statements.cs +++ b/src/SharpTS/Compilation/ILEmitter.Statements.cs @@ -3307,8 +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); + if (_ctx.DeferredClassDefinitions?.TryGet(classStmt, out var deferred) == true) + EmitDeferredComputedKeys(deferred.Registrar, 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/StatementEmitterBase.cs b/src/SharpTS/Compilation/StatementEmitterBase.cs index 11629fca0..74cad57f5 100644 --- a/src/SharpTS/Compilation/StatementEmitterBase.cs +++ b/src/SharpTS/Compilation/StatementEmitterBase.cs @@ -1835,8 +1835,8 @@ private void EmitStateMachineClassDeclaration(Stmt.Class classStmt) IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassInitialization.RunDefinition); - if (Ctx.DeferredComputedClassKeys?.TryGetValue(classStmt, out var deferred) == true) - EmitDeferredComputedKeys(deferred.Method, deferred.Keys); + if (Ctx.DeferredClassDefinitions?.TryGet(classStmt, out var deferred) == true) + EmitDeferredComputedKeys(deferred.Registrar, deferred.Keys); string storageName = GetClassStorageName(classStmt); var field = GetHoistedVariableField(storageName); @@ -1902,8 +1902,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); + if (Ctx.DeferredClassDefinitions?.TryGet(ce, out var deferred) == true) + EmitDeferredComputedKeys(deferred.Registrar, deferred.Keys); IL.Emit(OpCodes.Ldtoken, typeBuilder); IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); SetStackUnknown(); diff --git a/tests/SharpTS.Tests/CompilerTests/DeferredClassDefinitionRegistryTests.cs b/tests/SharpTS.Tests/CompilerTests/DeferredClassDefinitionRegistryTests.cs new file mode 100644 index 000000000..da188730e --- /dev/null +++ b/tests/SharpTS.Tests/CompilerTests/DeferredClassDefinitionRegistryTests.cs @@ -0,0 +1,95 @@ +using System.Collections; +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Compilation; +using SharpTS.Parsing; +using Xunit; + +namespace SharpTS.Tests.CompilerTests; + +public sealed class DeferredClassDefinitionRegistryTests +{ + [Fact] + public void ForwardReferencesShareOneRecordAndCompletionRequiresBodies() + { + var registry = new DeferredClassDefinitionRegistry(); + var (source, owner, initializer, registrar, fields) = NewDeclaration(); + var keys = new List { source.Fields[0].ComputedKey! }; + var definition = registry.Declare(source, owner, initializer, registrar, keys, fields); + keys.Clear(); + fields.Clear(); + Assert.True(registry.TryGet(source, out var bySource)); + Assert.True(registry.TryGet(owner, out var byType)); + Assert.Same(definition, bySource); + Assert.Same(definition, byType); + Assert.Single(definition.Keys); + Assert.Throws(() => ((IList)definition.Keys).Clear()); + Assert.Throws(registry.CompleteEmission); + Assert.Throws(() => registry.MarkInitializerEmitted(definition)); + initializer.GetILGenerator().Emit(OpCodes.Ret); + var il = registrar.GetILGenerator(); + il.Emit(OpCodes.Call, initializer); + il.Emit(OpCodes.Ret); + registry.MarkInitializerEmitted(definition); + Assert.Throws(() => registry.MarkInitializerEmitted(definition)); + registry.CompleteEmission(); + Assert.True(registry.IsComplete); + Assert.Throws(registry.CompleteEmission); + Assert.Throws(() => registry.MarkInitializerEmitted(definition)); + Assert.Throws(() => registry.Declare(source, owner, initializer, registrar, [], fields)); + owner.CreateType()!.GetMethod("Register")!.Invoke(null, [Array.Empty()]); + } + + [Fact] + public void EmptyRegistryAndSeparateCompilationsHaveExplicitAvailability() + { + var first = new DeferredClassDefinitionRegistry(); + var second = new DeferredClassDefinitionRegistry(); + var (source, owner, initializer, registrar, fields) = NewDeclaration(); + first.CompleteEmission(); + Assert.False(first.TryGet(source, out _)); + Assert.False(first.TryGet(owner, out _)); + Assert.Throws(() => first.RequireFieldKey(source.Fields[0])); + var definition = second.Declare(source, owner, initializer, registrar, [source.Fields[0].ComputedKey!], fields); + Assert.False(second.IsComplete); + Assert.False(first.TryGet(source, out _)); + var third = new DeferredClassDefinitionRegistry(); + Assert.Throws(() => third.MarkInitializerEmitted(definition)); + third.CompleteEmission(); + } + + [Fact] + public void InvalidDeclarationsAreAtomicAndDuplicatesCannotReplaceForwardReferences() + { + var registry = new DeferredClassDefinitionRegistry(); + var (source, owner, initializer, registrar, fields) = NewDeclaration(); + var other = NewDeclaration(); + var key = source.Fields[0].ComputedKey!; + Assert.Throws(() => registry.Declare(source, owner, initializer, registrar, null!, fields)); + Assert.Throws(() => registry.Declare(source, owner, other.Initializer, registrar, [key], fields)); + Assert.Throws(() => registry.Declare(source, owner, initializer, registrar, [null!], fields)); + Assert.Throws(() => registry.Declare(source, owner, initializer, registrar, [key], new Dictionary())); + var invalid = new Dictionary { [source.Fields[0]] = other.Fields.Values.Single() }; + Assert.Throws(() => registry.Declare(source, owner, initializer, registrar, [key], invalid)); + Assert.False(registry.TryGet(source, out _)); + Assert.False(registry.TryGetFieldKey(source.Fields[0], out _)); + var definition = registry.Declare(source, owner, initializer, registrar, [key], fields); + Assert.Throws(() => registry.Declare(source, owner, initializer, registrar, [key], fields)); + Assert.Throws(() => registry.Declare(other.Source, owner, initializer, registrar, [other.Source.Fields[0].ComputedKey!], other.Fields)); + Assert.True(registry.TryGet(owner, out var retained)); + Assert.Same(definition, retained); + Assert.Same(fields.Values.Single(), registry.RequireFieldKey(source.Fields[0])); + } + + private static (Stmt.Class Source, TypeBuilder Owner, MethodBuilder Initializer, MethodBuilder Registrar, + Dictionary Fields) NewDeclaration() + { + var source = Assert.IsType(Assert.Single(new Parser(new Lexer("class C { ['key'] = 1; }").ScanTokens()).ParseOrThrow())); + var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(Guid.NewGuid().ToString("N")), AssemblyBuilderAccess.Run); + var owner = assembly.DefineDynamicModule("module").DefineType("C", TypeAttributes.Public); + var initializer = owner.DefineMethod("Initialize", MethodAttributes.Public | MethodAttributes.Static, typeof(void), Type.EmptyTypes); + var registrar = owner.DefineMethod("Register", MethodAttributes.Public | MethodAttributes.Static, typeof(void), [typeof(object[])]); + var field = owner.DefineField("Key", typeof(object), FieldAttributes.Static); + return (source, owner, initializer, registrar, new Dictionary(ReferenceEqualityComparer.Instance) { [source.Fields[0]] = field }); + } +}