From 21e436d932fd6bc3346a09a94317fa8ce04826b4 Mon Sep 17 00:00:00 2001 From: nickna Date: Sun, 4 Oct 2026 21:58:59 -0700 Subject: [PATCH 01/16] Fix structural compatibility of distinct generic constructors (#1963) --- .../generic-constructor-compatibility.md | 24 +++ .../ILEmitter.Calls.Constructors.cs | 16 +- ...ecker.Compatibility.GenericConstructors.cs | 123 ++++++++++++++ .../TypeSystem/TypeChecker.Compatibility.cs | 3 + .../TypeSystem/TypeChecker.Expressions.cs | 26 ++- .../GenericConstructorCompatibilityTests.cs | 159 ++++++++++++++++++ 6 files changed, 346 insertions(+), 5 deletions(-) create mode 100644 src/SharpTS/TypeSystem/TypeChecker.Compatibility.GenericConstructors.cs create mode 100644 tests/SharpTS.Tests/SharedTests/GenericConstructorCompatibilityTests.cs diff --git a/docs/plans/generic-constructor-compatibility.md b/docs/plans/generic-constructor-compatibility.md index 757523a34..fa227e3b0 100644 --- a/docs/plans/generic-constructor-compatibility.md +++ b/docs/plans/generic-constructor-compatibility.md @@ -101,3 +101,27 @@ Stop after this declared distinct-generic-constructor relation passes the pinned boundary and controls. Repeated class evaluation/computed keys remain #1906; arbitrary dynamic superclass support remains #1907. General structural class, conditional-type or inference conformance is outside this successor. + +## Implementation evidence (#1963) + +The constructor relation now projects constructor parameters, constructed instance +members and required static members into a single contextual signature relation. +Alpha-renaming and inference therefore use one substitution across all three. +Private/protected declaration origins and ECMAScript private brands are checked +before structural projection, including separate static visibility metadata and +common-base origins. Existing declaration-identity fast paths and cache keys remain. +Class expressions now publish their private field/method metadata as private metadata. + +Runtime construction reads generic constructor bindings rather than selecting the +original expression template after reassignment. Literal inferred type arguments +are widened to their corresponding CLR primitive when closing an aliased constructor. + +On Windows ARM64/.NET 10.0.12, all three retained positive sources match Node in CLI +interpretation and IL-verified standalone output. Their shared tests also exercise +the interpreter, in-process compiled execution and serialized verification/execution +with 30-second harness deadlines. All four retained negatives remain rejected. +The final focused selection passes 52 tests; the broader generic/compatibility/class +expression/private-field selection passes 936 tests with compiled IL verification. +Release, code-quality gates and the actual analyzer-aware AOT/trim/single-file build +pass; its zero-warning inventory matches the committed baseline. Hosted execution +was not exercised by these new tests. diff --git a/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs b/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs index 2a4f88012..b720910ff 100644 --- a/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs +++ b/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs @@ -25,6 +25,14 @@ protected override void EmitNew(Expr.New n) var (namespaceParts, className) = ExtractQualifiedNameFromCallee(n.Callee); + if (n.Callee is Expr.Variable constructorVariable + && _ctx.TypeMap?.Get(n.Callee) is SharpTS.TypeSystem.TypeInfo.GenericClass + && _resolver.HasVariable(constructorVariable.Name.Lexeme)) + { + EmitCalleeExprConstruction(n); + return; + } + // A block-scoped class is a runtime lexical binding to a pre-emitted // Type token. Construct through the value path so TDZ and shadowing are // respected instead of resolving the internal Type globally. @@ -633,7 +641,13 @@ private void EmitReflectionConstructFromType(Expr.New n) { IL.Emit(OpCodes.Dup); IL.Emit(OpCodes.Ldc_I4, i); - Type genericArgument = _ctx.TypeMapper.MapTypeInfo(instantiated.TypeArguments[i]); + Type genericArgument = instantiated.TypeArguments[i] switch + { + SharpTS.TypeSystem.TypeInfo.StringLiteral => _ctx.Types.String, + SharpTS.TypeSystem.TypeInfo.NumberLiteral => _ctx.Types.Double, + SharpTS.TypeSystem.TypeInfo.BooleanLiteral => _ctx.Types.Boolean, + var argument => _ctx.TypeMapper.MapTypeInfo(argument) + }; // CLR generic arguments cannot be System.Void; erase void/never to object here. if (_ctx.Types.IsVoid(genericArgument)) genericArgument = _ctx.Types.Object; diff --git a/src/SharpTS/TypeSystem/TypeChecker.Compatibility.GenericConstructors.cs b/src/SharpTS/TypeSystem/TypeChecker.Compatibility.GenericConstructors.cs new file mode 100644 index 000000000..3f3d2cfed --- /dev/null +++ b/src/SharpTS/TypeSystem/TypeChecker.Compatibility.GenericConstructors.cs @@ -0,0 +1,123 @@ +using System.Collections.Frozen; +using SharpTS.Parsing; + +namespace SharpTS.TypeSystem; + +public partial class TypeChecker +{ + private bool GenericConstructorsRelated(TypeInfo.GenericClass target, TypeInfo.GenericClass source) + { + if (!MembersAccessibilityCompatible(target, source) + || !ConstructorPrivateBrandsRelated(target, source, isStatic: false) + || !ConstructorPrivateBrandsRelated(target, source, isStatic: true) + || !ConstructorStaticAccessibilityRelated(target, source)) + return false; + + // Put both observable sides in the signature result so contextual inference + // uses one substitution for parameters, instance members and static members. + var targets = ProjectGenericConstructors(target); + var sources = ProjectGenericConstructors(source); + return targets.All(t => sources.Any(s => SignatureRelatedTo(s, t))); + } + + private List ProjectGenericConstructors(TypeInfo.GenericClass type) + { + var arguments = type.TypeParams.Cast().ToList(); + var members = CollectGenericClassMembers(type, arguments, includeNonPublic: true); + foreach (var (name, member) in CollectConstructorStaticMembers(type)) + members.Add("'static:" + name, member); + if (type.StringIndexType is { } stringIndex) members.Add("'stringIndex", stringIndex); + if (type.NumberIndexType is { } numberIndex) members.Add("'numberIndex", numberIndex); + if (type.SymbolIndexType is { } symbolIndex) members.Add("'symbolIndex", symbolIndex); + var result = new TypeInfo.Record(members.ToFrozenDictionary()); + + var constructor = ResolveClassMemberTypeSubstituted( + new TypeInfo.InstantiatedGeneric(type, arguments), "constructor"); + var functions = constructor switch + { + TypeInfo.Function f => new List { f }, + TypeInfo.OverloadedFunction overloads => overloads.Signatures, + _ => [new TypeInfo.Function([], TypeInfo.Void.Shared)] + }; + return functions.Select(f => new NormalizedSignature(type.TypeParams, + new TypeInfo.Function(f.ParamTypes, result, f.RequiredParams, f.HasRestParam, + f.ThisType, f.ParamNames))).ToList(); + } + + private Dictionary CollectConstructorStaticMembers(TypeInfo type) + { + Dictionary members = []; + Dictionary substitutions = []; + TypeInfo? current = type; + for (int guard = 0; current is not null && guard < 256; guard++) + { + ClassMetadataCore? core; + if (current is TypeInfo.InstantiatedGeneric { GenericDefinition: TypeInfo.GenericClass generic } instance) + { + var arguments = instance.TypeArguments.Select(a => Substitute(a, substitutions)).ToList(); + substitutions = GenericClassSubs(generic, arguments); + core = generic.Core; + } + else + core = current switch + { + TypeInfo.Class c => c.Core, + TypeInfo.GenericClass c => c.Core, + TypeInfo.MutableClass c => c.Freeze().Core, + _ => null + }; + if (core is null) break; + foreach (var (name, member) in core.StaticProperties.Concat(core.StaticMethods)) + members.TryAdd(name, SubstitutePreservingSignatures(member, substitutions)); + current = core.Superclass; + } + return members; + } + + private static bool ConstructorPrivateBrandsRelated(TypeInfo target, TypeInfo source, bool isStatic) + { + var sourceCores = EnumerateClassCores(source).ToList(); + foreach (var core in EnumerateClassCores(target)) + { + bool branded = isStatic + ? core.StaticPrivateFieldTypes.Count > 0 || core.StaticPrivateMethodTypes.Count > 0 + : core.PrivateFieldTypes.Count > 0 || core.PrivateMethodTypes.Count > 0; + if (branded && !sourceCores.Any(s => ReferenceEquals(s, core) + || core.DeclarationId != 0 && s.DeclarationId == core.DeclarationId)) + return false; + } + return true; + } + + private static bool ConstructorStaticAccessibilityRelated(TypeInfo target, TypeInfo source) + { + var targets = CollectStaticBrands(target); + var sources = CollectStaticBrands(source); + foreach (var (name, targetBrand) in targets) + { + if (!sources.TryGetValue(name, out var sourceBrand)) continue; + if (targetBrand.Access == AccessModifier.Public && sourceBrand.Access == AccessModifier.Public) continue; + if (targetBrand.Access != sourceBrand.Access) return false; + if (targetBrand.DeclaringClassId != 0 && targetBrand.DeclaringClassId == sourceBrand.DeclaringClassId) continue; + if (targetBrand.Access == AccessModifier.Protected + && SourceDerivesFromDeclaration(source, targetBrand.DeclaringClassId)) continue; + return false; + } + return true; + } + + private static Dictionary CollectStaticBrands(TypeInfo type) + { + Dictionary brands = []; + foreach (var core in EnumerateClassCores(type)) + { + foreach (var name in core.StaticProperties.Keys) + brands.TryAdd(name, new MemberAccessBrand( + core.StaticFieldAccessMap.GetValueOrDefault(name, AccessModifier.Public), core.DeclarationId)); + foreach (var name in core.StaticMethods.Keys) + brands.TryAdd(name, new MemberAccessBrand( + core.StaticMethodAccessMap.GetValueOrDefault(name, AccessModifier.Public), core.DeclarationId)); + } + return brands; + } +} diff --git a/src/SharpTS/TypeSystem/TypeChecker.Compatibility.cs b/src/SharpTS/TypeSystem/TypeChecker.Compatibility.cs index 7ff037159..43af4d011 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Compatibility.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Compatibility.cs @@ -442,6 +442,9 @@ actual is TypeInfo.GenericClass actualConstructor && if (!_strictNullChecks && actual is TypeInfo.Null or TypeInfo.Undefined) return expected is not TypeInfo.Never; + if (expected is TypeInfo.GenericClass targetClass && actual is TypeInfo.GenericClass sourceClass) + return GenericConstructorsRelated(targetClass, sourceClass); + // Namespace/module types (typeof someNamespace / typeof import(...)) have no dedicated // relation anywhere below, so two structurally-identical namespaces — even the exact same // one referenced twice, e.g. `true ? af : null` vs `true ? null : af` — fell through to the diff --git a/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs b/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs index 90f01864b..e4d3a543d 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs @@ -2574,7 +2574,12 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) { var method = implementations[0]; var funcType = BuildMethodFuncType(method); - if (method.IsStatic) + if (method.IsPrivate) + { + if (method.IsStatic) mutableClass.StaticPrivateMethods[methodName] = funcType; + else mutableClass.PrivateMethods[methodName] = funcType; + } + else if (method.IsStatic) mutableClass.StaticMethods[methodName] = funcType; else mutableClass.Methods[methodName] = funcType; @@ -2593,7 +2598,12 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) TypeInfo fieldType = ResolveAnnotation(field.TypeAnnotation, field.TypeAnnotationNode) ?? TypeInfo.Any.Shared; - if (field.IsStatic) + if (field.IsPrivate) + { + if (field.IsStatic) mutableClass.StaticPrivateFields[fieldName] = fieldType; + else mutableClass.PrivateFields[fieldName] = fieldType; + } + else if (field.IsStatic) mutableClass.StaticProperties[fieldName] = fieldType; else mutableClass.FieldTypes[fieldName] = fieldType; @@ -2754,7 +2764,11 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) } else { - declaredMethodType = method.IsStatic + declaredMethodType = method.IsPrivate + ? method.IsStatic + ? classTypeForBody.StaticPrivateMethodTypes[method.Name.Lexeme] + : classTypeForBody.PrivateMethodTypes[method.Name.Lexeme] + : method.IsStatic ? classTypeForBody.StaticMethods[method.Name.Lexeme] : classTypeForBody.Methods[method.Name.Lexeme]; } @@ -2959,7 +2973,11 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) foreach (var field in classExpr.Fields.Where(f => f.Initializer != null)) { TypeInfo initType = CheckExpr(field.Initializer!); - TypeInfo fieldDeclaredType = field.IsStatic + TypeInfo fieldDeclaredType = field.IsPrivate + ? field.IsStatic + ? classTypeForBody.StaticPrivateFieldTypes[field.Name.Lexeme] + : classTypeForBody.PrivateFieldTypes[field.Name.Lexeme] + : field.IsStatic ? classTypeForBody.StaticProperties[field.Name.Lexeme] : classTypeForBody.FieldTypes[field.Name.Lexeme]; diff --git a/tests/SharpTS.Tests/SharedTests/GenericConstructorCompatibilityTests.cs b/tests/SharpTS.Tests/SharedTests/GenericConstructorCompatibilityTests.cs new file mode 100644 index 000000000..d780c5f4a --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/GenericConstructorCompatibilityTests.cs @@ -0,0 +1,159 @@ +using SharpTS.Tests.Infrastructure; +using SharpTS.TypeSystem; +using SharpTS.TypeSystem.Exceptions; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class GenericConstructorCompatibilityTests +{ + public static IEnumerable PositiveFixtures() + { + yield return ["public-pair.ts", "7\n"]; + yield return ["public-reassignment.ts", "first:1\nsecond:2\n"]; + yield return ["constrained-pair.ts", "ok\n"]; + } + + private static string Fixture(string file) + { + var root = new DirectoryInfo(AppContext.BaseDirectory); + while (root is not null && !File.Exists(Path.Combine(root.FullName, "SharpTS.sln"))) root = root.Parent; + Assert.NotNull(root); + return File.ReadAllText(Path.Combine(root.FullName, "tests", "fixtures", "GenericConstructorCompatibility", file)); + } + + [Theory, MemberData(nameof(PositiveFixtures))] + public void RetainedPositiveFixturesMatchBothEnginesAndSerializedOutput(string file, string expected) + { + var source = Fixture(file); + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + } + + [Theory] + [InlineData("private-pair.ts")] + [InlineData("static-pair.ts")] + [InlineData("narrower-constraint.ts")] + [InlineData("result-pair.ts")] + public void RetainedNegativeFixturesRemainRejected(string file) + => Assert.ThrowsAny(() => new TypeChecker().Check(TestHarness.ParseOrThrow(Fixture(file)))); + + [Theory, ModeData] + public void AliasesArgumentsReturnsAndInheritedConstructorsUseTheSelectedValue(ExecutionMode mode) + { + const string source = """ + class Base { protected brand: number = 1; value: T; + constructor(value: T) { this.value = value; } + read(): T { return this.value; } } + class Box extends Base { static tag: string = "box"; } + class Other extends Base { static tag: string = "other"; } + function identity(value: typeof Box): typeof Box { return value; } + let Alias: typeof Box = Box; + Alias = Alias; + Alias = identity(Other); + console.log(new Alias(7).read(), Alias.tag); + """; + Assert.Equal("7 other\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void OptionalDefaultAndRestConstructorsPreserveRuntimeBinding(ExecutionMode mode) + { + const string source = """ + const Box = class { value: T; constructor(value: T, ...rest: number[]) { this.value = value; } + read(): T { return this.value; } }; + const Other = class { value: U; constructor(value: U, count: number = 2) { this.value = value; } + read(): U { return this.value; } }; + let Alias: typeof Box = Other; + console.log(new Alias(7, 2).read()); + """; + Assert.Equal("7\n", TestHarness.Run(source, mode)); + } + + [Theory] + [InlineData("private value: number = 1;", "private value: number = 1;")] + [InlineData("protected value: number = 1;", "protected value: number = 1;")] + [InlineData("#value: number = 1;", "#value: number = 1;")] + [InlineData("static private value: number = 1;", "static private value: number = 1;")] + [InlineData("static protected value: number = 1;", "static protected value: number = 1;")] + [InlineData("static #value: number = 1;", "static #value: number = 1;")] + [InlineData("#read(): number { return 1; }", "#read(): number { return 1; }")] + [InlineData("static value: number = 1;", "static value: string = 'bad';")] + [InlineData("read(): number { return 1; }", "read(): string { return 'bad'; }")] + public void SameNamedDeclarationsKeepOriginsAndCacheIsolation(string left, string right) + { + var source = $$""" + const Left = class Same { {{left}} }; + const Right = class Same { {{right}} }; + let alias: typeof Left = Left; + alias = alias; + alias = Left; + alias = Right; + """; + Assert.ThrowsAny(() => new TypeChecker().Check(TestHarness.ParseOrThrow(source))); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ModuleImportOrderPreservesStructuralAssignmentAndActualConstructor(bool reverse) + { + var files = new Dictionary + { + ["left.ts"] = "export const Box = class Same { value: T; constructor(value: T) { this.value = value; } };", + ["right.ts"] = "export const Box = class Same { value: U; constructor(value: U) { this.value = value; console.log('right'); } };", + ["main.ts"] = (reverse + ? "import { Box as Right } from './right'; import { Box as Left } from './left';" + : "import { Box as Left } from './left'; import { Box as Right } from './right';") + """ + let Alias: typeof Left = Left; + Alias = Alias; + Alias = Right; + console.log(new Alias(7).value); + """ + }; + Assert.Equal("right\n7\n", TestHarness.RunModules(files, "main.ts", ExecutionMode.Interpreted)); + Assert.Empty(TestHarness.CompileModulesAndVerifyOnly(files, "main.ts")); + Assert.Equal("right\n7\n", TestHarness.RunModules(files, "main.ts", ExecutionMode.Compiled)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ModuleImportOrderDoesNotErasePrivateOrigins(bool reverse) + { + var files = new Dictionary + { + ["left.ts"] = "export const Box = class Same { private value: number = 1; };", + ["right.ts"] = "export const Box = class Same { private value: number = 1; };", + ["main.ts"] = (reverse + ? "import { Box as Right } from './right'; import { Box as Left } from './left';" + : "import { Box as Left } from './left'; import { Box as Right } from './right';") + + "let Alias: typeof Left = Left; Alias = Alias; Alias = Right;" + }; + Assert.Throws(() => TestHarness.CompileModulesAndVerifyOnly(files, "main.ts")); + } + + [Theory, ModeData] + public void CommonBasePrivateBrandsRemainCompatible(ExecutionMode mode) + => Assert.Equal("7\n", TestHarness.Run(""" + class Base { #brand: number = 1; value: T; + constructor(value: T) { this.value = value; } } + class Left extends Base {} + class Right extends Base {} + const Alias: typeof Left = Right; + console.log(new Alias(7).value); + """, mode)); + + [Theory] + [InlineData("constructor(value: T) {}", "constructor(value: U, extra: number) {}")] + [InlineData("constructor(value: T, ...rest: number[]) {}", "constructor(value: U, ...rest: string[]) {}")] + public void IncompatibleRequiredAndRestParametersAreRejected(string left, string right) + => Assert.ThrowsAny(() => new TypeChecker().Check(TestHarness.ParseOrThrow($$""" + class Left { {{left}} } + class Right { {{right}} } + const Alias: typeof Left = Right; + """))); +} From ac4e31eb058e3654a753b212bd1af8bc0d12d873 Mon Sep 17 00:00:00 2001 From: nickna Date: Sun, 4 Oct 2026 22:19:15 -0700 Subject: [PATCH 02/16] Preserve namespace class declaration ownership (#1968) --- docs/plans/namespace-class-identity.md | 22 +++ .../CallHandlers/SuperConstructorHandler.cs | 2 +- .../Compilation/CompilationContext.Classes.cs | 36 ++++ .../ExpressionEmitterBase.CallHelpers.cs | 2 +- .../Compilation/ILCompiler.Classes.Methods.cs | 9 +- src/SharpTS/Compilation/ILCompiler.Classes.cs | 34 +++- .../ILCompiler.ContextFactories.cs | 9 +- .../Compilation/ILCompiler.Namespaces.cs | 41 ++++- src/SharpTS/Compilation/ILCompiler.State.cs | 4 + src/SharpTS/Compilation/ILCompiler.cs | 2 + .../ILEmitter.Calls.Constructors.cs | 1 + .../Compilation/ILEmitter.Expressions.cs | 2 +- .../Compilation/ILEmitter.Namespaces.cs | 3 +- .../Compilation/ILEmitter.Properties.cs | 12 +- .../RuntimeEmitter.Objects.Properties.cs | 19 +++ .../RuntimeEmitter.RuntimeClass.cs | 1 + src/SharpTS/Compilation/TypeMapper.cs | 20 ++- .../Execution/Interpreter.Namespaces.cs | 4 + src/SharpTS/Execution/Interpreter.cs | 2 + .../TypeChecker.Statements.Classes.Stages.cs | 8 +- .../TypeChecker.Statements.Classes.cs | 2 +- src/SharpTS/TypeSystem/TypeMap.cs | 10 ++ .../NamespaceClassIdentityTests.cs | 157 ++++++++++++++++++ 23 files changed, 369 insertions(+), 33 deletions(-) create mode 100644 tests/SharpTS.Tests/SharedTests/NamespaceClassIdentityTests.cs diff --git a/docs/plans/namespace-class-identity.md b/docs/plans/namespace-class-identity.md index f42968bb4..f551eb3b4 100644 --- a/docs/plans/namespace-class-identity.md +++ b/docs/plans/namespace-class-identity.md @@ -89,3 +89,25 @@ evidence remains missing; the new controls demonstrate the same diagnostic famil without claiming to reconstruct the historical source. The finite successor is [#1968](https://github.com/nickna/SharpTS/issues/1968). + +## Implementation verification (#1968) + +The checked declaration now supplies the canonical emitted class owner. Namespace +and module scopes are retained through method, constructor and property-dispatch +emission, and checked references resolve that owner. Namespace object and variable +storage is module-local. Interpreter exports accept namespace declarations, and +reopened namespaces retain earlier exported class bindings. Emitted class metadata +keeps the guest simple name independently of the canonical CLR name. + +Both sibling fixtures now print `1 2 9`; both module import orders print `1 2`. +These unchanged fixtures pass API execution in both engines and CLI compilation, +IL verification and standalone execution, with empty stderr and a 30-second +per-case deadline. Additional shared controls cover nested/reopened namespaces, +private storage/origins, typed signatures, inheritance, bare self-construction, +module namespace variables and constructor aliases with observable names. + +The affected namespace/class-expression/generic/private/module selection passes +711 tests with `SHARPTS_VERIFY_COMPILED=1`, including #1781's original standalone +construction regression. The quality gate passes. Release and actual AOT analyzer +baseline checks pass; the analyzer inventory has zero warnings. Hosted execution is not claimed +for the new fixtures. The historical baseline evidence above remains unchanged. diff --git a/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs b/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs index ed0aa41d8..f8267aaa1 100644 --- a/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs +++ b/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs @@ -41,7 +41,7 @@ public bool TryHandle(IEmitterContext emitter, Expr.Call call) // Try class declaration constructors first var parentCtor = ctx.CurrentSuperclassName != null - ? ctx.ClassRegistry?.GetConstructor(ctx.CurrentSuperclassName) + ? ctx.ClassRegistry?.GetConstructorByQualifiedName(ctx.ResolveClassName(ctx.CurrentSuperclassName)) : null; if (parentCtor != null) { diff --git a/src/SharpTS/Compilation/CompilationContext.Classes.cs b/src/SharpTS/Compilation/CompilationContext.Classes.cs index 980205a31..1a130f237 100644 --- a/src/SharpTS/Compilation/CompilationContext.Classes.cs +++ b/src/SharpTS/Compilation/CompilationContext.Classes.cs @@ -160,6 +160,15 @@ public string ResolvePrivateMethodOwner(string fallbackClassName, string methodN /// public string ResolveClassName(string simpleClassName) { + var namespacePath = CurrentNamespacePath; + var declarationModule = IsScriptTopLevel ? null : CurrentModulePath; + while (namespacePath is not null) + { + if (ScopedClassNames.TryGetValue((declarationModule, namespacePath, simpleClassName), out var scoped)) return scoped; + int dot = namespacePath.LastIndexOf('.'); + namespacePath = dot < 0 ? null : namespacePath[..dot]; + } + if (ScopedClassNames.TryGetValue((declarationModule, null, simpleClassName), out var local)) return local; // A local declaration wins over the shared import/name index, which can // also contain a same-named class from another module. if (CurrentModulePath is not null) @@ -196,6 +205,33 @@ public string ResolveClassName(string simpleClassName) return baseName; } + internal IReadOnlyDictionary CheckedClassNames { get; set; } = new Dictionary(); + internal IReadOnlyDictionary<(string? Module, string? Namespace, string Name), string> ScopedClassNames { get; set; } + = new Dictionary<(string?, string?, string), string>(); + + internal string ResolveClassName(SharpTS.TypeSystem.TypeInfo? type, string fallback) + { + int? declaration = type switch + { + SharpTS.TypeSystem.TypeInfo.Class c => c.Core.DeclarationId, + SharpTS.TypeSystem.TypeInfo.GenericClass c => c.Core.DeclarationId, + SharpTS.TypeSystem.TypeInfo.MutableClass c => c.DeclarationId, + SharpTS.TypeSystem.TypeInfo.Instance i => GetCheckedClassDeclaration(i.ClassType), + SharpTS.TypeSystem.TypeInfo.InstantiatedGeneric i => GetCheckedClassDeclaration(i.GenericDefinition), + _ => null + }; + return declaration is { } id && CheckedClassNames.TryGetValue(id, out var name) ? name : ResolveClassName(fallback); + } + + private static int? GetCheckedClassDeclaration(SharpTS.TypeSystem.TypeInfo type) => type switch + { + SharpTS.TypeSystem.TypeInfo.Class c => c.Core.DeclarationId, + SharpTS.TypeSystem.TypeInfo.GenericClass c => c.Core.DeclarationId, + SharpTS.TypeSystem.TypeInfo.MutableClass c => c.DeclarationId, + SharpTS.TypeSystem.TypeInfo.InstantiatedGeneric i => GetCheckedClassDeclaration(i.GenericDefinition), + _ => null + }; + /// /// Gets the qualified class name for the current module context. /// Also applies .NET namespace if set via @Namespace directive. diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs index 48d80e0ca..cecd392c8 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs @@ -2176,7 +2176,7 @@ protected string ResolveInstanceClassName(Expr receiver, string simpleClassName) if (name.Length > 0) return ResolveClassNameForNew(parts, name); } - return Ctx.ResolveClassName(simpleClassName); + return Ctx.ResolveClassName(Ctx.TypeMap?.Get(receiver), simpleClassName); } protected bool TryEmitDirectMethodCall(Expr receiver, string methodName, List arguments) diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs index 26669a9c9..099f2a61d 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs @@ -30,6 +30,7 @@ private void EmitAllHasFieldsInterfaceMethodBodies(IEnumerable statements) { foreach (var classStmt in CollectClassDeclarations(statements)) { + using var namespaceScope = new ClassNamespaceScope(this, classStmt); if (classStmt.IsDeclare) continue; @@ -50,6 +51,7 @@ private void EmitAllHasFieldsInterfaceMethodBodies(IEnumerable statements) /// private void DefineClassMethodsOnly(Stmt.Class classStmt) { + using var namespaceScope = new ClassNamespaceScope(this, classStmt); // Skip @DotNetType external type classes - they don't have TypeBuilders if (classStmt.IsDeclare) return; @@ -84,7 +86,7 @@ private void DefineClassMethodsOnly(Stmt.Class classStmt) if (constructor != null) { ctorParamTypes = ParameterTypeResolver.ResolveConstructorParameters( - classStmt.Name.Lexeme, constructor.Parameters, _typeMapper, _typeMap); + qualifiedClassName, constructor.Parameters, _typeMapper, _typeMap); } else if (classStmt.SuperclassExpr != null) { @@ -172,7 +174,7 @@ private void DefineClassMethodsOnly(Stmt.Class classStmt) // Use typed parameters from TypeMap var paramTypes = ParameterTypeResolver.ResolveMethodParameters( - classStmt.Name.Lexeme, method.Name.Lexeme, method.Parameters, _typeMapper, _typeMap); + qualifiedClassName, method.Name.Lexeme, method.Parameters, _typeMapper, _typeMap); // Set return type based on method kind // Must check async generator FIRST since it has both IsAsync and IsGenerator true var returnType = (method.IsAsync && method.IsGenerator) ? _types.IAsyncEnumerableOfObject : @@ -201,7 +203,7 @@ private void DefineClassMethodsOnly(Stmt.Class classStmt) // Use typed parameters from TypeMap var paramTypes = ParameterTypeResolver.ResolveMethodParameters( - classStmt.Name.Lexeme, method.Name.Lexeme, method.Parameters, _typeMapper, _typeMap); + qualifiedClassName, method.Name.Lexeme, method.Parameters, _typeMapper, _typeMap); MethodAttributes methodAttrs = MethodAttributes.Public | MethodAttributes.Virtual; if (method.IsAbstract) @@ -546,6 +548,7 @@ private void CreateExplicitAccessorProperties(TypeBuilder typeBuilder, string cl private void EmitClassMethods(Stmt.Class classStmt) { + using var namespaceScope = new ClassNamespaceScope(this, classStmt); if (!_classes.EmittedMethodBodies.Add(classStmt)) return; diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.cs b/src/SharpTS/Compilation/ILCompiler.Classes.cs index c21c34860..a7d59360c 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.cs @@ -20,6 +20,21 @@ namespace SharpTS.Compilation; /// public partial class ILCompiler { + private readonly struct ClassNamespaceScope : IDisposable + { + private readonly ILCompiler _compiler; + private readonly string? _previous; + + public ClassNamespaceScope(ILCompiler compiler, Stmt.Class declaration) + { + _compiler = compiler; + _previous = compiler._currentNamespacePath; + compiler._currentNamespacePath = compiler._classes.DeclarationNamespaces.GetValueOrDefault(declaration); + } + + public void Dispose() => _compiler._currentNamespacePath = _previous; + } + private static void MarkJsVariadicConstructor(ConstructorBuilder constructor) { var parameter = constructor.DefineParameter( @@ -36,8 +51,22 @@ private void DefineClass(Stmt.Class classStmt) // Get qualified class name (includes module prefix and .NET namespace if set) string qualifiedClassName = GetQualifiedClassDeclarationName(classStmt); + var checkedClass = _typeMap.GetClassType(classStmt); + if (_currentNamespacePath is not null) + qualifiedClassName = ctx.GetQualifiedClassName( + $"$nsclass_{_currentNamespacePath.Replace('.', '_')}_{checkedClass?.Core.DeclarationId}_{classStmt.Name.Lexeme}"); + _classes.DeclarationNames[classStmt] = qualifiedClassName; + _classes.DeclarationNamespaces[classStmt] = _currentNamespacePath; + _classes.ScopedNames[(_modules.CurrentPath, _currentNamespacePath, classStmt.Name.Lexeme)] = qualifiedClassName; + if (checkedClass is not null) + { + _classes.CheckedDeclarationNames[checkedClass.Core.DeclarationId] = qualifiedClassName; + _typeMapper.RegisterCheckedClassName(checkedClass.Core.DeclarationId, qualifiedClassName); + _typeMap.SetClassType(qualifiedClassName, checkedClass); + } + // Track simple name -> module mapping for later lookups - if (_modules.CurrentPath != null && !_classes.BlockScopedNames.ContainsKey(classStmt)) + if (_modules.CurrentPath != null && _currentNamespacePath is null && !_classes.BlockScopedNames.ContainsKey(classStmt)) { _modules.ClassToModule[classStmt.Name.Lexeme] = _modules.CurrentPath; } @@ -114,6 +143,9 @@ private void DefineClass(Stmt.Class classStmt) qualifiedClassName, typeAttrs ); + typeBuilder.SetCustomAttribute( + _runtime.FunctionAttributes.FunctionNameCtor, + CustomAttributeEncoder.Encode(_runtime.FunctionAttributes.FunctionNameCtor, classStmt.Name.Lexeme)); if (_classes.BlockScopedNames.ContainsKey(classStmt)) _classes.BlockScopedBuilders[classStmt] = typeBuilder; diff --git a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs index 698c38c29..c2fa2fdbb 100644 --- a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs +++ b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs @@ -38,7 +38,8 @@ public partial class ILCompiler /// private CompilationContext CreateBaseCompilationContext(ILGenerator il, MethodBase? method = null) { - var ctx = new CompilationContext(il, _typeMapper, _functions.Builders, _classes.Builders, _namespaceFields, _namespaceVarFields, _types) + var ctx = new CompilationContext(il, _typeMapper, _functions.Builders, _classes.Builders, + NamespaceFieldsForModule(_modules.CurrentPath), NamespaceVarsForModule(_modules.CurrentPath), _types) { // Closure analysis registries ClosureAnalyzer = _closures.Analyzer, @@ -81,6 +82,8 @@ private CompilationContext CreateBaseCompilationContext(ILGenerator il, MethodBa StaticDirectEvalStatements = _staticDirectEvalStatements, StaticIndirectEvalCalls = _staticIndirectEvalCalls, TypeMap = _typeMap, + CheckedClassNames = _classes.CheckedDeclarationNames, + ScopedClassNames = _classes.ScopedNames, DeadCode = _deadCodeInfo, TypeEmitterRegistry = _typeEmitterRegistry, BuiltInModuleEmitterRegistry = _builtInModuleEmitterRegistry, @@ -256,6 +259,8 @@ private CompilationContext CreateNestedAsyncArrowContext(ILGenerator il, Compila FunctionNames = parentCtx.FunctionNames, FunctionGenericParameters = parentCtx.FunctionGenericParameters, TypeMap = parentCtx.TypeMap, + CheckedClassNames = parentCtx.CheckedClassNames, + ScopedClassNames = parentCtx.ScopedClassNames, DeadCode = parentCtx.DeadCode, AsyncMethods = null, AsyncArrowBuilders = _async.ArrowBuilders, @@ -326,6 +331,8 @@ private CompilationContext CreateDefinitionPhaseContext() return new CompilationContext(null!, _typeMapper, _functions.Builders, _classes.Builders, _namespaceFields, _namespaceVarFields, _types) { ClassToModule = _modules.ClassToModule, + CheckedClassNames = _classes.CheckedDeclarationNames, + ScopedClassNames = _classes.ScopedNames, FunctionToModule = _modules.FunctionToModule, EnumToModule = _modules.EnumToModule, IsStrictMode = _isStrictMode diff --git a/src/SharpTS/Compilation/ILCompiler.Namespaces.cs b/src/SharpTS/Compilation/ILCompiler.Namespaces.cs index 6224c9092..0626ea64c 100644 --- a/src/SharpTS/Compilation/ILCompiler.Namespaces.cs +++ b/src/SharpTS/Compilation/ILCompiler.Namespaces.cs @@ -14,6 +14,31 @@ public partial class ILCompiler /// See for the rationale (#567). /// private readonly Dictionary> _namespaceVarFields = []; + private readonly Dictionary<(string? Module, string Path), FieldBuilder> _namespaceFieldOwners = []; + + private string NamespaceStorageKey(string path) => GetDefinitionContext().GetQualifiedClassName(path); + + private Dictionary NamespaceFieldsForModule(string? module) + { + Dictionary fields = []; + foreach (var (owner, field) in _namespaceFieldOwners) + if (owner.Module is null) fields[owner.Path] = field; + foreach (var (owner, field) in _namespaceFieldOwners) + if (owner.Module == module) fields[owner.Path] = field; + return fields; + } + + private Dictionary> NamespaceVarsForModule(string? module) + { + Dictionary> fields = []; + foreach (var (owner, field) in _namespaceFieldOwners) + { + if (owner.Module != module) continue; + var storageKey = _namespaceFields.First(kv => ReferenceEquals(kv.Value, field)).Key; + if (_namespaceVarFields.TryGetValue(storageKey, out var variables)) fields[owner.Path] = variables; + } + return fields; + } /// /// The namespace path whose members are currently being defined or emitted, or null when @@ -57,13 +82,15 @@ private void DefineNamespaceFields(Stmt.Namespace ns, string parentPath = "") : $"{parentPath}.{ns.Name.Lexeme}"; // Create static field for this namespace if it doesn't exist - if (!_namespaceFields.ContainsKey(path)) + string storageKey = NamespaceStorageKey(path); + if (!_namespaceFields.ContainsKey(storageKey)) { var field = _programType.DefineField( - $"$ns_{path.Replace(".", "_")}", + $"$ns_{storageKey.Replace(".", "_")}", _runtime.Namespaces.Type, FieldAttributes.Public | FieldAttributes.Static); - _namespaceFields[path] = field; + _namespaceFields[storageKey] = field; + _namespaceFieldOwners[(_modules.CurrentPath, path)] = field; } // Record the enclosing namespace during the define phase too: DefineFunction routes @@ -174,6 +201,7 @@ private void ResolveNamespaceImportAliasFunctions() /// private void DefineNamespaceVarField(string nsPath, string varName) { + nsPath = NamespaceStorageKey(nsPath); if (!_namespaceVarFields.TryGetValue(nsPath, out var fields)) { fields = []; @@ -206,7 +234,7 @@ private void DefineNamespaceVarField(string nsPath, string varName) foreach (var part in nsPath.Split('.')) { prefix = prefix.Length == 0 ? part : $"{prefix}.{part}"; - if (_namespaceVarFields.TryGetValue(prefix, out var fields)) + if (_namespaceVarFields.TryGetValue(NamespaceStorageKey(prefix), out var fields)) { foreach (var (name, field) in fields) merged[name] = field; @@ -271,8 +299,9 @@ private void InitializeNamespaceFields(ILGenerator il) { // Initialize namespace fields ordered by nesting depth (parents first) // This ensures parent namespaces exist before children are added - foreach (var (nsPath, field) in _namespaceFields.OrderBy(kv => kv.Key.Count(c => c == '.'))) + foreach (var (owner, field) in _namespaceFieldOwners.OrderBy(kv => kv.Key.Path.Count(c => c == '.'))) { + string nsPath = owner.Path; // Get simple name (last part of path) string simpleName = nsPath.Contains('.') ? nsPath[(nsPath.LastIndexOf('.') + 1)..] @@ -288,7 +317,7 @@ private void InitializeNamespaceFields(ILGenerator il) if (dotIndex > 0) { string parentPath = nsPath[..dotIndex]; - if (_namespaceFields.TryGetValue(parentPath, out var parentField)) + if (_namespaceFieldOwners.TryGetValue((owner.Module, parentPath), out var parentField)) { il.Emit(OpCodes.Ldsfld, parentField); il.Emit(OpCodes.Ldstr, simpleName); diff --git a/src/SharpTS/Compilation/ILCompiler.State.cs b/src/SharpTS/Compilation/ILCompiler.State.cs index a37611c40..e981f9971 100644 --- a/src/SharpTS/Compilation/ILCompiler.State.cs +++ b/src/SharpTS/Compilation/ILCompiler.State.cs @@ -23,6 +23,10 @@ private sealed class ClassCompilationState public HashSet Declarations { get; } = new(ReferenceEqualityComparer.Instance); public HashSet EmittedMethodBodies { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary Builders { get; } = []; + public Dictionary DeclarationNames { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary DeclarationNamespaces { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary CheckedDeclarationNames { get; } = []; + public Dictionary<(string? Module, string? Namespace, string Name), string> ScopedNames { get; } = []; public Dictionary BlockScopedNames { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary BlockScopedBuilders { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary Superclass { get; } = []; diff --git a/src/SharpTS/Compilation/ILCompiler.cs b/src/SharpTS/Compilation/ILCompiler.cs index 49f0e5afd..6028ca101 100644 --- a/src/SharpTS/Compilation/ILCompiler.cs +++ b/src/SharpTS/Compilation/ILCompiler.cs @@ -1420,6 +1420,8 @@ private void DefineDeclarationFromStatement(Stmt stmt, bool blockScoped = false) private string GetQualifiedClassDeclarationName(Stmt.Class classStmt, bool resolve = false) { + if (_classes.DeclarationNames.TryGetValue(classStmt, out var declared)) + return declared; if (_classes.BlockScopedNames.TryGetValue(classStmt, out var generated)) return generated; var ctx = GetDefinitionContext(); diff --git a/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs b/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs index b720910ff..8ff1d7072 100644 --- a/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs +++ b/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs @@ -58,6 +58,7 @@ protected override void EmitNew(Expr.New n) // 4. ILEmitter-specific: resolve class name with namespace imports and imported class aliases string resolvedClassName = ResolveClassNameForNew(namespaceParts, className); + resolvedClassName = _ctx.ResolveClassName(_ctx.TypeMap?.Get(n.Callee), resolvedClassName); // 5. ILEmitter-specific: external .NET type construction if (_ctx.TypeMapper.ExternalTypes.TryGetValue(className, out var externalType) || diff --git a/src/SharpTS/Compilation/ILEmitter.Expressions.cs b/src/SharpTS/Compilation/ILEmitter.Expressions.cs index 7c60ef138..cccf5ff86 100644 --- a/src/SharpTS/Compilation/ILEmitter.Expressions.cs +++ b/src/SharpTS/Compilation/ILEmitter.Expressions.cs @@ -242,7 +242,7 @@ protected override void EmitVariable(Expr.Variable v) } // Check if it's a class - load the Type object - if (_ctx.Classes.TryGetValue(_ctx.ResolveClassName(name), out var classType)) + if (_ctx.Classes.TryGetValue(_ctx.ResolveClassName(_ctx.TypeMap?.Get(v), name), out var classType)) { IL.Emit(OpCodes.Ldtoken, classType); IL.Emit(OpCodes.Call, _ctx.Types.GetMethod(_ctx.Types.Type, "GetTypeFromHandle", _ctx.Types.RuntimeTypeHandle)); diff --git a/src/SharpTS/Compilation/ILEmitter.Namespaces.cs b/src/SharpTS/Compilation/ILEmitter.Namespaces.cs index 8e6182963..184f1f4ce 100644 --- a/src/SharpTS/Compilation/ILEmitter.Namespaces.cs +++ b/src/SharpTS/Compilation/ILEmitter.Namespaces.cs @@ -141,7 +141,8 @@ private void EmitNamespaceMember(Stmt member, FieldBuilder nsField, string nsPat case Stmt.Class classStmt: // Classes are defined separately - store the Type in namespace - if (_ctx.Classes.TryGetValue(_ctx.ResolveClassName(classStmt.Name.Lexeme), out var classType)) + if (_ctx.Classes.TryGetValue(_ctx.ResolveClassName( + _ctx.TypeMap?.GetClassType(classStmt), classStmt.Name.Lexeme), out var classType)) { IL.Emit(OpCodes.Ldsfld, nsField); IL.Emit(OpCodes.Ldstr, classStmt.Name.Lexeme); diff --git a/src/SharpTS/Compilation/ILEmitter.Properties.cs b/src/SharpTS/Compilation/ILEmitter.Properties.cs index 10063724a..7d6f8174a 100644 --- a/src/SharpTS/Compilation/ILEmitter.Properties.cs +++ b/src/SharpTS/Compilation/ILEmitter.Properties.cs @@ -237,9 +237,9 @@ protected override void EmitGet(Expr.Get g) return; // Handle static member access via class name - if (g.Object is Expr.Variable classVar) + if (g.Object is Expr.Variable classVar && !_resolver.HasVariable(classVar.Name.Lexeme)) { - string resolvedClassName = _ctx.ResolveClassName(classVar.Name.Lexeme); + string resolvedClassName = _ctx.ResolveClassName(_ctx.TypeMap?.Get(classVar), classVar.Name.Lexeme); if (_ctx.Classes.TryGetValue(resolvedClassName, out var classBuilder)) { // Try static getter first (for auto-accessors and explicit static accessors) @@ -1084,9 +1084,9 @@ protected override void EmitSet(Expr.Set s) // Handle static property assignment via class name: delegate to EmitStaticMemberSet // which handles setters (auto-accessor + explicit), regular static fields, and private // static fields with correct signature-driven coercion + return-value handling. - if (s.Object is Expr.Variable classVar) + if (s.Object is Expr.Variable classVar && !_resolver.HasVariable(classVar.Name.Lexeme)) { - string resolvedClassName = _ctx.ResolveClassName(classVar.Name.Lexeme); + string resolvedClassName = _ctx.ResolveClassName(_ctx.TypeMap?.Get(classVar), classVar.Name.Lexeme); if (_ctx.Classes.TryGetValue(resolvedClassName, out var classBuilder)) { if (EmitStaticMemberSet(resolvedClassName, classBuilder, s.Name.Lexeme, s.Value)) @@ -2500,7 +2500,7 @@ private bool TryEmitDirectGetterCall(Expr receiver, TypeInfo? receiverType, stri return true; } - string className = _ctx.ResolveClassName(simpleClassName); + string className = _ctx.ResolveClassName(receiverType, simpleClassName); // Convert TypeScript camelCase property name to .NET PascalCase for lookup string pascalPropertyName = NamingConventions.ToPascalCase(propertyName); @@ -2587,7 +2587,7 @@ private bool TryEmitDirectSetterCall(Expr receiver, TypeInfo? receiverType, stri return true; } - string className = _ctx.ResolveClassName(simpleClassName); + string className = _ctx.ResolveClassName(receiverType, simpleClassName); // Convert TypeScript camelCase property name to .NET PascalCase for lookup string pascalPropertyName = NamingConventions.ToPascalCase(propertyName); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs index c9b58d705..33a2e4135 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs @@ -86,6 +86,7 @@ private readonly record struct GetPropertyInputs( TypeBuilder FunctionApplyWrapperType, TypeBuilder FunctionBindWrapperType, TypeBuilder FunctionCallWrapperType, + EmittedFunctionAttributesRuntime FunctionAttributes, FieldBuilder FunctionPrototypeField, MethodBuilder FunctionPrototypePopulateMethod, MethodBuilder GetFunctionMethod, @@ -2440,6 +2441,24 @@ void EmitErrorTypeBranch(Type ctorType, MethodBuilder populate, FieldBuilder pro il.Emit(OpCodes.Ldstr, "BigInt"); il.Emit(OpCodes.Ret); il.MarkLabel(genericTypeNameLabel); + var reflectedTypeNameLabel = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, typeLocal); + il.Emit(OpCodes.Ldtoken, inputs.FunctionAttributes.FunctionNameType); + il.Emit(OpCodes.Call, _types.TypeGetTypeFromHandle); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "IsDefined", _types.Type, _types.Boolean)); + il.Emit(OpCodes.Brfalse, reflectedTypeNameLabel); + il.Emit(OpCodes.Ldloc, typeLocal); + il.Emit(OpCodes.Ldtoken, inputs.FunctionAttributes.FunctionNameType); + il.Emit(OpCodes.Call, _types.TypeGetTypeFromHandle); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetCustomAttributes", _types.Type, _types.Boolean)); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Castclass, inputs.FunctionAttributes.FunctionNameType); + il.Emit(OpCodes.Ldfld, inputs.FunctionAttributes.FunctionNameValueField); + il.Emit(OpCodes.Ret); + il.MarkLabel(reflectedTypeNameLabel); il.Emit(OpCodes.Ldloc, typeLocal); il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Type, "Name").GetGetMethod()!); il.Emit(OpCodes.Ret); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs index 9694c91f9..5e5b495e5 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs @@ -1215,6 +1215,7 @@ void EmitLinkProto(FieldBuilder child) runtime.FunctionBindings.ApplyType, runtime.FunctionBindings.BindType, runtime.FunctionBindings.CallType, + runtime.FunctionAttributes, runtime.FunctionPrototypes.Prototype, runtime.FunctionPrototypes.Populate, runtime.FunctionIntrospection.GetProperty, diff --git a/src/SharpTS/Compilation/TypeMapper.cs b/src/SharpTS/Compilation/TypeMapper.cs index 8c0be8a8b..98642a047 100644 --- a/src/SharpTS/Compilation/TypeMapper.cs +++ b/src/SharpTS/Compilation/TypeMapper.cs @@ -20,6 +20,12 @@ public class TypeMapper { private readonly ModuleBuilder _moduleBuilder; private readonly TypeProvider _types; + private readonly Dictionary _checkedClassNames = []; + + internal void RegisterCheckedClassName(int declarationId, string name) => _checkedClassNames[declarationId] = name; + + private Type GetCheckedClassType(ClassMetadataCore core) => GetClassType( + _checkedClassNames.GetValueOrDefault(core.DeclarationId, core.Name)); private Dictionary? _classBuilders; private UnionTypeGenerator? _unionGenerator; public ExternalTypeRegistry ExternalTypeDeclarations { get; } = new(); @@ -149,10 +155,10 @@ private void RequireLocalClass(TypeBuilder builder) TypeInfo.Array => _types.Object, // Will be TSArray at runtime TypeInfo.Function => _types.Object, // Will be delegate at runtime TypeInfo.Promise p => MapPromiseType(p), // Promise maps to Task - TypeInfo.Class c => GetClassType(c.Name), + TypeInfo.Class c => GetCheckedClassType(c.Core), TypeInfo.Instance i => i.ClassType switch { - TypeInfo.Class c => GetClassType(c.Name), + TypeInfo.Class c => GetCheckedClassType(c.Core), TypeInfo.InstantiatedGeneric => _types.Object, _ => _types.Object }, @@ -233,7 +239,7 @@ public Type GetClassType(string className) TypeInfo.Array arr => MapArrayTypeStrict(arr), TypeInfo.Function => _types.Delegate, // Functions map to Delegate for typed interop TypeInfo.Promise p => MapPromiseTypeStrict(p), - TypeInfo.Class c => GetClassType(c.Name), + TypeInfo.Class c => GetCheckedClassType(c.Core), TypeInfo.Instance i => MapInstanceTypeStrict(i), TypeInfo.Record => _types.Object, // Records remain dynamic objects TypeInfo.Void => _types.Void, @@ -253,7 +259,7 @@ public Type GetClassType(string className) TypeInfo.Symbol => _types.String, // Symbols map to string keys // Generic types - attempt to resolve if instantiated TypeInfo.TypeParameter => _types.Object, - TypeInfo.GenericClass gc => GetClassType(gc.Name), + TypeInfo.GenericClass gc => GetCheckedClassType(gc.Core), TypeInfo.GenericFunction => _types.Delegate, TypeInfo.GenericInterface => _types.Object, TypeInfo.InstantiatedGeneric ig => MapInstantiatedGenericStrict(ig), @@ -268,8 +274,8 @@ public Type GetClassType(string className) private Type MapInstanceTypeStrict(TypeInfo.Instance instance) => instance.ClassType switch { - TypeInfo.Class c => GetClassType(c.Name), - TypeInfo.GenericClass gc => GetClassType(gc.Name), + TypeInfo.Class c => GetCheckedClassType(c.Core), + TypeInfo.GenericClass gc => GetCheckedClassType(gc.Core), TypeInfo.InstantiatedGeneric ig => MapInstantiatedGenericStrict(ig), _ => _types.Object }; @@ -278,7 +284,7 @@ private Type MapInstantiatedGenericStrict(TypeInfo.InstantiatedGeneric ig) { // For instantiated generics, try to resolve the base type if (ig.GenericDefinition is TypeInfo.GenericClass gc) - return GetClassType(gc.Name); + return GetCheckedClassType(gc.Core); return _types.Object; } diff --git a/src/SharpTS/Execution/Interpreter.Namespaces.cs b/src/SharpTS/Execution/Interpreter.Namespaces.cs index 462f8eb99..57f5a30c2 100644 --- a/src/SharpTS/Execution/Interpreter.Namespaces.cs +++ b/src/SharpTS/Execution/Interpreter.Namespaces.cs @@ -47,6 +47,10 @@ private ExecutionResult ExecuteNamespace(Stmt.Namespace ns) { namespaceEnv.DefineNamespace(memberName, nestedNs); } + else + { + namespaceEnv.Define(memberName, memberValue); + } } } diff --git a/src/SharpTS/Execution/Interpreter.cs b/src/SharpTS/Execution/Interpreter.cs index ba6dcd460..36e751e40 100644 --- a/src/SharpTS/Execution/Interpreter.cs +++ b/src/SharpTS/Execution/Interpreter.cs @@ -2226,6 +2226,7 @@ private string GetDeclaredName(Stmt decl) // `export const x = …` now parses as Stmt.Const (was Stmt.Var before #428). Stmt.Const c => c.Name.Lexeme, Stmt.Enum e => e.Name.Lexeme, + Stmt.Namespace n => n.Name.Lexeme, _ => throw new InterpreterException($"Cannot get name of declaration type {decl.GetType().Name}") }; } @@ -2244,6 +2245,7 @@ private string GetDeclaredName(Stmt decl) // `export const x = …` now parses as Stmt.Const (was Stmt.Var before #428). Stmt.Const c => c.Name, Stmt.Enum e => e.Name, + Stmt.Namespace n => n.Name, _ => throw new InterpreterException($"Cannot get value of declaration type {decl.GetType().Name}") }; return _environment.Get(token).ToObject(); diff --git a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.Stages.cs b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.Stages.cs index f4d0803a2..0ae9f2e61 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.Stages.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.Stages.cs @@ -89,7 +89,7 @@ private TypeInfo.Class PublishInitialClassType( _environment.DefineType(classStmt.Name.Lexeme, genericClassType); // For body check, freeze the mutable class (methods/fields have TypeParameter types) classTypeForBody = mutableClass.Freeze(); - _typeMap.SetClassType(classStmt.Name.Lexeme, classTypeForBody); + _typeMap.SetClassType(classStmt, classTypeForBody); } else { @@ -97,7 +97,7 @@ private TypeInfo.Class PublishInitialClassType( TypeInfo.Class classType = mutableClass.Freeze(); _environment.Define(classStmt.Name.Lexeme, classType); _environment.DefineType(classStmt.Name.Lexeme, classType); - _typeMap.SetClassType(classStmt.Name.Lexeme, classType); + _typeMap.SetClassType(classStmt, classType); classTypeForBody = classType; } return classTypeForBody; @@ -392,14 +392,14 @@ private void RepublishInferredClassType( var frozen = mutableClass.FreezeGeneric(classTypeParams); _environment.Define(classStmt.Name.Lexeme, frozen); _environment.DefineType(classStmt.Name.Lexeme, frozen); - _typeMap.SetClassType(classStmt.Name.Lexeme, mutableClass.Freeze()); + _typeMap.SetClassType(classStmt, mutableClass.Freeze()); } else { var refrozen = mutableClass.Freeze(); _environment.Define(classStmt.Name.Lexeme, refrozen); _environment.DefineType(classStmt.Name.Lexeme, refrozen); - _typeMap.SetClassType(classStmt.Name.Lexeme, refrozen); + _typeMap.SetClassType(classStmt, refrozen); } // Structural compatibility results cache on CacheKey() (carries the stable DeclarationId), // so any comparison made against the placeholder during the body pass must not be reused. diff --git a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs index 5790b24e5..e3f542dad 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs @@ -1460,7 +1460,7 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) parentEnv.Define(classStmt.Name.Lexeme, classType); parentEnv.DefineType(classStmt.Name.Lexeme, classType); } - _typeMap.SetClassType(classStmt.Name.Lexeme, classType); + _typeMap.SetClassType(classStmt, classType); } // End EnvironmentScope } diff --git a/src/SharpTS/TypeSystem/TypeMap.cs b/src/SharpTS/TypeSystem/TypeMap.cs index cb7a82e2a..f2d7cc65c 100644 --- a/src/SharpTS/TypeSystem/TypeMap.cs +++ b/src/SharpTS/TypeSystem/TypeMap.cs @@ -56,6 +56,16 @@ public class TypeMap /// public void SetClassType(string className, TypeInfo.Class classType) => _classTypes[className] = classType; + private readonly Dictionary _classDeclarations = new(ReferenceEqualityComparer.Instance); + + public void SetClassType(Stmt.Class declaration, TypeInfo.Class classType) + { + _classDeclarations[declaration] = classType; + SetClassType(declaration.Name.Lexeme, classType); + } + + public TypeInfo.Class? GetClassType(Stmt.Class declaration) => _classDeclarations.GetValueOrDefault(declaration); + /// /// Gets the class type by name, or null if not found. /// diff --git a/tests/SharpTS.Tests/SharedTests/NamespaceClassIdentityTests.cs b/tests/SharpTS.Tests/SharedTests/NamespaceClassIdentityTests.cs new file mode 100644 index 000000000..e949255da --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/NamespaceClassIdentityTests.cs @@ -0,0 +1,157 @@ +using SharpTS.Tests.Infrastructure; +using SharpTS.TypeSystem; +using SharpTS.TypeSystem.Exceptions; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class NamespaceClassIdentityTests +{ + private static string Fixture(string file) + { + var root = new DirectoryInfo(AppContext.BaseDirectory); + while (root is not null && !File.Exists(Path.Combine(root.FullName, "SharpTS.sln"))) root = root.Parent; + Assert.NotNull(root); + return File.ReadAllText(Path.Combine(root.FullName, "tests", "fixtures", "NamespaceClassIdentity", file)); + } + + [Theory] + [InlineData("siblings.ts")] + [InlineData("siblings-nongeneric.ts")] + public void RetainedSiblingFixturesPreserveOwners(string file) + { + var source = Fixture(file); + Assert.Equal("1 2 9\n", TestHarness.RunInterpreted(source)); + Assert.Equal("1 2 9\n", TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal("1 2 9\n", output); + } + + [Theory] + [InlineData("main-left.ts")] + [InlineData("main-right.ts")] + public void RetainedModuleFixturesPreserveOwnersInBothImportOrders(string entry) + { + var files = new Dictionary + { + ["left.ts"] = Fixture("left.ts"), + ["right.ts"] = Fixture("right.ts"), + [entry] = Fixture(entry) + """ + const L = Left.Box, R = Right.Box; + console.log(L === R, L.name, R.name); + console.log(new L().read(), new R().read()); + """ + }; + const string expected = "1 2\nfalse Box Box\n1 2\n"; + Assert.Equal(expected, TestHarness.RunModules(files, entry, ExecutionMode.Interpreted)); + Assert.Empty(TestHarness.CompileModulesAndVerifyOnly(files, entry)); + Assert.Equal(expected, TestHarness.RunModules(files, entry, ExecutionMode.Compiled)); + } + + [Theory, ModeData] + public void NestedReopenedNamespacesKeepPrivateStorageAndBareClassReferences(ExecutionMode mode) + { + const string source = """ + namespace Left { + export let value = 1; + export class Box { + #private: number = 10; + value: T; + constructor(value: T) { this.value = value; } + read() { return value + this.#private; } + again(value: T) { return new Box(value); } + } + export namespace Nested { export class Box { read() { return 3; } } } + } + namespace Left { export class Other extends Box { read() { return super.read() + 1; } } } + namespace Right { + export let value = 2; + export class Box { + #private: number = 20; + value: T; + constructor(value: T) { this.value = value; } + read() { return value + this.#private; } + again(value: T) { return new Box(value); } + } + } + class Box { read() { return 9; } } + const l = new Left.Box(7), r = new Right.Box("ok"); + console.log(l.read(), r.read(), l.again(8).read(), r.again("next").read()); + console.log(l.value, r.value, new Left.Nested.Box().read(), new Left.Other(5).read(), new Box().read()); + """; + Assert.Equal("11 22 11 22\n7 ok 3 12 9\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void ModuleMemberBodiesResolveTheirOwnNamespaceInBothImportOrders(ExecutionMode mode) + { + foreach (var reverse in new[] { false, true }) + { + var files = new Dictionary + { + ["left.ts"] = """ + export namespace Library { + export let value = 1; + export class Box { + #value = 10; + read() { return value + this.#value; } + again() { return new Box(); } + static read() { return value; } + } + } + """, + ["right.ts"] = """ + export namespace Library { + export let value = 2; + export class Box { + #value = 20; + read() { return value + this.#value; } + again() { return new Box(); } + static read() { return value; } + } + } + """, + ["main.ts"] = (reverse + ? "import { Library as R } from './right'; import { Library as L } from './left';" + : "import { Library as L } from './left'; import { Library as R } from './right';") + """ + const l = new L.Box(), r = new R.Box(); + console.log(l.again().read(), r.again().read(), L.Box.read(), R.Box.read()); + """ + }; + Assert.Equal("11 22 1 2\n", TestHarness.RunModules(files, "main.ts", mode)); + } + } + + [Theory] + [InlineData("private value = 1;")] + [InlineData("protected value = 1;")] + [InlineData("#value = 1;")] + public void SiblingNamespacesKeepDistinctPrivateOrigins(string member) + => Assert.ThrowsAny(() => new TypeChecker().Check(TestHarness.ParseOrThrow($$""" + namespace Left { export class Box { {{member}} } } + namespace Right { export class Box { {{member}} } } + let alias: typeof Left.Box = Left.Box; + alias = Right.Box; + """))); + + [Theory, ModeData] + public void TypedSiblingMethodsAndStaticMembersKeepDeclarationMetadata(ExecutionMode mode) + => Assert.Equal("7 ok\n11 22\n", TestHarness.Run(""" + namespace Left { export class Box { + static value: number = 11; + value: number; constructor(value: number) { this.value = value; } + read(): number { return this.value; } + get result(): number { return this.value; } + } } + namespace Right { export class Box { + static value: number = 22; + value: string; constructor(value: string) { this.value = value; } + read(): string { return this.value; } + get result(): string { return this.value; } + } } + const l: Left.Box = new Left.Box(7), r: Right.Box = new Right.Box("ok"); + console.log(l.result, r.read()); + console.log(Left.Box.value, Right.Box.value); + """, mode)); +} From 011fde87e7079fc4967ac63a74490aab005e0323 Mon Sep 17 00:00:00 2001 From: nickna Date: Sun, 4 Oct 2026 22:53:13 -0700 Subject: [PATCH 03/16] Give repeated class expressions fresh guest constructor identity (#1964) --- docs/plans/repeated-class-evaluation.md | 34 +++ .../Compilation/ClassExpressionFactory.cs | 5 + .../CompilationContext.ClassExpressions.cs | 1 + .../EmittedClassDefinitionRuntime.cs | 55 ++++ src/SharpTS/Compilation/EmittedRuntime.cs | 1 + .../ExpressionEmitterBase.ClassDefinitions.cs | 86 ++++++ .../Compilation/ExpressionEmitterBase.cs | 20 +- .../ILCompiler.Classes.ClassExpressions.cs | 20 +- .../ILCompiler.Classes.Definitions.cs | 60 ++++ .../ILCompiler.Classes.HasFields.cs | 3 +- .../ILCompiler.ContextFactories.cs | 2 + src/SharpTS/Compilation/ILCompiler.State.cs | 2 + .../ILEmitter.Calls.Constructors.cs | 15 + .../Compilation/ILEmitter.Expressions.cs | 41 +-- .../Compilation/ParameterTypeResolver.cs | 2 + .../RuntimeEmitter.ClassDefinition.cs | 268 ++++++++++++++++++ .../RuntimeEmitter.DynamicConstruct.cs | 12 + .../RuntimeEmitter.Functions.NewOp.cs | 12 + .../Compilation/RuntimeEmitter.Functions.cs | 2 + .../RuntimeEmitter.Objects.Index.cs | 4 + .../RuntimeEmitter.Objects.Properties.cs | 30 ++ .../RuntimeEmitter.Objects.Prototype.cs | 14 + .../RuntimeEmitter.Objects.SetProperty.cs | 2 + .../Compilation/RuntimeEmitter.Operators.cs | 30 ++ .../Compilation/RuntimeEmitter.Reflect.cs | 9 + .../RuntimeEmitter.RuntimeClass.cs | 9 + src/SharpTS/Compilation/RuntimeEmitter.cs | 4 + .../Compilation/StatementEmitterBase.cs | 49 +--- src/SharpTS/Execution/Interpreter.Calls.cs | 6 +- .../Interpreter.ClassInitializers.cs | 19 ++ .../Execution/Interpreter.Expressions.cs | 1 + .../Execution/Interpreter.Properties.cs | 2 + .../Execution/Interpreter.Statements.cs | 1 + src/SharpTS/Runtime/Types/SharpTSClass.cs | 9 +- .../TypeSystem/TypeChecker.Expressions.cs | 6 + .../EmittedClassDefinitionRuntimeTests.cs | 52 ++++ .../RepeatedClassDefinitionTests.cs | 122 ++++++++ .../prototype-separate-values.ts | 9 + 38 files changed, 917 insertions(+), 102 deletions(-) create mode 100644 src/SharpTS/Compilation/ClassExpressionFactory.cs create mode 100644 src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs create mode 100644 src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs create mode 100644 src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs create mode 100644 src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs create mode 100644 src/SharpTS/Execution/Interpreter.ClassInitializers.cs create mode 100644 tests/SharpTS.Tests/CompilerTests/EmittedClassDefinitionRuntimeTests.cs create mode 100644 tests/SharpTS.Tests/SharedTests/RepeatedClassDefinitionTests.cs create mode 100644 tests/fixtures/RepeatedClassEvaluation/prototype-separate-values.ts diff --git a/docs/plans/repeated-class-evaluation.md b/docs/plans/repeated-class-evaluation.md index bffc375fe..7ddb8d771 100644 --- a/docs/plans/repeated-class-evaluation.md +++ b/docs/plans/repeated-class-evaluation.md @@ -97,3 +97,37 @@ new syntax and unrelated conformance failures remain their existing tasks. The inline prototype-call IL failure is a separate verification defect, [#1966](https://github.com/nickna/SharpTS/issues/1966); its failure is retained rather than used to claim this representation work is repaired. + +## Implementation evidence for #1964 + +Each class-expression evaluation now returns a fresh emitted `$ClassDefinition` +with its own prototype, constructor back-reference, name and arity. A generated +factory passes the selected definition into the shared template's constructor +before instance field initialization. Generic arguments are erased at that +factory boundary, so instances of one guest definition share its prototype. +Runtime construction, Reflect construction, `typeof`, and `instanceof` recognize +these definition values. The interpreter uses definition identity for class +brands and evaluates instance initializers with their owning environment and +receiver. Named expressions retain their local body binding. + +`RepeatedClassDefinitionTests` covers repeated evaluations in functions, loops, +closures, modules, async and generator suspension, plus generic arguments, +initializers, prototype mutation, default arguments and bound construction. The +ten source controls run in both engines and as serialized, IL-verified programs. +The module control runs in both engines. A reused runtime emitter test separately +verifies fresh metadata ownership and saved IL in ordinary and hosted emission; +this is emission evidence, not a hosted class-factory execution claim. + +On Windows ARM64/.NET 10.0.12 and Node v25.5.0, the unchanged +`constructors-only.ts` now prints `false` in verified CLI standalone output. +`prototype-separate-values.ts` prints `false`, `false`, `true true`, and +`true false`, matching Node. Both standalone runs complete within 30 seconds +with empty stderr and without SharpTS.dll in the output directory. The inline +`identity.ts` source remains the separate #1966 verifier control. + +The affected class-expression/owner/local-class, constructor, generic, computed +member, namespace and prototype suite passes all 1,181 tests with compiled IL +verification enabled. The full Release solution build and code-quality gates +pass (28 duplicate groups, zero errors). The actual AOT/trim/single-file analyzer +passes the unchanged zero-warning baseline. Definition-owned computed keys, +capture snapshots and static initialization continue in #1965. diff --git a/src/SharpTS/Compilation/ClassExpressionFactory.cs b/src/SharpTS/Compilation/ClassExpressionFactory.cs new file mode 100644 index 000000000..7cc0384ac --- /dev/null +++ b/src/SharpTS/Compilation/ClassExpressionFactory.cs @@ -0,0 +1,5 @@ +using System.Reflection.Emit; + +namespace SharpTS.Compilation; + +internal sealed record ClassExpressionFactory(MethodBuilder Method, Type Template, string Name, int Length); diff --git a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs index 424da11a4..aa44242c5 100644 --- a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs +++ b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs @@ -11,6 +11,7 @@ public partial class CompilationContext // Class expression builders (class expr node -> type builder) public Dictionary? ClassExprBuilders { get; set; } + internal Dictionary? ClassExprFactories { get; set; } // Class expression extended tracking public Dictionary>? ClassExprBackingFields { get; set; } diff --git a/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs new file mode 100644 index 000000000..2f100e5bb --- /dev/null +++ b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Reflection.Emit; + +namespace SharpTS.Compilation; + +/// Guest class evaluations, independent of their pre-emitted CLR templates. +public sealed class EmittedClassDefinitionRuntime +{ + internal EmittedClassDefinitionRuntime() { } + internal sealed record Declarations(TypeBuilder Type, Type InstanceInterface, + MethodInfo GetDefinition, ConstructorBuilder Constructor, FieldBuilder Template, + FieldBuilder Factory, FieldBuilder Prototype, MethodBuilder Create, MethodBuilder Construct, + MethodBuilder ReadProperty, MethodBuilder Invoke); + + private Declarations? _declarations; + private Declarations Required => _declarations + ?? throw new InvalidOperationException("Class-definition metadata has not been declared."); + public bool IsComplete { get; private set; } + public TypeBuilder Type => Required.Type; + public Type InstanceInterface => Required.InstanceInterface; + public MethodInfo GetDefinition => Required.GetDefinition; + public ConstructorBuilder Constructor => Required.Constructor; + public FieldBuilder Template => Required.Template; + public FieldBuilder Factory => Required.Factory; + public FieldBuilder Prototype => Required.Prototype; + public MethodBuilder Create => Required.Create; + public MethodBuilder Construct => Required.Construct; + public MethodBuilder ReadProperty => Required.ReadProperty; + public MethodBuilder Invoke => Required.Invoke; + + internal void Declare(Declarations declarations) + { + ArgumentNullException.ThrowIfNull(declarations); + if (_declarations is not null || IsComplete) + throw new InvalidOperationException("Class-definition metadata has already been declared."); + if (new MemberInfo[] { declarations.Constructor, declarations.Template, declarations.Factory, + declarations.Prototype, declarations.Create, declarations.Construct, + declarations.ReadProperty, declarations.Invoke } + .Any(member => member.DeclaringType != declarations.Type) + || declarations.GetDefinition.DeclaringType != declarations.InstanceInterface + || declarations.InstanceInterface.Module != declarations.Type.Module) + throw new InvalidOperationException("Class-definition handles must belong to their declared owner."); + _declarations = declarations; + } + + internal void CompleteEmission() + { + var declarations = Required; + if (IsComplete) throw new InvalidOperationException("Class-definition emission is already complete."); + if (new[] { declarations.Create, declarations.Construct, declarations.ReadProperty, declarations.Invoke } + .Any(method => method.GetILGenerator().ILOffset == 0)) + throw new InvalidOperationException("Class-definition method bodies have not all been emitted."); + IsComplete = true; + } +} diff --git a/src/SharpTS/Compilation/EmittedRuntime.cs b/src/SharpTS/Compilation/EmittedRuntime.cs index 3d71a1099..fa43b3bcd 100644 --- a/src/SharpTS/Compilation/EmittedRuntime.cs +++ b/src/SharpTS/Compilation/EmittedRuntime.cs @@ -146,6 +146,7 @@ public EmittedStableIteratorResultRuntime RequireStableIteratorResults() => Stab /// Required user-class prototype declarations and registration for one compilation. public EmittedClassPrototypeRuntime ClassPrototypes { get; } = new(); + public EmittedClassDefinitionRuntime ClassDefinitions { get; } = new(); /// Required public Object descriptor operations for this compilation. diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs new file mode 100644 index 000000000..efd219b1b --- /dev/null +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs @@ -0,0 +1,86 @@ +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public abstract partial class ExpressionEmitterBase +{ + protected bool TryEmitClassDefinitionSelf(string name) + { + if (!Ctx.IsInstanceMethod) return false; + var expression = Ctx.CurrentClassExpr ?? Ctx.ClassExprBuilders? + .FirstOrDefault(entry => ReferenceEquals(entry.Value, Ctx.CurrentClassBuilder)).Key; + if (expression?.Name?.Lexeme != name) return false; + EmitThis(); + EnsureBoxed(); + IL.Emit(OpCodes.Castclass, Ctx.Runtime!.ClassDefinitions.InstanceInterface); + IL.Emit(OpCodes.Callvirt, Ctx.Runtime.ClassDefinitions.GetDefinition); + SetStackUnknown(); + return true; + } + + protected void EmitGuestClassDefinition(Expr.ClassExpr expression) + { + if (Ctx.ClassExprFactories?.TryGetValue(expression, out var factory) != true) + throw new InvalidOperationException("Class expression factory has not been declared."); + var parent = IL.DeclareLocal(Types.Object); + if (expression.SuperclassExpr is { } heritage) + { + EmitClassHeritageValue(heritage, expression.Name?.Lexeme); + EnsureBoxed(); + } + else IL.Emit(OpCodes.Ldnull); + IL.Emit(OpCodes.Stloc, parent); + if (Ctx.ClassExprCaptureFields?.TryGetValue(expression, out var captures) == true) + { + foreach (var (name, field) in captures) + { + EmitVariable(new Expr.Variable(new Token(TokenType.IDENTIFIER, name, null, 0))); + EnsureBoxed(); + IL.Emit(OpCodes.Stsfld, field); + } + } + IL.Emit(OpCodes.Ldtoken, factory!.Template); + IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassInitialization.RunDefinition); + if (Ctx.DeferredClassDefinitions?.TryGet(expression, out var deferred) == true) + EmitDefinitionComputedKeys(deferred.Registrar, deferred.Keys); + IL.Emit(OpCodes.Ldtoken, factory.Template); + IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); + IL.Emit(OpCodes.Ldnull); + IL.Emit(OpCodes.Ldftn, factory.Method); + IL.Emit(OpCodes.Newobj, Types.GetConstructor(typeof(Func), Types.Object, Types.IntPtr)); + IL.Emit(OpCodes.Ldstr, factory.Name); + IL.Emit(OpCodes.Ldc_R8, (double)factory.Length); + IL.Emit(OpCodes.Ldloc, parent); + IL.Emit(OpCodes.Call, Ctx.Runtime.ClassDefinitions.Create); + SetStackUnknown(); + } + + protected void EmitDefinitionComputedKeys(MethodBuilder method, IReadOnlyList keys) + { + var values = new List(keys.Count); + foreach (var key in keys) + { + EmitExpression(key); + EnsureBoxed(); + var isSymbol = IL.DefineLabel(); + IL.Emit(OpCodes.Dup); + IL.Emit(OpCodes.Isinst, Ctx.Runtime!.Symbols.Type); + IL.Emit(OpCodes.Brtrue, isSymbol); + IL.Emit(OpCodes.Call, Ctx.Runtime.StringCoercion.ToJsString); + IL.MarkLabel(isSymbol); + values.Add(_helpers.SpillStoreObject()); + } + IL.Emit(OpCodes.Ldc_I4, values.Count); + IL.Emit(OpCodes.Newarr, Types.Object); + for (int i = 0; i < values.Count; i++) + { + IL.Emit(OpCodes.Dup); + IL.Emit(OpCodes.Ldc_I4, i); + IL.Emit(OpCodes.Ldloc, values[i]); + IL.Emit(OpCodes.Stelem_Ref); + } + IL.Emit(OpCodes.Call, method); + } +} diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.cs index 40001008e..65b6296ee 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.cs @@ -820,6 +820,7 @@ protected virtual void EmitSuper(Expr.Super s) /// protected virtual void EmitVariable(Expr.Variable v) { + if (TryEmitClassDefinitionSelf(v.Name.Lexeme)) return; string name = v.Name.Lexeme; if (TryEmitEnumInitializerMember(name)) @@ -975,9 +976,9 @@ protected virtual void EmitSet(Expr.Set s) // Handle static field assignment: Class.field = value if (s.Object is Expr.Variable classVar && - Ctx.Classes.TryGetValue(Ctx.ResolveClassName(classVar.Name.Lexeme), out var staticSetClassBuilder)) + Ctx.Classes.TryGetValue(Ctx.ResolveClassName(Ctx.TypeMap?.Get(classVar), classVar.Name.Lexeme), out var staticSetClassBuilder)) { - string resolvedClassName = Ctx.ResolveClassName(classVar.Name.Lexeme); + string resolvedClassName = Ctx.ResolveClassName(Ctx.TypeMap?.Get(classVar), classVar.Name.Lexeme); if (Ctx.ClassRegistry!.TryGetOwnCallableStaticField(resolvedClassName, s.Name.Lexeme, staticSetClassBuilder, out var staticField)) { EmitExpression(s.Value); @@ -1653,6 +1654,15 @@ protected virtual void EmitNew(Expr.New n) /// protected virtual void EmitUserClassNew(List namespaceParts, string className, Expr.New n) { + if (n.Callee is Expr.Variable variable && Ctx.VarToClassExpr?.ContainsKey(variable.Name.Lexeme) == true) + { + EmitExpression(n.Callee); + EnsureBoxed(); + EmitArgsArrayWithSpread(n.Arguments); + IL.Emit(OpCodes.Call, Ctx.Runtime!.DynamicConstruction.Value); + SetStackUnknown(); + return; + } string resolvedClassName = ResolveClassNameForNew(namespaceParts, className); // Check class expression constructors (e.g., const C = class { }; new C()) @@ -2381,9 +2391,9 @@ protected virtual bool TryEmitGlobalStore(string name) protected virtual bool TryEmitStaticFieldAccess(Expr.Get g) { if (g.Object is Expr.Variable classVar && - Ctx.Classes.TryGetValue(Ctx.ResolveClassName(classVar.Name.Lexeme), out var staticFieldClassBuilder)) + Ctx.Classes.TryGetValue(Ctx.ResolveClassName(Ctx.TypeMap?.Get(classVar), classVar.Name.Lexeme), out var staticFieldClassBuilder)) { - string resolvedClassName = Ctx.ResolveClassName(classVar.Name.Lexeme); + string resolvedClassName = Ctx.ResolveClassName(Ctx.TypeMap?.Get(classVar), classVar.Name.Lexeme); if (Ctx.ClassRegistry!.TryGetCallableStaticField(resolvedClassName, g.Name.Lexeme, staticFieldClassBuilder, out var staticField)) { EmitStaticFieldLoadWithShadow(resolvedClassName, staticFieldClassBuilder, g.Name.Lexeme, staticField!); @@ -2530,7 +2540,7 @@ protected void EmitClassHeritageExpression(Expr? heritage, string? innerClassNam IL.Emit(OpCodes.Pop); } - private void EmitClassHeritageValue(Expr heritage, string? innerClassName) + protected void EmitClassHeritageValue(Expr heritage, string? innerClassName) { switch (heritage) { diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs index fb8c04604..015c379b0 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs @@ -83,6 +83,18 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) _classes.GenericParameters.Declare(typeBuilder, classGenericParams ?? []); + EmitTypeDefinitions.AddInterfaceImplementation(typeBuilder, _runtime.ClassDefinitions.InstanceInterface); + var definitionField = typeBuilder.DefineField("$definition", _types.Object, FieldAttributes.Private); + _classExprs.DefinitionFields[classExpr] = definitionField; + var definitionGetter = typeBuilder.DefineMethod("$GetClassDefinition", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + _types.Object, Type.EmptyTypes); + typeBuilder.DefineMethodOverride(definitionGetter, _runtime.ClassDefinitions.GetDefinition); + var definitionIl = definitionGetter.GetILGenerator(); + definitionIl.Emit(OpCodes.Ldarg_0); + definitionIl.Emit(OpCodes.Ldfld, EmitterTypeHelpers.SelfFieldReference(definitionField)); + definitionIl.Emit(OpCodes.Ret); + // NOW resolve superclass (may use our generic params for type arguments) Type? baseType = null; if (superclassName != null) @@ -346,7 +358,7 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) var ctorBuilder = typeBuilder.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard, - ctorParamTypes + [.. ctorParamTypes, _runtime.ClassDefinitions.Type] ); if (constructor == null && superclassName == "Array") MarkJsVariadicConstructor(ctorBuilder); @@ -354,6 +366,7 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) _classes.Constructors[className] = ctorBuilder; RegisterArgumentsCapturingMethod(ctorBuilder, constructor?.Body); DefineClassPrototypeConstructor(typeBuilder); + DefineClassExpressionFactory(classExpr, typeBuilder, ctorBuilder, ctorParamTypes); // Define static methods (computed symbol-keyed methods are handled by // DefineClassExpressionSymbolMethods below, like the class-declaration path). @@ -696,6 +709,9 @@ private void EmitClassExpressionConstructor(Expr.ClassExpr classExpr, TypeBuilde var constructor = classExpr.Methods.FirstOrDefault(m => !m.IsStatic && m.Name.Lexeme == "constructor" && m.Body != null); var il = ctorBuilder.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg, ctorBuilder.GetParameters().Length); + il.Emit(OpCodes.Stfld, EmitterTypeHelpers.SelfFieldReference(_classExprs.DefinitionFields[classExpr])); var ctx = CreateClassExpressionContext(il, classExpr, typeBuilder, fieldsField, ctorBuilder); ctx.IsInstanceMethod = true; @@ -800,7 +816,7 @@ private void EmitClassExpressionConstructor(Expr.ClassExpr classExpr, TypeBuilde ctx.DefineParameter(constructor.Parameters[i].Name.Lexeme, i + 1, paramType); } - var constructorParamTypes = ctorBuilder.GetParameters().Select(p => p.ParameterType).ToArray(); + var constructorParamTypes = ctorBuilder.GetParameters().SkipLast(1).Select(p => p.ParameterType).ToArray(); EmitFunctionEnvironmentPrologue( il, ctx, diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs new file mode 100644 index 000000000..72592c0dc --- /dev/null +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs @@ -0,0 +1,60 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public partial class ILCompiler +{ + private void DefineClassExpressionFactory(Expr.ClassExpr expression, TypeBuilder builder, + ConstructorBuilder constructor, Type[] parameterTypes) + { + Type template = builder; + ConstructorInfo target = constructor; + if (builder.IsGenericTypeDefinition) + { + var erased = expression.TypeParams!.Select(parameter => parameter.Constraint is { } constraint + ? ResolveConstraintType(constraint) : _types.Object).ToArray(); + template = EmitGenerics.MakeGenericType(builder, erased); + target = EmitterTypeHelpers.ResolveConstructor(template, constructor); + } + var method = _programType.DefineMethod("$Create_" + _classExprs.Names[expression], + MethodAttributes.Public | MethodAttributes.Static, _types.Object, [_types.ObjectArray, _types.Object]); + var il = method.GetILGenerator(); + if (_functions.MethodsCapturingArguments.Contains(constructor)) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Stsfld, _runtime.Arguments.CurrentField); + } + for (int i = 0; i < parameterTypes.Length; i++) + { + if (parameterTypes[i] == _types.ObjectArray) + { + il.Emit(OpCodes.Ldarg_0); + continue; + } + var missing = il.DefineLabel(); + var ready = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ble, missing); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Br, ready); + il.MarkLabel(missing); + il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + il.MarkLabel(ready); + } + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, _runtime.ClassDefinitions.Type); + il.Emit(OpCodes.Newobj, target); + il.Emit(OpCodes.Ret); + string name = expression.Name?.Lexeme + ?? _classExprs.VarToClassExpr.FirstOrDefault(entry => ReferenceEquals(entry.Value, expression)).Key + ?? ""; + _classExprs.Factories[expression] = new(method, template, name, GetClassConstructorLength(expression.Methods)); + } +} diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs b/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs index 5033061ff..2d721fb0e 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs @@ -1004,8 +1004,7 @@ private void EmitClassPrototypeFieldFallback(ILGenerator il) var skipLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Object, "GetType")); - il.Emit(OpCodes.Call, _runtime.ClassPrototypes.Get); + il.Emit(OpCodes.Call, _runtime.ObjectPrototypes.GetPrototypeOf); il.Emit(OpCodes.Stloc, prototypeLocal); il.Emit(OpCodes.Ldarg_0); diff --git a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs index c2fa2fdbb..ae2ca5bc4 100644 --- a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs +++ b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs @@ -92,6 +92,7 @@ private CompilationContext CreateBaseCompilationContext(ILGenerator il, MethodBa ImportedNames = _importedNames, TopLevelNumericConstantValues = _topLevelNumericConstantValues, ClassExprBuilders = _classExprs.Builders, + ClassExprFactories = _classExprs.Factories, ClassExprStaticMethods = _classExprs.StaticMethods, ClassExprCaptureFields = _classExprs.CaptureFields, DeferredClassDefinitions = _classes.DeferredDefinitions, @@ -274,6 +275,7 @@ private CompilationContext CreateNestedAsyncArrowContext(ILGenerator il, Compila EnumToModule = parentCtx.EnumToModule, TypeEmitterRegistry = parentCtx.TypeEmitterRegistry, ClassExprBuilders = parentCtx.ClassExprBuilders, + ClassExprFactories = parentCtx.ClassExprFactories, ClassExprCaptureFields = parentCtx.ClassExprCaptureFields, BlockScopedClassBuilders = parentCtx.BlockScopedClassBuilders, DeferredClassDefinitions = parentCtx.DeferredClassDefinitions, diff --git a/src/SharpTS/Compilation/ILCompiler.State.cs b/src/SharpTS/Compilation/ILCompiler.State.cs index e981f9971..984223b1f 100644 --- a/src/SharpTS/Compilation/ILCompiler.State.cs +++ b/src/SharpTS/Compilation/ILCompiler.State.cs @@ -415,6 +415,8 @@ private sealed class ClassExpressionCompilationState public Dictionary> Getters { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary> Setters { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary Constructors { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary DefinitionFields { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary Factories { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary Superclass { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary EnclosingClass { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary> CaptureFields { get; } = new(ReferenceEqualityComparer.Instance); diff --git a/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs b/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs index 8ff1d7072..ae4e24349 100644 --- a/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs +++ b/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs @@ -25,6 +25,21 @@ protected override void EmitNew(Expr.New n) var (namespaceParts, className) = ExtractQualifiedNameFromCallee(n.Callee); + if (n.Callee is Expr.Variable expressionVariable + && _ctx.VarToClassExpr?.ContainsKey(expressionVariable.Name.Lexeme) == true + && _resolver.HasVariable(expressionVariable.Name.Lexeme)) + { + EmitExpression(n.Callee); + EnsureBoxed(); + var definition = IL.DeclareLocal(_ctx.Types.Object); + IL.Emit(OpCodes.Stloc, definition); + IL.Emit(OpCodes.Ldloc, definition); + EmitArgsArrayWithSpread(n.Arguments); + IL.Emit(OpCodes.Call, _ctx.Runtime!.DynamicConstruction.Value); + SetStackUnknown(); + return; + } + if (n.Callee is Expr.Variable constructorVariable && _ctx.TypeMap?.Get(n.Callee) is SharpTS.TypeSystem.TypeInfo.GenericClass && _resolver.HasVariable(constructorVariable.Name.Lexeme)) diff --git a/src/SharpTS/Compilation/ILEmitter.Expressions.cs b/src/SharpTS/Compilation/ILEmitter.Expressions.cs index cccf5ff86..884f9aa8a 100644 --- a/src/SharpTS/Compilation/ILEmitter.Expressions.cs +++ b/src/SharpTS/Compilation/ILEmitter.Expressions.cs @@ -47,6 +47,7 @@ protected override void EmitVariable(Expr.Variable v) if (TryEmitDefaultParameterTdz(name)) return; + if (TryEmitClassDefinitionSelf(name)) return; if (_ctx.LexicalInitializerTdzName == name) { @@ -1129,45 +1130,7 @@ private void EmitStringRawTaggedTemplate(Expr.TaggedTemplateLiteral ttl) // hoist-aware $RegExp emission now lives in the shared base so regex literals // compile identically in plain functions, arrows, and all state-machine bodies. - protected override void EmitClassExpression(Expr.ClassExpr ce) - { - // Class expressions evaluate to the Type object at runtime. - // The type has been pre-defined during collection phase. - if (_ctx.ClassExprBuilders != null && _ctx.ClassExprBuilders.TryGetValue(ce, out var typeBuilder)) - { - EmitClassHeritageExpression(ce.SuperclassExpr, ce.Name?.Lexeme); - - if (_ctx.ClassExprCaptureFields?.TryGetValue(ce, out var captureFields) == true) - { - foreach (var (name, field) in captureFields) - { - EmitVariable(new Expr.Variable( - new Token(TokenType.IDENTIFIER, name, null, 0))); - EnsureBoxed(); - IL.Emit(OpCodes.Stsfld, field); - } - } - - // Class-expression evaluation always creates its constructor prototype; - // static elements and computed keys share the same emitted .cctor. - IL.Emit(OpCodes.Ldtoken, typeBuilder); - IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); - IL.Emit(OpCodes.Call, _ctx.Runtime!.ClassInitialization.RunDefinition); - 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); - IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); - SetStackUnknown(); - } - else - { - // Fallback: push null (should not happen if collection worked) - IL.Emit(OpCodes.Ldnull); - SetStackUnknown(); - } - } + protected override void EmitClassExpression(Expr.ClassExpr ce) => EmitGuestClassDefinition(ce); protected override void EmitDelete(Expr.Delete del) { diff --git a/src/SharpTS/Compilation/ParameterTypeResolver.cs b/src/SharpTS/Compilation/ParameterTypeResolver.cs index 71eeb9468..8cf5e3a08 100644 --- a/src/SharpTS/Compilation/ParameterTypeResolver.cs +++ b/src/SharpTS/Compilation/ParameterTypeResolver.cs @@ -185,6 +185,8 @@ private static Type WidenIfUndefinedReachableParam(Type mapped, Stmt.Parameter p /// private static Type CoerceParamSlotType(Type mapped, TSTypeInfo source, TypeMapper typeMapper) { + if (mapped == typeof(Delegate) || mapped.IsSubclassOf(typeof(Delegate))) + return typeof(object); if (typeMapper.IsDynamicRuntimeType(mapped)) return typeof(object); if (Nullable.GetUnderlyingType(mapped) != null) diff --git a/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs new file mode 100644 index 000000000..c760fb030 --- /dev/null +++ b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs @@ -0,0 +1,268 @@ +using System.Reflection; +using System.Reflection.Emit; + +namespace SharpTS.Compilation; + +public partial class RuntimeEmitter +{ + private void DefineClassDefinitionTypes(ModuleBuilder module, EmittedClassDefinitionRuntime definitions) + { + var contract = module.DefineType("$IClassDefinitionInstance", + TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract); + var getter = contract.DefineMethod("$GetClassDefinition", + MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual, + _types.Object, Type.EmptyTypes); + var instanceInterface = contract.CreateType()!; + var type = module.DefineType("$ClassDefinition", TypeAttributes.Public | TypeAttributes.Sealed); + var constructor = type.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes); + var il = constructor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.GetDefaultConstructor(_types.Object)); + il.Emit(OpCodes.Ret); + var factoryType = typeof(Func); + var template = type.DefineField("Template", _types.Type, FieldAttributes.Assembly); + var factory = type.DefineField("Factory", factoryType, FieldAttributes.Assembly); + var prototype = type.DefineField("Prototype", _types.Object, FieldAttributes.Assembly); + var create = type.DefineMethod("Create", MethodAttributes.Public | MethodAttributes.Static, + type, [_types.Type, factoryType, _types.String, _types.Double, _types.Object]); + var construct = type.DefineMethod("Construct", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [type, _types.ObjectArray]); + var read = type.DefineMethod("ReadProperty", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [type, _types.String]); + var invoke = type.DefineMethod("Invoke", MethodAttributes.Public, _types.Object, [_types.ObjectArray]); + definitions.Declare(new(type, instanceInterface, instanceInterface.GetMethod(getter.Name)!, + constructor, template, factory, prototype, create, construct, read, invoke)); + } + + private void EmitClassDefinitionBodies(EmittedRuntime runtime) + { + var definitions = runtime.ClassDefinitions; + var storage = runtime.DescriptorStorage; + var il = definitions.Create.GetILGenerator(); + var definition = il.DeclareLocal(definitions.Type); + var prototype = il.DeclareLocal(_types.Object); + var templatePrototype = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Newobj, definitions.Constructor); + il.Emit(OpCodes.Stloc, definition); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Stfld, definitions.Template); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Stfld, definitions.Factory); + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.DictionaryStringObject)); + il.Emit(OpCodes.Newobj, runtime.ObjectStorage.Constructor); + il.Emit(OpCodes.Stloc, prototype); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Stfld, definitions.Prototype); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, runtime.ClassPrototypes.Get); + il.Emit(OpCodes.Stloc, templatePrototype); + + // Copy descriptors into a fresh prototype; descriptor flags must not be + // shared with another evaluation of the same syntax node. + var keys = il.DeclareLocal(_types.ListOfObject); + il.Emit(OpCodes.Ldloc, templatePrototype); + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.DictionaryStringObject)); + il.Emit(OpCodes.Call, storage.GetAllExtraKeys); + il.Emit(OpCodes.Stloc, keys); + var index = il.DeclareLocal(_types.Int32); + var key = il.DeclareLocal(_types.String); + var original = il.DeclareLocal(storage.DescriptorType); + var copy = il.DeclareLocal(storage.DescriptorType); + var loop = il.DefineLabel(); + var next = il.DefineLabel(); + var done = il.DefineLabel(); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, index); + il.MarkLabel(loop); + il.Emit(OpCodes.Ldloc, index); + il.Emit(OpCodes.Ldloc, keys); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.ListOfObject, "Count")!.GetGetMethod()!); + il.Emit(OpCodes.Bge, done); + il.Emit(OpCodes.Ldloc, keys); + il.Emit(OpCodes.Ldloc, index); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ListOfObject, "get_Item", _types.Int32)); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Stloc, key); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Brfalse, next); + il.Emit(OpCodes.Ldloc, templatePrototype); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Call, storage.GetPropertyDescriptor); + il.Emit(OpCodes.Stloc, original); + il.Emit(OpCodes.Newobj, storage.DescriptorConstructor); + il.Emit(OpCodes.Stloc, copy); + foreach (var property in new[] { storage.DescriptorValue, storage.DescriptorGetter, + storage.DescriptorSetter, storage.DescriptorWritable, storage.DescriptorEnumerable, + storage.DescriptorConfigurable }) + { + il.Emit(OpCodes.Ldloc, copy); + il.Emit(OpCodes.Ldloc, original); + il.Emit(OpCodes.Callvirt, property.GetGetMethod()!); + il.Emit(OpCodes.Callvirt, property.GetSetMethod()!); + } + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Ldloc, copy); + il.Emit(OpCodes.Call, storage.DefineProperty); + il.Emit(OpCodes.Pop); + il.MarkLabel(next); + il.Emit(OpCodes.Ldloc, index); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, index); + il.Emit(OpCodes.Br, loop); + il.MarkLabel(done); + + var basePrototype = il.DeclareLocal(_types.Object); + var noParent = il.DefineLabel(); + var haveParent = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_S, (byte)4); + il.Emit(OpCodes.Brfalse, noParent); + il.Emit(OpCodes.Ldarg_S, (byte)4); + il.Emit(OpCodes.Ldstr, "prototype"); + il.Emit(OpCodes.Call, runtime.ObjectRead.Property); + il.Emit(OpCodes.Stloc, basePrototype); + il.Emit(OpCodes.Ldloc, basePrototype); + il.Emit(OpCodes.Brfalse, noParent); + il.Emit(OpCodes.Ldloc, basePrototype); + il.Emit(OpCodes.Isinst, runtime.Sentinels.UndefinedType); + il.Emit(OpCodes.Brtrue, noParent); + il.Emit(OpCodes.Br, haveParent); + il.MarkLabel(noParent); + il.Emit(OpCodes.Ldloc, templatePrototype); + il.Emit(OpCodes.Call, runtime.ObjectPrototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, basePrototype); + il.MarkLabel(haveParent); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Ldloc, basePrototype); + il.Emit(OpCodes.Call, storage.SetPrototype); + EmitDataDescriptor(prototype, "constructor", () => il.Emit(OpCodes.Ldloc, definition), true); + EmitDataDescriptor(definition, "prototype", () => il.Emit(OpCodes.Ldloc, prototype), false, false); + EmitDataDescriptor(definition, "name", () => il.Emit(OpCodes.Ldarg_2), false); + EmitDataDescriptor(definition, "length", () => + { + il.Emit(OpCodes.Ldarg_3); + il.Emit(OpCodes.Box, _types.Double); + }, false); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldarg_S, (byte)4); + var parentPresent = il.DefineLabel(); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue, parentPresent); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldsfld, runtime.FunctionPrototypes.Prototype); + il.MarkLabel(parentPresent); + il.Emit(OpCodes.Call, storage.SetPrototype); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ret); + + void EmitDataDescriptor(LocalBuilder owner, string name, Action value, bool writable, bool configurable = true) + { + var descriptor = il.DeclareLocal(storage.DescriptorType); + il.Emit(OpCodes.Newobj, storage.DescriptorConstructor); + il.Emit(OpCodes.Stloc, descriptor); + il.Emit(OpCodes.Ldloc, descriptor); + value(); + il.Emit(OpCodes.Callvirt, storage.DescriptorValue.GetSetMethod()!); + foreach (var (property, flag) in new[] { (storage.DescriptorWritable, writable), + (storage.DescriptorEnumerable, false), (storage.DescriptorConfigurable, configurable) }) + { + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(flag ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Callvirt, property.GetSetMethod()!); + } + il.Emit(OpCodes.Ldloc, owner); + il.Emit(OpCodes.Ldstr, name); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Call, storage.DefineProperty); + il.Emit(OpCodes.Pop); + } + + il = definitions.Construct.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, definitions.Factory); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, _types.GetMethod(typeof(Func), "Invoke", _types.ObjectArray, _types.Object)); + il.Emit(OpCodes.Ret); + + il = definitions.ReadProperty.GetILGenerator(); + var ownDescriptor = il.DeclareLocal(storage.DescriptorType); + var inheritedProperty = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, storage.GetPropertyDescriptor); + il.Emit(OpCodes.Stloc, ownDescriptor); + il.Emit(OpCodes.Ldloc, ownDescriptor); + il.Emit(OpCodes.Brfalse, inheritedProperty); + var dataProperty = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, ownDescriptor); + il.Emit(OpCodes.Callvirt, storage.DescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Brfalse, dataProperty); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, ownDescriptor); + il.Emit(OpCodes.Callvirt, storage.DescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Call, runtime.Invocation.Method); + il.Emit(OpCodes.Ret); + il.MarkLabel(dataProperty); + il.Emit(OpCodes.Ldloc, ownDescriptor); + il.Emit(OpCodes.Callvirt, storage.DescriptorValue.GetGetMethod()!); + il.Emit(OpCodes.Ret); + il.MarkLabel(inheritedProperty); + var ordinary = il.DefineLabel(); + foreach (var name in new[] { "bind", "call", "apply", "name", "length", "prototype" }) + { + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, name); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brtrue, ordinary); + } + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, definitions.Template); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.ObjectRead.Property); + il.Emit(OpCodes.Ret); + il.MarkLabel(ordinary); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.FunctionIntrospection.GetProperty); + il.Emit(OpCodes.Ret); + + il = definitions.Invoke.GetILGenerator(); + GuestErrorEmitter.ThrowError(il, runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor, + "Class constructor cannot be invoked without 'new'"); + definitions.Type.CreateType(); + definitions.CompleteEmission(); + } + + private void EmitClassDefinitionTemplateReceiver(ILGenerator il, EmittedClassDefinitionRuntime definitions) + { + var ordinary = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, definitions.Type); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, ordinary); + il.Emit(OpCodes.Ldfld, definitions.Template); + il.Emit(OpCodes.Starg_S, (byte)0); + var ready = il.DefineLabel(); + il.Emit(OpCodes.Br, ready); + il.MarkLabel(ordinary); + il.Emit(OpCodes.Pop); + il.MarkLabel(ready); + } + + private void EmitClassDefinitionSymbolReceiver(ILGenerator il, EmittedClassDefinitionRuntime definitions, Type symbolType) + { + var ordinary = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, symbolType); + il.Emit(OpCodes.Brfalse, ordinary); + EmitClassDefinitionTemplateReceiver(il, definitions); + il.MarkLabel(ordinary); + } +} diff --git a/src/SharpTS/Compilation/RuntimeEmitter.DynamicConstruct.cs b/src/SharpTS/Compilation/RuntimeEmitter.DynamicConstruct.cs index 1529b36ac..b4d7ad990 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.DynamicConstruct.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.DynamicConstruct.cs @@ -6,6 +6,7 @@ namespace SharpTS.Compilation; public partial class RuntimeEmitter { private readonly record struct ConstructDynamicValueInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedBoxedPrimitiveRuntime BoxedPrimitives, EmittedErrorRuntime Errors, EmittedFunctionConstructionRuntime FunctionConstruction, @@ -43,6 +44,17 @@ ConstructDynamicValueInputs inputs var il = method.GetILGenerator(); var throwLabel = il.DefineLabel(); + var notClassDefinition = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, notClassDefinition); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, inputs.ClassDefinitions.Construct); + il.Emit(OpCodes.Ret); + il.MarkLabel(notClassDefinition); + il.Emit(OpCodes.Pop); + // null / $Undefined → TypeError il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Brfalse, throwLabel); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Functions.NewOp.cs b/src/SharpTS/Compilation/RuntimeEmitter.Functions.NewOp.cs index 784001e36..3a23cc24e 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Functions.NewOp.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Functions.NewOp.cs @@ -6,6 +6,7 @@ namespace SharpTS.Compilation; public partial class RuntimeEmitter { internal readonly record struct NewOnFunctionInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedDescriptorStorageRuntime DescriptorStorage, EmittedErrorRuntime Errors, EmittedFunctionBindingRuntime FunctionBindings, @@ -45,6 +46,17 @@ NewOnFunctionInputs inputs var il = method.GetILGenerator(); + var notClassDefinition = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, notClassDefinition); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, inputs.ClassDefinitions.Construct); + il.Emit(OpCodes.Ret); + il.MarkLabel(notClassDefinition); + il.Emit(OpCodes.Pop); + var newObjLocal = il.DeclareLocal(inputs.ObjectStorage.Type); var resultLocal = il.DeclareLocal(_types.Object); var prevThisLocal = il.DeclareLocal(_types.Object); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Functions.cs b/src/SharpTS/Compilation/RuntimeEmitter.Functions.cs index b759a809b..e0978fc79 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Functions.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Functions.cs @@ -33,6 +33,7 @@ EmittedInvocationRuntime Invocation ); private readonly record struct FunctionBindWrapperClassInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedFunctionValueRuntime FunctionValues, EmittedArrayOperationsRuntime ArrayOperations, EmittedMapRuntime? Map, @@ -1494,6 +1495,7 @@ void AcceptCallable(Type callableType) il.Emit(OpCodes.Brtrue, callableTargetLabel); } AcceptCallable(inputs.FunctionValues.Type); + AcceptCallable(inputs.ClassDefinitions.Type); AcceptCallable(functionBindings.BoundType); AcceptCallable(functionBindings.AnyType); AcceptCallable(functionBindings.BindType); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs index 0b46e76af..30d4b0d06 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs @@ -6,6 +6,7 @@ namespace SharpTS.Compilation; public partial class RuntimeEmitter { private readonly record struct SetIndexInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedArrayOperationsRuntime ArrayOperations, EmittedArrayStorageRuntime ArrayStorage, EmittedBufferRuntime? Buffer, @@ -33,6 +34,7 @@ Type UndefinedType ); private readonly record struct GetIndexInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedArrayOperationsRuntime ArrayOperations, EmittedArrayStorageRuntime ArrayStorage, TypeBuilder BoundTSFunctionType, @@ -118,6 +120,7 @@ private void EmitGetIndex(TypeBuilder typeBuilder, EmittedObjectReadRuntime obje objectRead.Index = method; var il = method.GetILGenerator(); + EmitClassDefinitionSymbolReceiver(il, inputs.ClassDefinitions, inputs.Symbols.Type); var arrayLabel = il.DefineLabel(); var stringLabel = il.DefineLabel(); var dictLabel = il.DefineLabel(); @@ -1191,6 +1194,7 @@ private void EmitSetIndex(TypeBuilder typeBuilder, EmittedObjectWriteRuntime obj objectWrite.Index = method; var il = method.GetILGenerator(); + EmitClassDefinitionSymbolReceiver(il, inputs.ClassDefinitions, inputs.Symbols.Type); var dictLabel = il.DefineLabel(); var dictStringKeyLabel = il.DefineLabel(); var dictNumericKeyLabel = il.DefineLabel(); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs index 33a2e4135..ee3d18ea1 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Properties.cs @@ -65,6 +65,7 @@ private LocalBuilder EmitStaticMemberLookupOwner(ILGenerator il, LocalBuilder ow } private readonly record struct GetPropertyInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedAbortRuntime? Abort, FieldBuilder ArgumentsLengthField, TypeBuilder ArgumentsType, @@ -1371,6 +1372,35 @@ void EmitBoundRuntimeMethod(MethodBuilder helper, string jsName, int jsLength) il.Emit(OpCodes.Ret); il.MarkLabel(notGlobalThisLabel); + var notDefinitionValue = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, notDefinitionValue); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, inputs.ClassDefinitions.ReadProperty); + il.Emit(OpCodes.Ret); + il.MarkLabel(notDefinitionValue); + il.Emit(OpCodes.Pop); + + var notDefinitionConstructor = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldstr, "constructor"); + il.Emit(OpCodes.Call, _types.GetMethod(_types.String, "op_Equality", _types.String, _types.String)); + il.Emit(OpCodes.Brfalse, notDefinitionConstructor); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.InstanceInterface); + il.Emit(OpCodes.Dup); + var noDefinitionReceiver = il.DefineLabel(); + il.Emit(OpCodes.Brfalse, noDefinitionReceiver); + il.Emit(OpCodes.Callvirt, inputs.ClassDefinitions.GetDefinition); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, noDefinitionReceiver); + il.Emit(OpCodes.Ret); + il.MarkLabel(noDefinitionReceiver); + il.Emit(OpCodes.Pop); + il.MarkLabel(notDefinitionConstructor); + // __proto__ accessor (ECMA-262 Annex B.2.2.1): obj.__proto__ delegates // to Object.getPrototypeOf(obj). All object types support this — the // accessor lives on Object.prototype, but intercepting here avoids diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Prototype.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Prototype.cs index ffa1b31e7..00c470095 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Prototype.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Prototype.cs @@ -28,6 +28,7 @@ Type UndefinedType ); private readonly record struct ObjectGetPrototypeOfInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedArrayOperationsRuntime ArrayOperations, EmittedBooleanRuntime Booleans, TypeBuilder BoundTSFunctionType, @@ -670,6 +671,19 @@ FieldBuilder prototypeStoreField // Also check local _prototypeStore table for backward compatibility il.MarkLabel(checkLocalTableLabel); + var notAssociatedInstance = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.InstanceInterface); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, notAssociatedInstance); + il.Emit(OpCodes.Callvirt, inputs.ClassDefinitions.GetDefinition); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, notAssociatedInstance); + il.Emit(OpCodes.Ldfld, inputs.ClassDefinitions.Prototype); + il.Emit(OpCodes.Ret); + il.MarkLabel(notAssociatedInstance); + il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldsfld, prototypeStoreField); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldloca, tempLocal); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs index f2f60a22d..5a56dc0a6 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs @@ -54,6 +54,7 @@ MethodBuilder SafeGetMethod ); private readonly record struct SetPropertyInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedAbortRuntime? Abort, FieldBuilder ArgumentsLengthField, TypeBuilder ArgumentsType, @@ -786,6 +787,7 @@ private void EmitSetProperty(EmittedObjectWriteRuntime objectWrite, SetPropertyI var method = (MethodBuilder)objectWrite.Property; var il = method.GetILGenerator(); + EmitClassDefinitionTemplateReceiver(il, inputs.ClassDefinitions); var nullLabel = il.DefineLabel(); var dictLabel = il.DefineLabel(); var tsObjectLabel = il.DefineLabel(); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Operators.cs b/src/SharpTS/Compilation/RuntimeEmitter.Operators.cs index aebba6093..39e50f7c7 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Operators.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Operators.cs @@ -9,6 +9,7 @@ namespace SharpTS.Compilation; public partial class RuntimeEmitter { private readonly record struct TypeOfInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedArrayOperationsRuntime ArrayOperations, TypeBuilder BoundAnyFunctionType, TypeBuilder BoundTSFunctionType, @@ -26,6 +27,7 @@ Type UndefinedType ); private readonly record struct InstanceOfInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedAbortRuntime? Abort, EmittedBoxedPrimitiveRuntime BoxedPrimitives, MethodBuilder GetFunctionMethod, @@ -553,6 +555,9 @@ private void EmitTypeOf(TypeBuilder typeBuilder, EmittedOperatorRuntime operator il.Emit(OpCodes.Brtrue, functionLabel); // System.Type => "function" + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Brtrue, functionLabel); // Compiled class references (e.g. `const f = Foo` where Foo is a class) are // emitted as Ldtoken + GetTypeFromHandle, which yields a System.Type. Node/JS // spec says classes are functions, so `typeof Foo === 'function'` must hold. @@ -687,6 +692,31 @@ private void EmitInstanceOf(TypeBuilder typeBuilder, EmittedOperatorRuntime oper il.MarkLabel(notDictRhsLabel); + var notDefinition = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Brfalse, notDefinition); + var definitionPrototype = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Ldfld, inputs.ClassDefinitions.Prototype); + il.Emit(OpCodes.Stloc, definitionPrototype); + var candidate = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Stloc, candidate); + var walk = il.DefineLabel(); + il.MarkLabel(walk); + il.Emit(OpCodes.Ldloc, candidate); + il.Emit(OpCodes.Call, inputs.ObjectPrototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, candidate); + il.Emit(OpCodes.Ldloc, candidate); + il.Emit(OpCodes.Brfalse, falseLabel); + il.Emit(OpCodes.Ldloc, candidate); + il.Emit(OpCodes.Ldloc, definitionPrototype); + il.Emit(OpCodes.Beq, trueLabel); + il.Emit(OpCodes.Br, walk); + il.MarkLabel(notDefinition); + // Per JS spec, `instance instanceof F` where F is a user function walks // instance's prototype chain looking for F.prototype. Compiled mode's // legacy InstanceOf used .NET IsAssignableFrom, which is type-system diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Reflect.cs b/src/SharpTS/Compilation/RuntimeEmitter.Reflect.cs index d068b3208..a03028e55 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Reflect.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Reflect.cs @@ -6,6 +6,7 @@ namespace SharpTS.Compilation; public partial class RuntimeEmitter { private readonly record struct IsConstructorInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedFunctionBindingRuntime FunctionBindings, EmittedFunctionValueRuntime FunctionValues, EmittedFunctionAttributesRuntime FunctionAttributes, @@ -1068,6 +1069,14 @@ IsConstructorInputs inputs il.Emit(OpCodes.Ret); il.MarkLabel(notTypeLabel); + var notDefinition = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Brfalse, notDefinition); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Ret); + il.MarkLabel(notDefinition); + // $TSFunction → check method's declaring type var notTSFunctionLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs index 5e5b495e5..08add3f50 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs @@ -768,6 +768,7 @@ void EmitLinkProto(FieldBuilder child) typeBuilder, runtime.Operators, new TypeOfInputs( + runtime.ClassDefinitions, runtime.ArrayOperations, runtime.FunctionBindings.AnyType, runtime.FunctionBindings.BoundType, @@ -919,6 +920,7 @@ void EmitLinkProto(FieldBuilder child) typeBuilder, runtime.Operators, new InstanceOfInputs( + runtime.ClassDefinitions, runtime.Abort, runtime.BoxedPrimitives, runtime.FunctionIntrospection.GetProperty, @@ -1128,6 +1130,7 @@ void EmitLinkProto(FieldBuilder child) typeBuilder, runtime.FunctionIntrospection, new IsConstructorInputs( + runtime.ClassDefinitions, runtime.FunctionBindings, runtime.FunctionValues, runtime.FunctionAttributes, @@ -1194,6 +1197,7 @@ void EmitLinkProto(FieldBuilder child) EmitGetProperty( runtime.ObjectRead, new GetPropertyInputs( + runtime.ClassDefinitions, runtime.Abort, runtime.Arguments.LengthField, runtime.Arguments.Type, @@ -1259,6 +1263,7 @@ void EmitLinkProto(FieldBuilder child) runtime.WeakSet ) ); + EmitClassDefinitionBodies(runtime); // UnwrapIfBoxed's body is filled after GetIndex below. In addition to // GetProperty/InvokeMethodValue it now uses indexed symbol lookup for // the @@toPrimitive hook. @@ -1321,6 +1326,7 @@ void EmitLinkProto(FieldBuilder child) EmitSetProperty( runtime.ObjectWrite, new SetPropertyInputs( + runtime.ClassDefinitions, runtime.Abort, runtime.Arguments.LengthField, runtime.Arguments.Type, @@ -1471,6 +1477,7 @@ void EmitLinkProto(FieldBuilder child) typeBuilder, runtime.ObjectRead, new GetIndexInputs( + runtime.ClassDefinitions, runtime.ArrayOperations, runtime.ArrayStorage, runtime.FunctionBindings.BoundType, @@ -1509,6 +1516,7 @@ void EmitLinkProto(FieldBuilder child) typeBuilder, runtime.ObjectWrite, new SetIndexInputs( + runtime.ClassDefinitions, runtime.ArrayOperations, runtime.ArrayStorage, runtime.Buffer, @@ -1985,6 +1993,7 @@ void EmitLinkProto(FieldBuilder child) runtime.ClassPrototypes, runtime.ObjectPrototypes, new ObjectGetPrototypeOfInputs( + runtime.ClassDefinitions, runtime.ArrayOperations, runtime.Booleans, runtime.FunctionBindings.BoundType, diff --git a/src/SharpTS/Compilation/RuntimeEmitter.cs b/src/SharpTS/Compilation/RuntimeEmitter.cs index aa0d8edcb..4c0bbaece 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.cs @@ -308,6 +308,7 @@ public EmittedRuntime EmitAll(ModuleBuilder moduleBuilder, RuntimeFeatureSet fea EmitHasFieldsInterface(moduleBuilder, runtime.ObjectFields); runtime.ObjectFields.CompleteEmission(); EmitCompactObjectRecordInterface(moduleBuilder, runtime.Records); + DefineClassDefinitionTypes(moduleBuilder, runtime.ClassDefinitions); // Emit $Object class for standalone object support // NOTE: Must stay in sync with SharpTS.Runtime.Types.SharpTSObject @@ -609,6 +610,7 @@ public EmittedRuntime EmitAll(ModuleBuilder moduleBuilder, RuntimeFeatureSet fea moduleBuilder, runtime.FunctionBindings, new FunctionBindWrapperClassInputs( + runtime.ClassDefinitions, runtime.FunctionValues, runtime.ArrayOperations, runtime.Map, @@ -698,6 +700,7 @@ public EmittedRuntime EmitAll(ModuleBuilder moduleBuilder, RuntimeFeatureSet fea runtime.RuntimeClass.Type, runtime.DynamicConstruction, new NewOnFunctionInputs( + runtime.ClassDefinitions, runtime.DescriptorStorage, runtime.Errors, runtime.FunctionBindings, @@ -716,6 +719,7 @@ public EmittedRuntime EmitAll(ModuleBuilder moduleBuilder, RuntimeFeatureSet fea runtime.RuntimeClass.Type, runtime.DynamicConstruction, new ConstructDynamicValueInputs( + runtime.ClassDefinitions, runtime.BoxedPrimitives, runtime.Errors, runtime.FunctionConstruction, diff --git a/src/SharpTS/Compilation/StatementEmitterBase.cs b/src/SharpTS/Compilation/StatementEmitterBase.cs index bef25f6bc..c00020c91 100644 --- a/src/SharpTS/Compilation/StatementEmitterBase.cs +++ b/src/SharpTS/Compilation/StatementEmitterBase.cs @@ -1867,58 +1867,13 @@ private void EmitStateMachineClassDeclaration(Stmt.Class classStmt) /// values to as an object array. /// protected void EmitDeferredComputedKeys(MethodBuilder method, IReadOnlyList keys) - { - var values = new List(keys.Count); - foreach (var key in keys) - { - EmitExpression(key); - EnsureBoxed(); - var isSymbol = IL.DefineLabel(); - IL.Emit(OpCodes.Dup); - IL.Emit(OpCodes.Isinst, Ctx.Runtime!.Symbols.Type); - IL.Emit(OpCodes.Brtrue, isSymbol); - IL.Emit(OpCodes.Call, Ctx.Runtime.StringCoercion.ToJsString); - IL.MarkLabel(isSymbol); - values.Add(_helpers.SpillStoreObject()); - } - - IL.Emit(OpCodes.Ldc_I4, values.Count); - IL.Emit(OpCodes.Newarr, Types.Object); - for (int i = 0; i < values.Count; i++) - { - IL.Emit(OpCodes.Dup); - IL.Emit(OpCodes.Ldc_I4, i); - IL.Emit(OpCodes.Ldloc, values[i]); - IL.Emit(OpCodes.Stelem_Ref); - } - IL.Emit(OpCodes.Call, method); - } + => EmitDefinitionComputedKeys(method, keys); /// /// Default implementation for class expressions. /// Loads the pre-defined TypeBuilder as a Type object at runtime. /// - protected override void EmitClassExpression(Expr.ClassExpr ce) - { - if (Ctx?.ClassExprBuilders != null && Ctx.ClassExprBuilders.TryGetValue(ce, out var typeBuilder)) - { - EmitClassHeritageExpression(ce.SuperclassExpr, ce.Name?.Lexeme); - IL.Emit(OpCodes.Ldtoken, typeBuilder); - IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); - IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassInitialization.RunDefinition); - 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(); - } - else - { - // Fallback: push null (should not happen if collection worked) - IL.Emit(OpCodes.Ldnull); - SetStackUnknown(); - } - } + protected override void EmitClassExpression(Expr.ClassExpr ce) => EmitGuestClassDefinition(ce); #endregion diff --git a/src/SharpTS/Execution/Interpreter.Calls.cs b/src/SharpTS/Execution/Interpreter.Calls.cs index 0e58ee8a1..98c2696ec 100644 --- a/src/SharpTS/Execution/Interpreter.Calls.cs +++ b/src/SharpTS/Execution/Interpreter.Calls.cs @@ -1252,7 +1252,7 @@ private object EvaluateInstanceof(object? left, object? right) SharpTSClass? current = instance.GetClass(); while (current != null) { - if (current.Name == targetClass.Name) return true; + if (ReferenceEquals(current, targetClass)) return true; current = current.Superclass; } return false; @@ -1266,7 +1266,7 @@ private object EvaluateInstanceof(object? left, object? right) SharpTSClass? current = subclassArray.Klass; while (current != null) { - if (current.Name == targetClass.Name) return true; + if (ReferenceEquals(current, targetClass)) return true; current = current.Superclass; } return false; @@ -1280,7 +1280,7 @@ private object EvaluateInstanceof(object? left, object? right) SharpTSClass? current = subclassPromise.Klass; while (current != null) { - if (current.Name == targetClass.Name) return true; + if (ReferenceEquals(current, targetClass)) return true; current = current.Superclass; } return false; diff --git a/src/SharpTS/Execution/Interpreter.ClassInitializers.cs b/src/SharpTS/Execution/Interpreter.ClassInitializers.cs new file mode 100644 index 000000000..669f7c2d7 --- /dev/null +++ b/src/SharpTS/Execution/Interpreter.ClassInitializers.cs @@ -0,0 +1,19 @@ +using SharpTS.Parsing; +using SharpTS.Runtime; +using SharpTS.Runtime.Types; + +namespace SharpTS.Execution; + +public partial class Interpreter +{ + internal object? EvaluateClassInitializer(SharpTSClass owner, object receiver, Expr expression) + { + var environment = new RuntimeEnvironment(owner.InitializerEnvironment ?? _environment) + { + PrivateClass = owner + }; + environment.Define("this", receiver); + if (owner.Superclass is { } parent) environment.Define("super", parent); + using (PushScope(environment)) return Evaluate(expression); + } +} diff --git a/src/SharpTS/Execution/Interpreter.Expressions.cs b/src/SharpTS/Execution/Interpreter.Expressions.cs index 6de7dbf39..0f9c8b664 100644 --- a/src/SharpTS/Execution/Interpreter.Expressions.cs +++ b/src/SharpTS/Execution/Interpreter.Expressions.cs @@ -2278,6 +2278,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon staticSetters: staticSetters.Count > 0 ? staticSetters : null); klass.BindPrivateMemberOwners(); + klass.InitializerEnvironment = classEnv; if (symbolAccessors != null) { diff --git a/src/SharpTS/Execution/Interpreter.Properties.cs b/src/SharpTS/Execution/Interpreter.Properties.cs index ef1168ddf..b60a8cad3 100644 --- a/src/SharpTS/Execution/Interpreter.Properties.cs +++ b/src/SharpTS/Execution/Interpreter.Properties.cs @@ -1624,6 +1624,8 @@ or BuiltInAsyncMethod // shadowed them with a static property (handled above). if (memberName == "name") return klass.Name; if (memberName == "length") return (double)klass.Arity(); + if (memberName is "bind" or "call" or "apply") + return FunctionBuiltIns.GetMember(klass, memberName); // Class constructors are function objects and therefore inherit the // ordinary Object.prototype methods through Function.prototype. diff --git a/src/SharpTS/Execution/Interpreter.Statements.cs b/src/SharpTS/Execution/Interpreter.Statements.cs index f2f548874..257e8ee84 100644 --- a/src/SharpTS/Execution/Interpreter.Statements.cs +++ b/src/SharpTS/Execution/Interpreter.Statements.cs @@ -2328,6 +2328,7 @@ private async ValueTask VisitClassCore(IEvaluationContext ctx, staticSetters.Count > 0 ? staticSetters : null); klass.BindPrivateMemberOwners(); + klass.InitializerEnvironment = _environment; if (symbolAccessors != null) { diff --git a/src/SharpTS/Runtime/Types/SharpTSClass.cs b/src/SharpTS/Runtime/Types/SharpTSClass.cs index f886b67a3..aa916512b 100644 --- a/src/SharpTS/Runtime/Types/SharpTSClass.cs +++ b/src/SharpTS/Runtime/Types/SharpTSClass.cs @@ -43,6 +43,7 @@ public class SharpTSClass( public string Name { get; } = name; public SharpTSClass? Superclass { get; } = superclass; public bool IsAbstract { get; } = isAbstract; + internal RuntimeEnvironment? InitializerEnvironment { get; set; } private readonly FrozenDictionary _methods = methods.ToFrozenDictionary(); private readonly FrozenDictionary _staticMethods = staticMethods.ToFrozenDictionary(); private readonly Dictionary _staticProperties = staticProperties; @@ -183,13 +184,13 @@ protected void InitializeInstanceFields(Interpreter interpreter, SharpTSInstance foreach (var field in _instanceFields) { object? value = field.Initializer != null - ? interpreter.Evaluate(field.Initializer) + ? interpreter.EvaluateClassInitializer(this, instance, field.Initializer) : field.ComputedKey != null ? SharpTSUndefined.Instance : null; // Check if this is a computed property name if (field.ComputedKey != null) { - object? key = interpreter.Evaluate(field.ComputedKey); + object? key = interpreter.EvaluateClassInitializer(this, instance, field.ComputedKey); if (key is SharpTSSymbol symbol) { // Symbol key - store in symbol fields @@ -515,7 +516,7 @@ protected void InitializePrivateFields(Interpreter interpreter, SharpTSInstance foreach (var field in _instancePrivateFields) { object? value = field.Initializer != null - ? interpreter.Evaluate(field.Initializer) + ? interpreter.EvaluateClassInitializer(this, instance, field.Initializer) : null; fields[field.Name.Lexeme] = value; } @@ -622,7 +623,7 @@ protected void InitializeAutoAccessors(Interpreter interpreter, SharpTSInstance foreach (var autoAccessor in _instanceAutoAccessors) { object? value = autoAccessor.Initializer != null - ? interpreter.Evaluate(autoAccessor.Initializer) + ? interpreter.EvaluateClassInitializer(this, instance, autoAccessor.Initializer) : null; storage[autoAccessor.Name.Lexeme] = value; } diff --git a/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs b/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs index e4d3a543d..a68d5ea05 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs @@ -2704,6 +2704,8 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) // Check method bodies TypeEnvironment classEnv = new(_environment); + if (classExpr.Name is { } innerName) + classEnv.Define(innerName.Lexeme, classExprResultType); if (classTypeParams != null) { for (int i = 0; i < classTypeParams.Count; i++) @@ -2739,7 +2741,11 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) { TypeEnvironment methodEnv; if (method.IsStatic) + { methodEnv = new TypeEnvironment(prevEnv); + if (classExpr.Name is { } staticInnerName) + methodEnv.Define(staticInnerName.Lexeme, classExprResultType); + } else methodEnv = new TypeEnvironment(_environment); diff --git a/tests/SharpTS.Tests/CompilerTests/EmittedClassDefinitionRuntimeTests.cs b/tests/SharpTS.Tests/CompilerTests/EmittedClassDefinitionRuntimeTests.cs new file mode 100644 index 000000000..061149014 --- /dev/null +++ b/tests/SharpTS.Tests/CompilerTests/EmittedClassDefinitionRuntimeTests.cs @@ -0,0 +1,52 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Compilation; +using SharpTS.Parsing; +using Xunit; + +namespace SharpTS.Tests.CompilerTests; + +public sealed class EmittedClassDefinitionRuntimeTests +{ + [Fact] + public void RequiredHandlesCannotBeReadOrCompletedBeforeDeclaration() + { + var owner = new EmittedRuntime().ClassDefinitions; + Assert.Throws(() => owner.Create); + Assert.Throws(owner.CompleteEmission); + Assert.False(owner.IsComplete); + Assert.NotSame(owner, new EmittedRuntime().ClassDefinitions); + Assert.Null(typeof(EmittedRuntime).GetProperty(nameof(EmittedRuntime.ClassDefinitions))!.SetMethod); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ReusedEmitterFinalizesFreshDefinitionOwnersAndVerifiableBodies(bool hosted) + { + var emitter = new RuntimeEmitter(TypeProvider.Runtime, emitHosted: hosted); + var owners = new HashSet(); + foreach (string source in new[] { "const n = 1;", "async function f() { await Promise.resolve(1); } f();" }) + { + var assembly = new PersistedAssemblyBuilder(new AssemblyName($"definitions_{Guid.NewGuid():N}"), typeof(object).Assembly); + var module = assembly.DefineDynamicModule("main"); + var features = new RuntimeFeatureDetector().Detect(new Parser(new Lexer(source).ScanTokens()).ParseOrThrow()); + var runtime = emitter.EmitAll(module, features); + var owner = runtime.ClassDefinitions; + Assert.True(owner.IsComplete); + Assert.True(owners.Add(owner)); + Assert.Same(module, owner.Create.Module); + Assert.Same(owner.Type, owner.Create.DeclaringType); + Assert.Throws(owner.CompleteEmission); + using var bytes = new MemoryStream(); + assembly.Save(bytes); + bytes.Position = 0; + using var verifier = new ILVerifier(extraProbeDirectories: [AppContext.BaseDirectory]); + Assert.Empty(verifier.Verify(bytes)); + var loaded = Assembly.Load(bytes.ToArray()); + Assert.DoesNotContain(loaded.GetReferencedAssemblies(), reference => reference.Name == "SharpTS"); + Assert.Equal(hosted, loaded.GetReferencedAssemblies().Any(reference => reference.Name == "SharpTS.Hosting.Abstractions")); + Assert.NotNull(loaded.GetType("$ClassDefinition")); + } + } +} diff --git a/tests/SharpTS.Tests/SharedTests/RepeatedClassDefinitionTests.cs b/tests/SharpTS.Tests/SharedTests/RepeatedClassDefinitionTests.cs new file mode 100644 index 000000000..d9a232497 --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/RepeatedClassDefinitionTests.cs @@ -0,0 +1,122 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class RepeatedClassDefinitionTests +{ + public static IEnumerable Cases() + { + yield return ["function", """ + function make() { return class Named {}; } + const First = make(), Second = make(); + const first = new First(), second = new Second(); + const fp = Object.getPrototypeOf(first), sp = Object.getPrototypeOf(second); + console.log(First === Second, fp === sp); + console.log(fp === First.prototype, sp === Second.prototype); + console.log(first.constructor === First, second.constructor === Second); + console.log(first instanceof First, first instanceof Second, second instanceof First, second instanceof Second); + console.log(First.name, Second.name, First.length, typeof First); + """, "false false\ntrue true\ntrue true\ntrue false false true\nNamed Named 0 function\n"]; + yield return ["generic", """ + function make() { return class Box { value: T; constructor(value: T) { this.value = value; } }; } + const First = make(), Second = make(); + const one = new First(7), two = new First("ok"), three = new Second(9); + const op = Object.getPrototypeOf(one), tp = Object.getPrototypeOf(two), hp = Object.getPrototypeOf(three); + console.log(op === tp, op === hp, one.constructor === First, two.constructor === First); + console.log(one.value, two.value, three.value, First.length); + """, "true false true true\n7 ok 9 1\n"]; + yield return ["field-initialization", """ + function make() { return class Named { + owner: any = this.constructor; + prototype: any = Object.getPrototypeOf(this); + }; } + const First = make(), Second = make(); + const first = new First(), second = new Second(); + console.log(first.owner === First, second.owner === Second); + console.log(first.prototype === First.prototype, second.prototype === Second.prototype); + """, "true true\ntrue true\n"]; + yield return ["loop-closure", """ + const constructors: any[] = []; + function outer() { return () => class Named {}; } + const make = outer(); + for (let i = 0; i < 3; i++) constructors.push(make()); + console.log(constructors[0] === constructors[1], constructors[1] === constructors[2]); + const first = new constructors[0](), last = new constructors[2](); + console.log(first.constructor === constructors[0], last.constructor === constructors[2]); + """, "false false\ntrue true\n"]; + yield return ["generator", """ + function* make() { for (let i = 0; i < 2; i++) { yield class Named {}; } } + const iterator = make(); + const First: any = iterator.next().value, Second: any = iterator.next().value; + console.log(First === Second); + const first = new First(), second = new Second(); + console.log(first.constructor === First, second.constructor === Second); + """, "false\ntrue true\n"]; + yield return ["async", """ + async function make() { await Promise.resolve(0); return class Named {}; } + async function run() { + const First = await make(), Second = await make(); + console.log(First === Second); + const first = new First(), second = new Second(); + console.log(first.constructor === First, second.constructor === Second); + } + run(); + """, "false\ntrue true\n"]; + yield return ["reflect", """ + function make() { return class Named { value: number; constructor(value: number) { this.value = value; } }; } + const First = make(), Second = make(); + const first: any = Reflect.construct(First, [7]), second: any = Reflect.construct(Second, [9]); + console.log(first.value, second.value, first.constructor === First, second.constructor === Second); + console.log(first instanceof First, first instanceof Second); + """, "7 9 true true\ntrue false\n"]; + yield return ["self-and-prototype-properties", """ + function make() { return class Named { self() { return Named; } }; } + const First = make(), Second = make(); + First.prototype.choice = 7; + const first: any = new First(), second: any = new Second(); + console.log(first.self() === First, second.self() === Second); + console.log(first.choice, second.choice); + """, "true true\n7 undefined\n"]; + yield return ["arguments-and-defaults", """ + function make() { return class Named { + value: number; count: number; + constructor(value: number = 7) { this.value = value; this.count = arguments.length; } + }; } + const First = make(), Second: any = make(); + const first = new First(), second = new Second(9, 10); + console.log(first.value, first.count, second.value, second.count, First.length); + """, "7 0 9 2 0\n"]; + yield return ["bound-construction", """ + function make() { return class Named { value: number; constructor(value: number) { this.value = value; } }; } + const First = make(), Second = make(); + const Bound: any = First.bind(null, 7); + const first: any = new Bound(); + console.log(first.value, first.constructor === First, first instanceof First, first instanceof Second); + """, "7 true true false\n"]; + } + + [Theory, MemberData(nameof(Cases))] + public void EvaluationsOwnConstructorAndPrototypeIdentity(string name, string source, string expected) + { + Assert.NotEmpty(name); + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + } + + [Theory, ModeData] + public void ModuleFactoryKeepsEvaluationIdentityAcrossCalls(ExecutionMode mode) + => Assert.Equal("false true true\n", TestHarness.RunModules(new() + { + ["factory.ts"] = "export function make() { return class Named {}; }", + ["main.ts"] = """ + import { make } from './factory'; + const First = make(), Second = make(); + const first = new First(), second = new Second(); + console.log(First === Second, first.constructor === First, second.constructor === Second); + """ + }, "main.ts", mode)); +} diff --git a/tests/fixtures/RepeatedClassEvaluation/prototype-separate-values.ts b/tests/fixtures/RepeatedClassEvaluation/prototype-separate-values.ts new file mode 100644 index 000000000..faa8ac873 --- /dev/null +++ b/tests/fixtures/RepeatedClassEvaluation/prototype-separate-values.ts @@ -0,0 +1,9 @@ +// #1964 identity control keeps prototype results in locals; identity.ts retains #1966's inline IL probe. +function make() { return class Named {}; } +const First = make(), Second = make(); +const first = new First(), second = new Second(); +const fp = Object.getPrototypeOf(first), sp = Object.getPrototypeOf(second); +console.log(First === Second); +console.log(fp === sp); +console.log(first.constructor === First, second.constructor === Second); +console.log(first instanceof First, first instanceof Second); From 69f2b081821978823e054919b19018b49af776c7 Mon Sep 17 00:00:00 2001 From: nickna Date: Sun, 4 Oct 2026 22:55:22 -0700 Subject: [PATCH 04/16] Make inline constructor prototype queries verifiable (#1966) --- docs/plans/repeated-class-evaluation.md | 18 +++++++ .../ILEmitter.Calls.Constructors.cs | 16 +++--- .../InlineConstructorPrototypeTests.cs | 53 +++++++++++++++++++ 3 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 tests/SharpTS.Tests/SharedTests/InlineConstructorPrototypeTests.cs diff --git a/docs/plans/repeated-class-evaluation.md b/docs/plans/repeated-class-evaluation.md index 7ddb8d771..419744e25 100644 --- a/docs/plans/repeated-class-evaluation.md +++ b/docs/plans/repeated-class-evaluation.md @@ -131,3 +131,21 @@ verification enabled. The full Release solution build and code-quality gates pass (28 duplicate groups, zero errors). The actual AOT/trim/single-file analyzer passes the unchanged zero-warning baseline. Definition-owned computed keys, capture snapshots and static initialization continue in #1965. + +## Implementation evidence for #1966 + +The unchanged inline `identity.ts` reproduced the reported `BackwardBranch` +error at offset 709 after #1964. The reflection constructor's argument-padding +loop jumped forward over its body and then backward into it while the enclosing +prototype query retained values on the evaluation stack. Moving the loop check +to the top makes its stack height determinable in one forward scan. + +The original source now passes CLI IL verification and prints `false` / `false` +in standalone output, matching Node, within 30 seconds with empty stderr. +`InlineConstructorPrototypeTests` verifies all three source controls as saved +assemblies and executes them in both engines and without SharpTS.dll. Additional +controls retain left-to-right, exactly-once callee/argument evaluation, omitted +parameter defaults, and prototype identity for an aliased CLR constructor. +All 279 affected constructor, class-expression owner, generic-constructor and +prototype tests pass with compiled verification enabled; the Release core/test +build passes. The original reference expectation and verifier remain intact. diff --git a/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs b/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs index ae4e24349..9dfd7d514 100644 --- a/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs +++ b/src/SharpTS/Compilation/ILEmitter.Calls.Constructors.cs @@ -712,10 +712,14 @@ private void EmitReflectionConstructFromType(Expr.New n) IL.Emit(OpCodes.Ldc_I4_0); IL.Emit(OpCodes.Stloc, padIndexLocal); var padCheckLabel = IL.DefineLabel(); - var padNextLabel = IL.DefineLabel(); - IL.Emit(OpCodes.Br, padCheckLabel); + var padDoneLabel = IL.DefineLabel(); - IL.MarkLabel(padNextLabel); + // The loop body must be reachable in a forward scan even when the + // enclosing expression carries values on the evaluation stack (#1966). + IL.MarkLabel(padCheckLabel); + IL.Emit(OpCodes.Ldloc, padIndexLocal); + IL.Emit(OpCodes.Ldloc, arityLocal); + IL.Emit(OpCodes.Bge, padDoneLabel); IL.Emit(OpCodes.Ldloc, parametersLocal); IL.Emit(OpCodes.Ldloc, padIndexLocal); IL.Emit(OpCodes.Ldelem_Ref); @@ -735,10 +739,8 @@ private void EmitReflectionConstructFromType(Expr.New n) IL.Emit(OpCodes.Add); IL.Emit(OpCodes.Stloc, padIndexLocal); - IL.MarkLabel(padCheckLabel); - IL.Emit(OpCodes.Ldloc, padIndexLocal); - IL.Emit(OpCodes.Ldloc, arityLocal); - IL.Emit(OpCodes.Blt, padNextLabel); + IL.Emit(OpCodes.Br, padCheckLabel); + IL.MarkLabel(padDoneLabel); for (int i = 0; i < argTemps.Count; i++) { diff --git a/tests/SharpTS.Tests/SharedTests/InlineConstructorPrototypeTests.cs b/tests/SharpTS.Tests/SharedTests/InlineConstructorPrototypeTests.cs new file mode 100644 index 000000000..9019fee2d --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/InlineConstructorPrototypeTests.cs @@ -0,0 +1,53 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class InlineConstructorPrototypeTests +{ + public static IEnumerable Cases() + { + yield return [""" + function make() { return class {}; } + const First = make(); + const Second = make(); + console.log(First === Second); + console.log(Object.getPrototypeOf(new First()) === Object.getPrototypeOf(new Second())); + """, "false\nfalse\n"]; + yield return [""" + const events: string[] = []; + function make() { return class { + constructor(a: number = 3, b: number = 4) { events.push(a + ":" + b); } + }; } + const First = make(); + function target() { events.push("target"); return First; } + function arg(value: number) { events.push("arg" + value); return value; } + console.log(Object.getPrototypeOf(new (target())(arg(1), arg(2))) === First.prototype); + console.log(Object.getPrototypeOf(new (target())()) === First.prototype); + console.log(events.join(",")); + """, "true\ntrue\ntarget,arg1,arg2,1:2,target,3:4\n"]; + yield return [""" + class Named { + value: number; + constructor(value: number = 7) { this.value = value; } + } + const Alias: any = Named; + let calls = 0; + function value() { calls++; return 9; } + console.log(Object.getPrototypeOf(new Alias(value())) === Named.prototype); + console.log(Object.getPrototypeOf(new Alias()) === Named.prototype); + console.log(new Alias().value, calls); + """, "true\ntrue\n7 1\n"]; + } + + [Theory, MemberData(nameof(Cases))] + public void InlineQueriesPreservePrototypeAndEvaluationOrderInVerifiedAssemblies(string source, string expected) + { + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + Assert.Equal(expected, TestHarness.RunCompiledStandalone(source)); + } +} From c782c870ca222d0f141f7051d2052fef2f300f03 Mon Sep 17 00:00:00 2001 From: nickna Date: Mon, 5 Oct 2026 00:04:29 -0700 Subject: [PATCH 05/16] Isolate keys, captures, and initialization per class evaluation (#1965) --- docs/plans/repeated-class-evaluation.md | 39 +++ eng/CodeQuality/duplicates.json | 8 - ...yncGeneratorMoveNextEmitter.Expressions.cs | 84 +----- .../AsyncMoveNextEmitter.ArrowFunctions.cs | 82 +----- .../Compilation/AsyncMoveNextEmitter.cs | 1 + .../CallHandlers/ClassExprStaticHandler.cs | 15 +- .../CallHandlers/SuperConstructorHandler.cs | 11 + .../CallHandlers/ThisStaticContextHandler.cs | 2 +- .../Compilation/ClassExpressionFactory.cs | 4 +- src/SharpTS/Compilation/ClosureAnalyzer.cs | 15 +- .../CompilationContext.ClassExpressions.cs | 8 +- .../EmittedClassDefinitionRuntime.cs | 26 +- .../ExpressionEmitterBase.ClassDefinitions.cs | 229 ++++++++++++++-- .../ExpressionEmitterBase.Operators.cs | 1 + .../Compilation/ExpressionEmitterBase.cs | 26 +- src/SharpTS/Compilation/ILCompiler.Async.cs | 6 +- .../Compilation/ILCompiler.AsyncGenerators.cs | 6 +- .../ILCompiler.Classes.Accessors.cs | 2 + .../ILCompiler.Classes.ClassExpressions.cs | 170 +++++------- ...mpiler.Classes.DefinitionInitialization.cs | 195 ++++++++++++++ ...Compiler.Classes.DefinitionMethodValues.cs | 77 ++++++ .../ILCompiler.Classes.Definitions.cs | 4 +- .../ILCompiler.Classes.HasFields.cs | 27 ++ .../ILCompiler.ContextFactories.cs | 27 +- .../Compilation/ILCompiler.Generators.cs | 6 +- .../Compilation/ILCompiler.IteratorStubs.cs | 5 +- src/SharpTS/Compilation/ILCompiler.State.cs | 3 +- src/SharpTS/Compilation/ILCompiler.cs | 11 + .../Compilation/ILEmitter.Calls.Closures.cs | 11 +- .../Compilation/ILEmitter.Expressions.cs | 24 ++ .../Compilation/ILEmitter.Operators.cs | 10 + .../Compilation/ILEmitter.Properties.cs | 18 +- .../RuntimeEmitter.ClassCaptures.cs | 95 +++++++ .../RuntimeEmitter.ClassDefinition.cs | 65 +++-- .../Compilation/RuntimeEmitter.Iterator.cs | 13 +- .../RuntimeEmitter.Objects.Index.cs | 85 +++++- .../RuntimeEmitter.Objects.SetProperty.cs | 8 +- .../RuntimeEmitter.RuntimeClass.cs | 2 + .../Compilation/RuntimeEmitter.Symbols.cs | 2 +- .../SharedTests/RepeatedClassStateTests.cs | 248 ++++++++++++++++++ 40 files changed, 1292 insertions(+), 379 deletions(-) create mode 100644 src/SharpTS/Compilation/ILCompiler.Classes.DefinitionInitialization.cs create mode 100644 src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs create mode 100644 src/SharpTS/Compilation/RuntimeEmitter.ClassCaptures.cs create mode 100644 tests/SharpTS.Tests/SharedTests/RepeatedClassStateTests.cs diff --git a/docs/plans/repeated-class-evaluation.md b/docs/plans/repeated-class-evaluation.md index 419744e25..70b03835c 100644 --- a/docs/plans/repeated-class-evaluation.md +++ b/docs/plans/repeated-class-evaluation.md @@ -149,3 +149,42 @@ parameter defaults, and prototype identity for an aliased CLR constructor. All 279 affected constructor, class-expression owner, generic-constructor and prototype tests pass with compiled verification enabled; the Release core/test build passes. The original reference expectation and verifier remain intact. + +## Implementation evidence for #1965 + +Each class-expression definition now owns its ordered computed keys, captured +bindings, member descriptors, static fields and initialization. Keys are evaluated +once, before static fields/blocks, and retained across suspension; instances read +their lexical definition's keys rather than mutable template static fields. +String and Symbol methods/accessors register in source order on the fresh +prototype or constructor. Static methods retain the selected definition separately +from the caller's `this`. Captured bindings retain their original display-class or +cell storage, so later writes and closures returned by methods remain live. +Async/generator method state machines retain the instance or static definition, +and suspended closure emitters share the same environment population path. +Instance method values use pre-emitted adapters that retain the lexical definition +separately from their explicit guest receiver. Borrowing a method across class +evaluations therefore retains its original captures while observing the caller's +public fields, including after suspension. + +`RepeatedClassStateTests` exercises 18 source controls in interpretation, +in-process compilation, serialized IL verification/execution and standalone +deployment, plus two module execution modes with serialized module verification. +The source controls include retained constructors after a second evaluation, +string/Symbol fields and members, accessors, static captures and borrowed +receivers, initialization order, abrupt keys, interleaved awaits, inherited keys, +awaited keys/heritage, live captured writes and closures returned after suspension. +All 18 match Node v25.5.0. The unchanged `computed-keys.ts` passes verified CLI +standalone execution within 30 seconds, with empty stderr and no SharpTS.dll, +printing `false 2` / `5 5 undefined undefined 5`. The unchanged one-definition +generic computed-key expectation remains in the affected regression selection. + +The full Release solution build passes. Quality gates report 27 duplicate groups +and zero errors; the obsolete suspended-closure duplicate exception was removed +after sharing emission. The actual AOT/trim/single-file analyzer matches the +unchanged zero-warning baseline. The affected verification selection has 1,596 +passing tests. A separately recorded async-lock verifier failure also reproduces +on unchanged `69f2b081`; three other failures in the wider async selection likewise +reproduce there. These baseline failures are not counted as passes. Hosted +execution is not part of this evidence. Runtime-valued ordinary-user-class +heritage remains the separate #1967 task. diff --git a/eng/CodeQuality/duplicates.json b/eng/CodeQuality/duplicates.json index e3264a5ed..bf58776a7 100644 --- a/eng/CodeQuality/duplicates.json +++ b/eng/CodeQuality/duplicates.json @@ -175,14 +175,6 @@ ], "Reason": "Net and TLS servers expose matching names but bind methods to different transport implementations; retain the explicit transport adapters." }, - { - "Hash": "b9c0a74f210d34929340b991ac7822719362ccb3f317c74c54af1aa6c9730260", - "Members": [ - "src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs::AsyncGeneratorMoveNextEmitter.EmitCapturingArrowFunction(Expr.ArrowFunction af, MethodBuilder method, TypeBuilder displayClass)", - "src/SharpTS/Compilation/AsyncMoveNextEmitter.ArrowFunctions.cs::AsyncMoveNextEmitter.EmitCapturingArrowFunction(Expr.ArrowFunction af, MethodBuilder method, TypeBuilder displayClass)" - ], - "Reason": "Async and async-generator emitters own different state machines; retain matching closure threading until a shared capture-emission contract is extracted." - }, { "Hash": "d53d7b94262ab121d5902bfd0de8b29cc05e24ede4a7a03b176c758ae601a880", "Members": [ diff --git a/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs b/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs index 770669a79..6f2fae48f 100644 --- a/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs +++ b/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Expressions.cs @@ -337,87 +337,9 @@ private void EmitAsyncArrowFunction(Expr.ArrowFunction af) private void EmitCapturingArrowFunction(Expr.ArrowFunction af, MethodBuilder method, TypeBuilder displayClass) { - if (_ctx!.DisplayClassConstructors == null || !_ctx.DisplayClassConstructors.TryGetValue(af, out var displayCtor)) - { - _il.Emit(OpCodes.Ldnull); - SetStackUnknown(); - return; - } - - _il.Emit(OpCodes.Newobj, displayCtor); - - // Thread the entry-point display class into the arrow's $entryPointDC field so it reads - // captured TOP-LEVEL variables through shared storage (the async-generator analog of #732). - if (_ctx.ArrowEntryPointDCFields?.TryGetValue(af, out var entryPointDCField) == true && - _ctx.EntryPointDisplayClassStaticField != null) - { - _il.Emit(OpCodes.Dup); - _il.Emit(OpCodes.Ldsfld, _ctx.EntryPointDisplayClassStaticField); - _il.Emit(OpCodes.Stfld, entryPointDCField); - } - - // Thread the state machine's function display class into the arrow's $functionDC field so a - // write to a captured-and-mutated generator local reaches shared storage instead of a by-value - // snapshot — the case the compile-time guard previously rejected (#725). - if (_ctx.ArrowFunctionDCFields?.TryGetValue(af, out var functionDCField) == true && - _builder.FunctionDCField != null) - { - _il.Emit(OpCodes.Dup); - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldfld, _builder.FunctionDCField); - _il.Emit(OpCodes.Stfld, functionDCField); - } - - if (_ctx.DisplayClassFields == null || !_ctx.DisplayClassFields.TryGetValue(af, out var fieldMap)) - { - Types.EmitLoadMethodInfoViaHandle(_il, method); - _il.Emit(OpCodes.Newobj, _ctx.Runtime!.FunctionConstruction.Constructor); - SetStackUnknown(); - return; - } - - // Populate captured fields - foreach (var (capturedVar, field) in fieldMap) - { - _il.Emit(OpCodes.Dup); - - // Per-iteration cell capture (#650): snapshot the StrongBox REFERENCE. - if (_ctx.CellBindingLocals.TryGetValue(capturedVar, out var cellLocal)) - { - _il.Emit(OpCodes.Ldloc, cellLocal); - _il.Emit(OpCodes.Stfld, field); - continue; - } - - // #767: pivot a captured nested-block shadow to its renamed storage (identity otherwise). - var sourceVar = PivotCaptureSource(_analysis.BlockScopeCaptureRenames, af, capturedVar); - - var hoistedField = _builder.GetVariableField(sourceVar); - if (hoistedField != null) - { - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldfld, hoistedField); - } - else if (capturedVar == "this" && _builder.ThisField != null) - { - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldfld, _builder.ThisField); - } - else if (_ctx.Locals.TryGetLocal(sourceVar, out var local)) - { - _il.Emit(OpCodes.Ldloc, local); - } - else if (!TryEmitGlobalVariable(sourceVar)) - { - _il.Emit(OpCodes.Ldnull); - } - - _il.Emit(OpCodes.Stfld, field); - } - - Types.EmitLoadMethodInfoViaHandle(_il, method); - _il.Emit(OpCodes.Newobj, _ctx.Runtime!.FunctionConstruction.Constructor); - SetStackUnknown(); + if (_ctx!.DisplayClassConstructors.TryGetValue(af, out var constructor)) + EmitCapturingArrowViaHooks(af, method, constructor); + else EmitNullConstant(); } private void EmitNonCapturingArrowFunction(MethodBuilder method) diff --git a/src/SharpTS/Compilation/AsyncMoveNextEmitter.ArrowFunctions.cs b/src/SharpTS/Compilation/AsyncMoveNextEmitter.ArrowFunctions.cs index 2e74f2413..705679fa3 100644 --- a/src/SharpTS/Compilation/AsyncMoveNextEmitter.ArrowFunctions.cs +++ b/src/SharpTS/Compilation/AsyncMoveNextEmitter.ArrowFunctions.cs @@ -63,85 +63,9 @@ private void EmitAsyncArrowFunction(Expr.ArrowFunction af) private void EmitCapturingArrowFunction(Expr.ArrowFunction af, MethodBuilder method, TypeBuilder displayClass) { - if (_ctx!.DisplayClassConstructors == null || !_ctx.DisplayClassConstructors.TryGetValue(af, out var displayCtor)) - { - _il.Emit(OpCodes.Ldnull); - SetStackUnknown(); - return; - } - - _il.Emit(OpCodes.Newobj, displayCtor); - - if (_ctx.ArrowEntryPointDCFields?.TryGetValue(af, out var entryPointDCField) == true && - _ctx.EntryPointDisplayClassStaticField != null) - { - _il.Emit(OpCodes.Dup); - _il.Emit(OpCodes.Ldsfld, _ctx.EntryPointDisplayClassStaticField); - _il.Emit(OpCodes.Stfld, entryPointDCField); - } - - // Populate $functionDC field if this arrow captures function-level variables - if (_ctx.ArrowFunctionDCFields?.TryGetValue(af, out var functionDCField) == true && - _builder.FunctionDCField != null) - { - _il.Emit(OpCodes.Dup); - _il.Emit(OpCodes.Ldarg_0); // state machine - _il.Emit(OpCodes.Ldfld, _builder.FunctionDCField); - _il.Emit(OpCodes.Stfld, functionDCField); - } - - if (_ctx.DisplayClassFields == null || !_ctx.DisplayClassFields.TryGetValue(af, out var fieldMap)) - { - Types.EmitLoadMethodInfoViaHandle(_il, method); - _il.Emit(OpCodes.Newobj, _ctx.Runtime!.FunctionConstruction.Constructor); - SetStackUnknown(); - return; - } - - foreach (var (capturedVar, field) in fieldMap) - { - _il.Emit(OpCodes.Dup); - - // Per-iteration cell capture (#650): snapshot the StrongBox REFERENCE. - if (_ctx.CellBindingLocals.TryGetValue(capturedVar, out var cellLocal)) - { - _il.Emit(OpCodes.Ldloc, cellLocal); - _il.Emit(OpCodes.Stfld, field); - continue; - } - - // #767: pivot a captured nested-block shadow to its renamed storage (identity otherwise). - var sourceVar = PivotCaptureSource(_analysis.BlockScopeCaptureRenames, af, capturedVar); - - var hoistedField = _builder.GetVariableField(sourceVar); - if (hoistedField != null) - { - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldfld, hoistedField); - } - else if (capturedVar == "this" && _builder.ThisField != null) - { - _il.Emit(OpCodes.Ldarg_0); - _il.Emit(OpCodes.Ldfld, _builder.ThisField); - } - else if (_ctx.Locals.TryGetLocal(sourceVar, out var local)) - { - _il.Emit(OpCodes.Ldloc, local); - } - else if (!TryEmitGlobalVariable(sourceVar)) - { - // Preserve the historical unresolved-name fallback. Resolvable - // globals (notably function declarations with expando state) - // are loaded through the same canonical path as a normal read. - _il.Emit(OpCodes.Ldnull); - } - - _il.Emit(OpCodes.Stfld, field); - } - - Types.EmitLoadMethodInfoViaHandle(_il, method); - _il.Emit(OpCodes.Newobj, _ctx.Runtime!.FunctionConstruction.Constructor); - SetStackUnknown(); + if (_ctx!.DisplayClassConstructors.TryGetValue(af, out var constructor)) + EmitCapturingArrowViaHooks(af, method, constructor); + else EmitNullConstant(); } private void EmitNonCapturingArrowFunction(Expr.ArrowFunction af, MethodBuilder method) diff --git a/src/SharpTS/Compilation/AsyncMoveNextEmitter.cs b/src/SharpTS/Compilation/AsyncMoveNextEmitter.cs index 737275098..15b63d1d5 100644 --- a/src/SharpTS/Compilation/AsyncMoveNextEmitter.cs +++ b/src/SharpTS/Compilation/AsyncMoveNextEmitter.cs @@ -24,6 +24,7 @@ public partial class AsyncMoveNextEmitter : AsyncFunctionMoveNextEmitter, IEmitt protected override TypeProvider Types => _types; protected override IVariableResolver Resolver => _resolver!; protected override FieldBuilder? GetThisField() => _builder.ThisField; + protected override FieldBuilder? GetFunctionDCField() => _builder.FunctionDCField; #region IEmitterContext Implementation diff --git a/src/SharpTS/Compilation/CallHandlers/ClassExprStaticHandler.cs b/src/SharpTS/Compilation/CallHandlers/ClassExprStaticHandler.cs index cd53b9813..87afe44a4 100644 --- a/src/SharpTS/Compilation/CallHandlers/ClassExprStaticHandler.cs +++ b/src/SharpTS/Compilation/CallHandlers/ClassExprStaticHandler.cs @@ -22,12 +22,21 @@ public bool TryHandle(IEmitterContext emitter, Expr.Call call) !ctx.VarToClassExpr.TryGetValue(classExprVar.Name.Lexeme, out var classExpr) || ctx.ClassExprStaticMethods == null || !ctx.ClassExprStaticMethods.TryGetValue(classExpr, out var exprStaticMethods) || - !exprStaticMethods.TryGetValue(classExprGet.Name.Lexeme, out var exprStaticMethod)) + !exprStaticMethods.ContainsKey(classExprGet.Name.Lexeme) + || emitter.ArgsContainSuspension(call.Arguments)) return false; var il = emitter.IL; - emitter.EmitStaticCallArguments(call.Arguments, exprStaticMethod); - il.Emit(OpCodes.Call, exprStaticMethod); + emitter.EmitExpression(classExprGet.Object); + emitter.EnsureBoxed(); + var receiver = il.DeclareLocal(ctx.Types.Object); + il.Emit(OpCodes.Stloc, receiver); + il.Emit(OpCodes.Ldloc, receiver); + il.Emit(OpCodes.Ldloc, receiver); + il.Emit(OpCodes.Ldstr, classExprGet.Name.Lexeme); + il.Emit(OpCodes.Call, ctx.Runtime!.ObjectRead.Property); + emitter.EmitArgsArrayWithSpread(call.Arguments); + il.Emit(OpCodes.Call, ctx.Runtime.Invocation.Method); emitter.SetStackUnknown(); return true; } diff --git a/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs b/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs index f8267aaa1..8ad9454e7 100644 --- a/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs +++ b/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs @@ -279,6 +279,17 @@ private static void EmitSuperCtorCall(IEmitterContext emitter, ConstructorBuilde // evaluating and discarding surplus values safely, it publishes the // exact caller list when the base constructor binds `arguments`. emitter.EmitStaticCallArguments(arguments, parentCtor); + if (ctx.CurrentClassExpr is { } expression + && ctx.ClassExprDefinitionFields?.TryGetValue(expression, out var definitionField) == true + && parentCtor.GetParameters().LastOrDefault()?.ParameterType == ctx.Runtime!.ClassDefinitions.Type) + { + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, EmitterTypeHelpers.SelfFieldReference(definitionField)); + il.Emit(OpCodes.Castclass, ctx.Runtime.ClassDefinitions.Type); + il.Emit(OpCodes.Ldfld, ctx.Runtime.ClassDefinitions.Parent); + il.Emit(OpCodes.Isinst, ctx.Runtime.ClassDefinitions.Type); + } System.Reflection.ConstructorInfo ctorToCall = parentCtor; Type? baseType = ctx.CurrentClassBuilder?.BaseType; diff --git a/src/SharpTS/Compilation/CallHandlers/ThisStaticContextHandler.cs b/src/SharpTS/Compilation/CallHandlers/ThisStaticContextHandler.cs index 1a51ecc12..228056bda 100644 --- a/src/SharpTS/Compilation/CallHandlers/ThisStaticContextHandler.cs +++ b/src/SharpTS/Compilation/CallHandlers/ThisStaticContextHandler.cs @@ -21,7 +21,7 @@ public bool TryHandle(IEmitterContext emitter, Expr.Call call) var ctx = emitter.Context; // Only applies in static context - if (ctx.IsInstanceMethod || ctx.CurrentClassBuilder == null) + if (ctx.IsInstanceMethod || ctx.ClassDefinitionParameterIndex != null || ctx.ClassDefinitionVariableName != null || ctx.CurrentClassBuilder == null) return false; string? currentClassName = ctx.CurrentClassName; diff --git a/src/SharpTS/Compilation/ClassExpressionFactory.cs b/src/SharpTS/Compilation/ClassExpressionFactory.cs index 7cc0384ac..202b1a66e 100644 --- a/src/SharpTS/Compilation/ClassExpressionFactory.cs +++ b/src/SharpTS/Compilation/ClassExpressionFactory.cs @@ -1,5 +1,7 @@ using System.Reflection.Emit; +using SharpTS.Parsing; namespace SharpTS.Compilation; -internal sealed record ClassExpressionFactory(MethodBuilder Method, Type Template, string Name, int Length); +internal sealed record ClassExpressionFactory(MethodBuilder Method, Type Template, string Name, int Length, + MethodBuilder Initializer, IReadOnlyList Keys); diff --git a/src/SharpTS/Compilation/ClosureAnalyzer.cs b/src/SharpTS/Compilation/ClosureAnalyzer.cs index 29d5837e6..833130703 100644 --- a/src/SharpTS/Compilation/ClosureAnalyzer.cs +++ b/src/SharpTS/Compilation/ClosureAnalyzer.cs @@ -41,6 +41,7 @@ public class ClosureAnalyzer : AstVisitorBase // Stack of function scopes - tracks the function node at each nesting level private readonly Stack _functionStack = new(); + private readonly Stack<(Expr.ClassExpr Expression, HashSet EnclosingFunctions)> _classCaptureOwners = new(); // Maps function node → set of its local variables that are captured by inner closures private readonly Dictionary> _functionCapturedLocals = []; @@ -399,6 +400,9 @@ private void ReferenceVariable(string name) // This enables function-level display class creation // Walk up the function stack to find which function defines this variable var definingFunc = FindDefiningFunction(name); + foreach (var (expression, enclosingFunctions) in _classCaptureOwners) + if (definingFunc == null || enclosingFunctions.Contains(definingFunc)) + _captures[expression].Add(name); if (definingFunc != null) { // The variable is defined in some function, not at top level @@ -679,12 +683,12 @@ protected override void VisitClassExpr(Expr.ClassExpr expr) // Class expressions don't declare the class name in the outer scope // (unlike class declarations), but we still need to analyze all bodies + _classCaptureOwners.Push((expr, _functionStack.ToHashSet())); // Analyze field initializers for captured variables - foreach (var field in expr.Fields) - { - if (field.Initializer != null) - Visit(field.Initializer); - } + AnalyzeFunctionBody(expr, [], expr.Fields.Where(field => field.Initializer != null) + .Select(field => (Stmt)new Stmt.Expression(field.Initializer!)) + .Concat((expr.StaticInitializers ?? []).OfType().SelectMany(block => block.Body)) + .ToList()); // Analyze methods foreach (var method in expr.Methods) @@ -705,6 +709,7 @@ protected override void VisitClassExpr(Expr.ClassExpr expr) AnalyzeFunctionBody(accessor, parameters, accessor.Body); } } + _classCaptureOwners.Pop(); } protected override void VisitBlock(Stmt.Block stmt) diff --git a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs index aa44242c5..a813bf9c9 100644 --- a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs +++ b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs @@ -12,6 +12,7 @@ public partial class CompilationContext // Class expression builders (class expr node -> type builder) public Dictionary? ClassExprBuilders { get; set; } internal Dictionary? ClassExprFactories { get; set; } + internal Dictionary? ClassExprDefinitionFields { get; set; } // Class expression extended tracking public Dictionary>? ClassExprBackingFields { get; set; } @@ -27,10 +28,15 @@ public partial class CompilationContext public Dictionary? ClassExprConstructors { get; set; } public ClassGenericParameterRegistry? ClassGenericParameters { get; set; } public Dictionary? ClassExprSuperclass { get; set; } - public Dictionary>? ClassExprCaptureFields { get; set; } + public Dictionary>? ClassExprCaptureSlots { get; set; } // Current class expression being compiled public Expr.ClassExpr? CurrentClassExpr { get; set; } + internal int? ClassDefinitionParameterIndex { get; set; } + internal int? ClassDefinitionThisParameterIndex { get; set; } + internal string? ClassDefinitionVariableName { get; set; } + internal FieldBuilder? ClassDefinitionOwnerField { get; set; } + internal string? GuestThisVariableName { get; set; } // Variable name to class expression mapping (for static member access) public Dictionary? VarToClassExpr { get; set; } diff --git a/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs index 2f100e5bb..a6c41da36 100644 --- a/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs +++ b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs @@ -9,8 +9,10 @@ public sealed class EmittedClassDefinitionRuntime internal EmittedClassDefinitionRuntime() { } internal sealed record Declarations(TypeBuilder Type, Type InstanceInterface, MethodInfo GetDefinition, ConstructorBuilder Constructor, FieldBuilder Template, - FieldBuilder Factory, FieldBuilder Prototype, MethodBuilder Create, MethodBuilder Construct, - MethodBuilder ReadProperty, MethodBuilder Invoke); + FieldBuilder Factory, FieldBuilder Prototype, FieldBuilder Keys, FieldBuilder Parent, FieldBuilder Captures, MethodBuilder Create, MethodBuilder Construct, + MethodBuilder ReadProperty, MethodBuilder Invoke, TypeBuilder CaptureType, ConstructorBuilder CaptureConstructor, + FieldBuilder CaptureOwner, FieldBuilder CaptureField, MethodBuilder ReadCapture, MethodBuilder WriteCapture, + MethodBuilder FindEnvironment); private Declarations? _declarations; private Declarations Required => _declarations @@ -23,10 +25,17 @@ internal sealed record Declarations(TypeBuilder Type, Type InstanceInterface, public FieldBuilder Template => Required.Template; public FieldBuilder Factory => Required.Factory; public FieldBuilder Prototype => Required.Prototype; + public FieldBuilder Keys => Required.Keys; + public FieldBuilder Parent => Required.Parent; + public FieldBuilder Captures => Required.Captures; public MethodBuilder Create => Required.Create; public MethodBuilder Construct => Required.Construct; public MethodBuilder ReadProperty => Required.ReadProperty; public MethodBuilder Invoke => Required.Invoke; + public ConstructorBuilder CaptureConstructor => Required.CaptureConstructor; + public MethodBuilder ReadCapture => Required.ReadCapture; + public MethodBuilder WriteCapture => Required.WriteCapture; + public MethodBuilder FindEnvironment => Required.FindEnvironment; internal void Declare(Declarations declarations) { @@ -34,11 +43,15 @@ internal void Declare(Declarations declarations) if (_declarations is not null || IsComplete) throw new InvalidOperationException("Class-definition metadata has already been declared."); if (new MemberInfo[] { declarations.Constructor, declarations.Template, declarations.Factory, - declarations.Prototype, declarations.Create, declarations.Construct, - declarations.ReadProperty, declarations.Invoke } + declarations.Prototype, declarations.Keys, declarations.Parent, declarations.Captures, declarations.Create, declarations.Construct, + declarations.ReadProperty, declarations.Invoke, declarations.ReadCapture, declarations.WriteCapture, + declarations.FindEnvironment } .Any(member => member.DeclaringType != declarations.Type) || declarations.GetDefinition.DeclaringType != declarations.InstanceInterface - || declarations.InstanceInterface.Module != declarations.Type.Module) + || declarations.InstanceInterface.Module != declarations.Type.Module + || declarations.CaptureType.Module != declarations.Type.Module + || new MemberInfo[] { declarations.CaptureConstructor, declarations.CaptureOwner, declarations.CaptureField } + .Any(member => member.DeclaringType != declarations.CaptureType)) throw new InvalidOperationException("Class-definition handles must belong to their declared owner."); _declarations = declarations; } @@ -47,7 +60,8 @@ internal void CompleteEmission() { var declarations = Required; if (IsComplete) throw new InvalidOperationException("Class-definition emission is already complete."); - if (new[] { declarations.Create, declarations.Construct, declarations.ReadProperty, declarations.Invoke } + if (new[] { declarations.Create, declarations.Construct, declarations.ReadProperty, declarations.Invoke, + declarations.ReadCapture, declarations.WriteCapture, declarations.FindEnvironment } .Any(method => method.GetILGenerator().ILOffset == 0)) throw new InvalidOperationException("Class-definition method bodies have not all been emitted."); IsComplete = true; diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs index efd219b1b..797e05783 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs @@ -1,4 +1,5 @@ using System.Reflection.Emit; +using System.Reflection; using SharpTS.Parsing; namespace SharpTS.Compilation; @@ -7,44 +8,132 @@ public abstract partial class ExpressionEmitterBase { protected bool TryEmitClassDefinitionSelf(string name) { - if (!Ctx.IsInstanceMethod) return false; var expression = Ctx.CurrentClassExpr ?? Ctx.ClassExprBuilders? .FirstOrDefault(entry => ReferenceEquals(entry.Value, Ctx.CurrentClassBuilder)).Key; if (expression?.Name?.Lexeme != name) return false; - EmitThis(); + if (!TryEmitOwnedClassDefinition()) return false; + SetStackUnknown(); + return true; + } + + protected bool TryEmitOwnedClassDefinition() + { + if (Ctx.ClassDefinitionOwnerField is { } ownerField) + { + IL.Emit(OpCodes.Ldarg_0); + if (GetThisField() is { } stateReceiver) + { + IL.Emit(OpCodes.Ldfld, stateReceiver); + IL.Emit(OpCodes.Castclass, ownerField.DeclaringType!); + } + IL.Emit(OpCodes.Ldfld, ownerField); + } + else if (Ctx.ClassDefinitionVariableName is { } variable) + EmitVariable(new Expr.Variable(new Token(TokenType.IDENTIFIER, variable, null, 0))); + else if (Ctx.ClassDefinitionParameterIndex is { } parameter) + IL.Emit(OpCodes.Ldarg, parameter); + else + { + var expression = Ctx.CurrentClassExpr ?? Ctx.ClassExprBuilders? + .FirstOrDefault(entry => ReferenceEquals(entry.Value, Ctx.CurrentClassBuilder)).Key; + if (!Ctx.IsInstanceMethod || expression == null || + Ctx.ClassExprDefinitionFields?.TryGetValue(expression, out var field) != true) return false; + EmitThis(); + // Read the lexical owner's field: a derived instance can carry a + // distinct child definition while executing this base method. + var reference = EmitterTypeHelpers.SelfFieldReference(field!); + if (field!.DeclaringType!.IsGenericTypeDefinition && Ctx.ClassExprFactories?.TryGetValue(expression, out var factory) == true) + reference = EmitterTypeHelpers.ResolveField(factory.Template, field); + IL.Emit(OpCodes.Castclass, reference.DeclaringType!); + IL.Emit(OpCodes.Ldfld, reference); + } + IL.Emit(OpCodes.Castclass, Ctx.Runtime!.ClassDefinitions.Type); + SetStackUnknown(); + return true; + } + + protected bool TryEmitGuestThis() + { + if (Ctx.GuestThisVariableName is not { } name) return false; + EmitVariable(new Expr.Variable(new Token(TokenType.IDENTIFIER, name, null, 0))); EnsureBoxed(); - IL.Emit(OpCodes.Castclass, Ctx.Runtime!.ClassDefinitions.InstanceInterface); - IL.Emit(OpCodes.Callvirt, Ctx.Runtime.ClassDefinitions.GetDefinition); + return true; + } + + protected bool TryEmitGuestThisGet(Expr.Get get) + { + if (get.Object is not Expr.This || get.Optional || !TryEmitGuestThis()) return false; + IL.Emit(OpCodes.Ldstr, get.Name.Lexeme); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectRead.Property); + SetStackUnknown(); + return true; + } + + protected bool TryEmitClassDefinitionCapture(string name) + { + if (!TryGetClassDefinitionCaptureSlot(name, out var slot) || !TryEmitOwnedClassDefinition()) return false; + IL.Emit(OpCodes.Ldfld, Ctx.Runtime!.ClassDefinitions.Captures); + IL.Emit(OpCodes.Ldc_I4, slot); + IL.Emit(OpCodes.Ldelem_Ref); + IL.Emit(OpCodes.Call, Ctx.Runtime.ClassDefinitions.ReadCapture); + Ctx.EmitLexicalTdzValueCheck(IL, name); SetStackUnknown(); return true; } + protected bool TryGetClassDefinitionCaptureSlot(string name, out int slot) + { + slot = -1; + if (Ctx.TryGetParameter(name, out _) || Ctx.Locals.GetLocal(name) != null || GetHoistedVariableField(name) != null) return false; + var expression = Ctx.CurrentClassExpr ?? Ctx.ClassExprBuilders? + .FirstOrDefault(entry => ReferenceEquals(entry.Value, Ctx.CurrentClassBuilder)).Key; + if (expression == null || Ctx.ClassExprCaptureSlots?.TryGetValue(expression, out var captures) != true + || !captures!.TryGetValue(name, out slot)) return false; + return true; + } + + protected bool TryEmitStoreClassDefinitionCapture(string name) + { + if (!TryGetClassDefinitionCaptureSlot(name, out var slot)) return false; + var value = IL.DeclareLocal(Types.Object); + IL.Emit(OpCodes.Stloc, value); + if (!TryEmitOwnedClassDefinition()) throw new InvalidOperationException("Capture has no class definition owner."); + IL.Emit(OpCodes.Ldc_I4, slot); + IL.Emit(OpCodes.Ldloc, value); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassDefinitions.WriteCapture); + return true; + } + + protected bool TryEmitClassDefinitionEnvironment(Type environment) + { + if (!TryEmitOwnedClassDefinition()) return false; + IL.Emit(OpCodes.Ldtoken, environment); + IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassDefinitions.FindEnvironment); + IL.Emit(OpCodes.Castclass, environment); + return true; + } + protected void EmitGuestClassDefinition(Expr.ClassExpr expression) { if (Ctx.ClassExprFactories?.TryGetValue(expression, out var factory) != true) throw new InvalidOperationException("Class expression factory has not been declared."); - var parent = IL.DeclareLocal(Types.Object); if (expression.SuperclassExpr is { } heritage) { EmitClassHeritageValue(heritage, expression.Name?.Lexeme); EnsureBoxed(); } else IL.Emit(OpCodes.Ldnull); - IL.Emit(OpCodes.Stloc, parent); - if (Ctx.ClassExprCaptureFields?.TryGetValue(expression, out var captures) == true) - { - foreach (var (name, field) in captures) - { - EmitVariable(new Expr.Variable(new Token(TokenType.IDENTIFIER, name, null, 0))); - EnsureBoxed(); - IL.Emit(OpCodes.Stsfld, field); - } - } + var parent = _helpers.SpillStoreObject(); IL.Emit(OpCodes.Ldtoken, factory!.Template); IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassInitialization.RunDefinition); - if (Ctx.DeferredClassDefinitions?.TryGet(expression, out var deferred) == true) - EmitDefinitionComputedKeys(deferred.Registrar, deferred.Keys); + var keys = IL.DeclareLocal(Types.ObjectArray); + EmitDefinitionKeyArray(factory.Keys); + IL.Emit(OpCodes.Stloc, keys); + var captures = IL.DeclareLocal(Types.ObjectArray); + EmitDefinitionCaptureArray(Ctx.ClassExprCaptureSlots!.GetValueOrDefault(expression)?.Keys.ToArray() ?? []); + IL.Emit(OpCodes.Stloc, captures); IL.Emit(OpCodes.Ldtoken, factory.Template); IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); IL.Emit(OpCodes.Ldnull); @@ -53,23 +142,116 @@ protected void EmitGuestClassDefinition(Expr.ClassExpr expression) IL.Emit(OpCodes.Ldstr, factory.Name); IL.Emit(OpCodes.Ldc_R8, (double)factory.Length); IL.Emit(OpCodes.Ldloc, parent); + IL.Emit(OpCodes.Ldloc, keys); + IL.Emit(OpCodes.Ldloc, captures); IL.Emit(OpCodes.Call, Ctx.Runtime.ClassDefinitions.Create); + IL.Emit(OpCodes.Dup); + IL.Emit(OpCodes.Call, factory.Initializer); SetStackUnknown(); } protected void EmitDefinitionComputedKeys(MethodBuilder method, IReadOnlyList keys) + { + EmitDefinitionKeyArray(keys); + IL.Emit(OpCodes.Call, method); + } + + private void EmitDefinitionCaptureArray(IReadOnlyList names) + { + var values = new List(names.Count); + foreach (var name in names) + { + FieldInfo? field = null; + if (Ctx.CellBindingLocals.TryGetValue(name, out var cell)) + { + IL.Emit(OpCodes.Ldloc, cell); + field = Types.StrongBoxOfObjectValueField; + } + else if (Ctx.FunctionDisplayClassFields?.TryGetValue(name, out var functionField) == true + && Ctx.FunctionDisplayClassLocal != null) + { + IL.Emit(OpCodes.Ldloc, Ctx.FunctionDisplayClassLocal); + field = functionField; + } + else if (Ctx.FunctionDisplayClassFields?.TryGetValue(name, out functionField) == true + && GetFunctionDCField() is { } stateField) + { + IL.Emit(OpCodes.Ldarg_0); + IL.Emit(OpCodes.Ldfld, stateField); + field = functionField; + } + else if (Ctx.FunctionDisplayClassFields?.TryGetValue(name, out functionField) == true + && Ctx.CurrentArrowFunctionDCField != null) + { + IL.Emit(OpCodes.Ldarg_0); + IL.Emit(OpCodes.Ldfld, Ctx.CurrentArrowFunctionDCField); + field = functionField; + } + else if (Ctx.CapturedFields?.TryGetValue(name, out var capturedField) == true) + { + IL.Emit(OpCodes.Ldarg_0); + field = capturedField; + } + else if (Ctx.EntryPointDisplayClassFields?.TryGetValue(name, out var entryField) == true + && (Ctx.EntryPointDisplayClassLocal != null || Ctx.EntryPointDisplayClassStaticField != null)) + { + if (Ctx.EntryPointDisplayClassLocal != null) IL.Emit(OpCodes.Ldloc, Ctx.EntryPointDisplayClassLocal); + else IL.Emit(OpCodes.Ldsfld, Ctx.EntryPointDisplayClassStaticField!); + field = entryField; + } + else if (Ctx.TopLevelStaticVars?.TryGetValue(name, out var staticField) == true) + { + IL.Emit(OpCodes.Ldnull); + field = staticField; + } + if (field != null) + { + IL.Emit(OpCodes.Ldtoken, field); + IL.Emit(OpCodes.Ldtoken, field.DeclaringType!); + IL.Emit(OpCodes.Call, Types.GetMethod(Types.FieldInfo, "GetFieldFromHandle", + Types.Resolve("System.RuntimeFieldHandle"), Types.Resolve("System.RuntimeTypeHandle"))); + IL.Emit(OpCodes.Newobj, Ctx.Runtime!.ClassDefinitions.CaptureConstructor); + } + else if (TryGetClassDefinitionCaptureSlot(name, out var slot) && TryEmitOwnedClassDefinition()) + { + IL.Emit(OpCodes.Ldfld, Ctx.Runtime!.ClassDefinitions.Captures); + IL.Emit(OpCodes.Ldc_I4, slot); + IL.Emit(OpCodes.Ldelem_Ref); + } + else + { + EmitVariable(new Expr.Variable(new Token(TokenType.IDENTIFIER, name, null, 0))); + EnsureBoxed(); + } + values.Add(_helpers.SpillStoreObject()); + } + IL.Emit(OpCodes.Ldc_I4, values.Count); + IL.Emit(OpCodes.Newarr, Types.Object); + for (int i = 0; i < values.Count; i++) + { + IL.Emit(OpCodes.Dup); + IL.Emit(OpCodes.Ldc_I4, i); + IL.Emit(OpCodes.Ldloc, values[i]); + IL.Emit(OpCodes.Stelem_Ref); + } + } + + protected void EmitDefinitionKeyArray(IReadOnlyList keys, bool coerceKeys = true) { var values = new List(keys.Count); foreach (var key in keys) { EmitExpression(key); EnsureBoxed(); - var isSymbol = IL.DefineLabel(); - IL.Emit(OpCodes.Dup); - IL.Emit(OpCodes.Isinst, Ctx.Runtime!.Symbols.Type); - IL.Emit(OpCodes.Brtrue, isSymbol); - IL.Emit(OpCodes.Call, Ctx.Runtime.StringCoercion.ToJsString); - IL.MarkLabel(isSymbol); + if (coerceKeys) + { + var isSymbol = IL.DefineLabel(); + IL.Emit(OpCodes.Dup); + IL.Emit(OpCodes.Isinst, Ctx.Runtime!.Symbols.Type); + IL.Emit(OpCodes.Brtrue, isSymbol); + IL.Emit(OpCodes.Call, Ctx.Runtime.StringCoercion.ToJsString); + IL.MarkLabel(isSymbol); + } values.Add(_helpers.SpillStoreObject()); } IL.Emit(OpCodes.Ldc_I4, values.Count); @@ -81,6 +263,5 @@ protected void EmitDefinitionComputedKeys(MethodBuilder method, IReadOnlyList protected virtual void EmitStoreVariable(string name) { + if (TryEmitStoreClassDefinitionCapture(name)) return; // Per-iteration loop-binding cell (#650): write through the StrongBox. This base // helper (used by state-machine increments/compound/logical assignment) hand-rolls // the store instead of routing through the resolver, so the cell case is handled here. diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.cs index 65b6296ee..8f32dfdfe 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.cs @@ -771,6 +771,7 @@ protected virtual void EmitSetIndex(Expr.SetIndex si) /// protected virtual void EmitThis() { + if (TryEmitGuestThis()) return; var thisField = GetThisField(); if (thisField != null) { @@ -821,6 +822,7 @@ protected virtual void EmitSuper(Expr.Super s) protected virtual void EmitVariable(Expr.Variable v) { if (TryEmitClassDefinitionSelf(v.Name.Lexeme)) return; + if (TryEmitClassDefinitionCapture(v.Name.Lexeme)) return; string name = v.Name.Lexeme; if (TryEmitEnumInitializerMember(name)) @@ -900,6 +902,7 @@ protected virtual void EmitAssign(Expr.Assign a) } IL.Emit(OpCodes.Dup); + if (TryEmitStoreClassDefinitionCapture(name)) { SetStackUnknown(); return; } if (TryEmitGlobalStore(name)) return; Resolver.TryStoreVariable(name); @@ -1938,7 +1941,7 @@ protected static string PivotCaptureSource( && names.TryGetValue(capturedVar, out var storage) ? storage : capturedVar; - private void EmitCapturingArrowViaHooks(Expr.ArrowFunction af, MethodBuilder method, ConstructorBuilder displayCtor) + protected void EmitCapturingArrowViaHooks(Expr.ArrowFunction af, MethodBuilder method, ConstructorBuilder displayCtor) { EmitCapturingArrowDisplayViaHooks(af, displayCtor); Types.EmitLoadMethodInfoViaHandle(IL, method); @@ -1972,13 +1975,19 @@ private void EmitCapturingArrowDisplayViaHooks( // than a by-value snapshot — the write case that was previously rejected (#674). The arrow's // own snapshot fields (populated below) deliberately omit these vars (see the function-DC // skip in CollectAndDefineArrowFunctions), so the two paths don't both materialize them. - if (Ctx.ArrowFunctionDCFields?.TryGetValue(af, out var arrowFunctionDCField) == true && - GetFunctionDCField() is FieldBuilder stateMachineFunctionDC) + if (Ctx.ArrowFunctionDCFields?.TryGetValue(af, out var arrowFunctionDCField) == true) { IL.Emit(OpCodes.Dup); - IL.Emit(OpCodes.Ldarg_0); - IL.Emit(OpCodes.Ldfld, stateMachineFunctionDC); - IL.Emit(OpCodes.Stfld, arrowFunctionDCField); + if (GetFunctionDCField() is FieldBuilder stateMachineFunctionDC && + stateMachineFunctionDC.FieldType == arrowFunctionDCField.FieldType) + { + IL.Emit(OpCodes.Ldarg_0); + IL.Emit(OpCodes.Ldfld, stateMachineFunctionDC); + IL.Emit(OpCodes.Stfld, arrowFunctionDCField); + } + else if (TryEmitClassDefinitionEnvironment(arrowFunctionDCField.FieldType)) + IL.Emit(OpCodes.Stfld, arrowFunctionDCField); + else IL.Emit(OpCodes.Pop); } if (Ctx.DisplayClassFields?.TryGetValue(af, out var fieldMap) == true) @@ -2004,6 +2013,7 @@ private void EmitCapturingArrowDisplayViaHooks( // stub never populates, so resolving it via the map would snapshot null // (NRE when the arrow dereferences `this`). The real receiver lives in the // builder's dedicated ThisField (set by the instance-method stub). + else if (capturedVar == "this" && TryEmitGuestThis()) { } else if (capturedVar == "this" && GetThisField() is FieldBuilder thisField) { IL.Emit(OpCodes.Ldarg_0); @@ -2018,7 +2028,7 @@ private void EmitCapturingArrowDisplayViaHooks( { IL.Emit(OpCodes.Ldloc, local); } - else if (!TryEmitGlobalVariable(sourceVar)) + else if (!TryEmitClassDefinitionCapture(sourceVar) && !TryEmitGlobalVariable(sourceVar)) { IL.Emit(OpCodes.Ldnull); } @@ -2411,6 +2421,7 @@ protected virtual bool TryEmitStaticFieldAccess(Expr.Get g) /// protected bool TryEmitStaticClassMethodValue(Expr.Get g) { + if (Ctx.ClassDefinitionParameterIndex != null || Ctx.ClassDefinitionVariableName != null) return false; if (g.Object is not Expr.This || Ctx.IsInstanceMethod || Ctx.CurrentClassBuilder == null) return false; @@ -2899,6 +2910,7 @@ protected bool TryEmitNamespaceVarGet(Expr.Get g) protected virtual void EmitGet(Expr.Get g) { + if (TryEmitGuestThisGet(g)) return; if (StringEmitter.TryEmitPrimitiveStringLengthGet(this, g)) return; diff --git a/src/SharpTS/Compilation/ILCompiler.Async.cs b/src/SharpTS/Compilation/ILCompiler.Async.cs index 1d279af9f..b58acb6a7 100644 --- a/src/SharpTS/Compilation/ILCompiler.Async.cs +++ b/src/SharpTS/Compilation/ILCompiler.Async.cs @@ -1579,6 +1579,8 @@ private void EmitAsyncMethodBody(MethodBuilder methodBuilder, Stmt.Function meth { // Analyze async function to determine await points and hoisted variables var analysis = _async.Analyzer.Analyze(method); + if (isInstanceMethod && (_classExprs.DefinitionMethods.ContainsKey(methodBuilder) || _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, methodBuilder.DeclaringType)))) + analysis = analysis with { UsesThis = true }; // Check if method has @lock decorator bool hasLock = HasLockDecorator(method); @@ -1594,7 +1596,8 @@ private void EmitAsyncMethodBody(MethodBuilder methodBuilder, Stmt.Function meth isInstanceMethod: isInstanceMethod, hasAsyncArrows: hasAsyncArrows, hasLock: hasLock, - hoistedParameterTypes: GetStableAsyncParameterFieldTypes(method) + hoistedParameterTypes: GetStableAsyncParameterFieldTypes(method), + hasDynamicThis: !isInstanceMethod && _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, methodBuilder.DeclaringType)) ); RegisterStateMachine( methodBuilder, @@ -1657,6 +1660,7 @@ private void EmitAsyncMethodBody(MethodBuilder methodBuilder, Stmt.Function meth // nested private member access inside the async body resolves under module compilation. ctx.CurrentClassName = currentClassName ?? methodBuilder.DeclaringType?.Name; ctx.CurrentClassBuilder = methodBuilder.DeclaringType as TypeBuilder; + ApplyClassDefinitionStateMachineContext(ctx, methodBuilder, isInstanceMethod); // Entry-point display class for captured top-level variables ApplyCapturedTopLevelVariableAccess(ctx); ctx.ArrowEntryPointDCFields = _closures.ArrowEntryPointDCFields.Count > 0 ? _closures.ArrowEntryPointDCFields : null; diff --git a/src/SharpTS/Compilation/ILCompiler.AsyncGenerators.cs b/src/SharpTS/Compilation/ILCompiler.AsyncGenerators.cs index 00fcbecda..0d3795595 100644 --- a/src/SharpTS/Compilation/ILCompiler.AsyncGenerators.cs +++ b/src/SharpTS/Compilation/ILCompiler.AsyncGenerators.cs @@ -185,6 +185,8 @@ private void EmitAsyncGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Func { // Analyze async generator function to determine yield/await points and hoisted variables var analysis = _asyncGenerators.Analyzer.Analyze(method); + if (isInstanceMethod && (_classExprs.DefinitionMethods.ContainsKey(methodBuilder) || _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, methodBuilder.DeclaringType)))) + analysis = analysis with { UsesThis = true }; // Build state machine type. A static async generator method (#778) has no `this`/instance fields, // so it is set up like a free function (isInstanceMethod: false, static stub). Use the @@ -195,7 +197,8 @@ private void EmitAsyncGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Func $"{methodBuilder.DeclaringType!.Name}_{methodBuilder.Name}", analysis, isInstanceMethod: isInstanceMethod, - runtime: _runtime + runtime: _runtime, + hasDynamicThis: !isInstanceMethod && _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, methodBuilder.DeclaringType)) ); RegisterStateMachine( methodBuilder, @@ -238,6 +241,7 @@ private void EmitAsyncGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Func // generator threads its QUALIFIED class name so nested private member access resolves — #720). ctx.CurrentClassName = currentClassName ?? methodBuilder.DeclaringType?.Name; ctx.CurrentClassBuilder = methodBuilder.DeclaringType as TypeBuilder; + ApplyClassDefinitionStateMachineContext(ctx, methodBuilder, isInstanceMethod); // Entry-point display class for captured top-level variables ApplyCapturedTopLevelVariableAccess(ctx); // Per-arrow $entryPointDC field map so a capturing arrow nested in this instance async diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Accessors.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Accessors.cs index 199786bbc..94f87dddd 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Accessors.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Accessors.cs @@ -31,6 +31,8 @@ private void DefineSymbolAccessorMethod(TypeBuilder typeBuilder, Stmt.Accessor a MethodAttributes attrs = MethodAttributes.Public | MethodAttributes.HideBySig; if (accessor.IsStatic) attrs |= MethodAttributes.Static; Type[] paramTypes = isGetter ? [] : [typeof(object)]; + if (accessor.IsStatic && _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, typeBuilder))) + paramTypes = [_runtime.ClassDefinitions.Type, .. paramTypes]; var methodBuilder = typeBuilder.DefineMethod( $"$sym_{(isGetter ? "get" : "set")}_{list.Count}", diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs index 015c379b0..1da0b3203 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs @@ -26,6 +26,9 @@ private void DefineClassExpressionTypes() /// private void DefineClassExpression(Expr.ClassExpr classExpr) { + if (_classExprs.Builders.ContainsKey(classExpr)) return; + if (ResolveClassExpressionParent(classExpr) is { } parent && !ReferenceEquals(parent, classExpr)) + DefineClassExpression(parent); string className = _classExprs.Names[classExpr]; // Create TypeBuilder with appropriate attributes @@ -44,16 +47,13 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) var lexicalCaptures = new HashSet(StringComparer.Ordinal); foreach (var method in classExpr.Methods) lexicalCaptures.UnionWith(_closures.Analyzer.GetCaptures(method)); + lexicalCaptures.UnionWith(_closures.Analyzer.GetCaptures(classExpr)); + foreach (var accessor in classExpr.Accessors ?? []) + lexicalCaptures.UnionWith(_closures.Analyzer.GetCaptures(accessor)); if (classExpr.Name != null) lexicalCaptures.Remove(classExpr.Name.Lexeme); - var captureFields = new Dictionary(StringComparer.Ordinal); - foreach (var capture in lexicalCaptures) - { - captureFields[capture] = typeBuilder.DefineField( - $"$lex_{capture}", _types.Object, - FieldAttributes.Public | FieldAttributes.Static); - } - _classExprs.CaptureFields[classExpr] = captureFields; + _classExprs.CaptureSlots[classExpr] = lexicalCaptures.OrderBy(name => name, StringComparer.Ordinal) + .Select((name, index) => (name, index)).ToDictionary(entry => entry.name, entry => entry.index, StringComparer.Ordinal); // Track superclass name for inheritance resolution string? superclassName = Expr.GetSuperclassLeafName(classExpr.SuperclassExpr); @@ -84,7 +84,7 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) _classes.GenericParameters.Declare(typeBuilder, classGenericParams ?? []); EmitTypeDefinitions.AddInterfaceImplementation(typeBuilder, _runtime.ClassDefinitions.InstanceInterface); - var definitionField = typeBuilder.DefineField("$definition", _types.Object, FieldAttributes.Private); + var definitionField = typeBuilder.DefineField("$definition", _types.Object, FieldAttributes.Assembly); _classExprs.DefinitionFields[classExpr] = definitionField; var definitionGetter = typeBuilder.DefineMethod("$GetClassDefinition", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, @@ -194,7 +194,7 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) // parameter) cannot be widened to / narrowed from the object-typed $IHasFields slots // without box/unbox of `T`, which yields unverifiable IL. Like class declarations // (ILCompiler.Classes.cs), these fields live in the `_fields` dictionary instead (#291). - foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsDeclare)) + foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsDeclare && f.ComputedKey == null)) { bool isGenericField = classGenericParams != null && field.TypeAnnotation != null && @@ -385,9 +385,10 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) method.Name.Lexeme, MethodAttributes.Public | MethodAttributes.Static, returnType, - paramTypes + [_runtime.ClassDefinitions.Type, .. paramTypes] ); _classExprs.StaticMethods[classExpr][method.Name.Lexeme] = methodBuilder; + methodBuilder.DefineParameter(1, ParameterAttributes.None, "__classDefinition"); RegisterArgumentsCapturingMethod(methodBuilder, method.Body); } @@ -472,7 +473,14 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) } } - DefineDeferredComputedMethodKeyRegistrar(classExpr, typeBuilder, classExpr.Fields); + var keys = _classes.ComputedMembers.GetMethods(typeBuilder) + .Select(entry => (entry.Key, entry.Method.Name.Start)) + .Concat(_classes.ComputedMembers.GetAccessors(typeBuilder) + .Select(entry => (entry.Accessor.ComputedKey!, entry.Accessor.Name.Start))) + .Concat(classExpr.Fields.Where(field => field.ComputedKey != null && !field.IsDeclare) + .Select(field => (field.ComputedKey!, field.Name.Start))) + .OrderBy(entry => entry.Start).Select(entry => entry.Item1).ToArray(); + _classExprs.Factories[classExpr] = _classExprs.Factories[classExpr] with { Keys = keys }; } /// @@ -606,15 +614,6 @@ private CompilationContext CreateClassExpressionContext( // binding throws ReferenceError at runtime (same omission #300 fixed // for class-declaration accessor bodies). ApplyCapturedTopLevelVariableAccess(ctx, memberBodyExports: true); - if (_classExprs.CaptureFields.TryGetValue(classExpr, out var captureFields) - && captureFields.Count > 0) - { - ctx.TopLevelStaticVars = ctx.TopLevelStaticVars == null - ? new Dictionary(captureFields, StringComparer.Ordinal) - : new Dictionary(ctx.TopLevelStaticVars, StringComparer.Ordinal); - foreach (var (name, field) in captureFields) - ctx.TopLevelStaticVars[name] = field; - } return ctx; } @@ -623,80 +622,11 @@ private CompilationContext CreateClassExpressionContext( /// private void EmitClassExpressionStaticConstructor(Expr.ClassExpr classExpr, TypeBuilder typeBuilder) { - bool hasStaticFields = classExpr.Fields.Any(f => f.IsStatic && f.Initializer != null); - bool hasStaticInitializers = classExpr.StaticInitializers?.Count > 0; - - var cctor = typeBuilder.DefineConstructor( - MethodAttributes.Static | MethodAttributes.Private | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, - CallingConventions.Standard, - Type.EmptyTypes - ); - + var cctor = typeBuilder.DefineTypeInitializer(); var il = cctor.GetILGenerator(); - var ctx = CreateClassExpressionContext(il, classExpr, typeBuilder, null, cctor); - ctx.IsStaticConstructorContext = true; - var emitter = new ILEmitter(ctx); - - EmitClassPrototypeRegistration( - il, typeBuilder, GetClassConstructorLength(classExpr.Methods)); - - if (_classes.DeferredDefinitions.TryGet(typeBuilder, 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) - { - foreach (var initializer in classExpr.StaticInitializers!) - { - switch (initializer) - { - case Stmt.Field field when field.IsStatic && field.ComputedKey != null: - _classes.DeferredDefinitions.TryGetFieldKey(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); - if (staticField.FieldType == typeof(object)) - emitter.EmitBoxIfNeeded(field.Initializer); - il.Emit(OpCodes.Stsfld, staticField); - break; - - case Stmt.StaticBlock block: - foreach (var stmt in block.Body) - emitter.EmitStatement(stmt); - break; - } - } - } - else - { - // Fallback for backward compatibility (fields only) - foreach (var field in classExpr.Fields.Where(f => f.IsStatic && f.Initializer != null)) - { - var staticField = _classExprs.StaticFields[classExpr][field.Name.Lexeme]; - emitter.EmitExpression(field.Initializer!); - if (staticField.FieldType == typeof(object)) - emitter.EmitBoxIfNeeded(field.Initializer!); - il.Emit(OpCodes.Stsfld, staticField); - } - } - - // Register symbol-keyed computed accessors (#281) and methods (#755) in the runtime registry, - // keyed by this class's Type so dynamic bracket get / for...of dispatch can find them. - EmitSymbolAccessorRegistrations(emitter, il, typeBuilder); - EmitSymbolMethodRegistrations(emitter, il, typeBuilder); - + EmitClassPrototypeRegistration(il, typeBuilder, GetClassConstructorLength(classExpr.Methods)); il.Emit(OpCodes.Ret); - if (deferredDefinition != null) - _classes.DeferredDefinitions.MarkInitializerEmitted(deferredDefinition); + EmitClassDefinitionInitializer(classExpr, typeBuilder); } /// @@ -796,7 +726,17 @@ private void EmitClassExpressionConstructor(Expr.ClassExpr classExpr, TypeBuilde else if (ResolveClassExprImplicitBaseConstructor(classExpr) is { } baseCtor) { foreach (var p in baseCtor.GetParameters()) - emitter.EmitOmittedArgument(p.ParameterType); + { + if (p.ParameterType == _runtime.ClassDefinitions.Type) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, EmitterTypeHelpers.SelfFieldReference(_classExprs.DefinitionFields[classExpr])); + il.Emit(OpCodes.Castclass, _runtime.ClassDefinitions.Type); + il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.Parent); + il.Emit(OpCodes.Isinst, _runtime.ClassDefinitions.Type); + } + else emitter.EmitOmittedArgument(p.ParameterType); + } il.Emit(OpCodes.Call, baseCtor); } else @@ -862,7 +802,15 @@ void EmitInstanceFieldInitializers() if (field.ComputedKey != null) { il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldsfld, _classes.DeferredDefinitions.RequireFieldKey(field)); + var keys = _classExprs.Factories[classExpr].Keys; + int keyIndex = Enumerable.Range(0, keys.Count) + .First(index => ReferenceEquals(keys[index], field.ComputedKey)); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, EmitterTypeHelpers.SelfFieldReference(_classExprs.DefinitionFields[classExpr])); + il.Emit(OpCodes.Castclass, _runtime.ClassDefinitions.Type); + il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.Keys); + il.Emit(OpCodes.Ldc_I4, keyIndex); + il.Emit(OpCodes.Ldelem_Ref); if (field.Initializer != null) { emitter.EmitExpression(field.Initializer); @@ -1106,11 +1054,20 @@ private void EmitClassExpressionStaticMethodBody(Expr.ClassExpr classExpr, Stmt. var typeBuilder = _classExprs.Builders[classExpr]; + var loweredMethod = method with { Parameters = [new Stmt.Parameter( + new Token(TokenType.IDENTIFIER, "__classDefinition", null, method.Name.Line), "any"), .. method.Parameters] }; + if (_generatorMethodFunctionDCKeys.TryGetValue(method, out var generatorKey)) + _generatorMethodFunctionDCKeys[loweredMethod] = generatorKey; + if (_asyncGeneratorMethodFunctionDCKeys.TryGetValue(method, out var asyncGeneratorKey)) + _asyncGeneratorMethodFunctionDCKeys[loweredMethod] = asyncGeneratorKey; + if (_asyncMethodFunctionDCKeys.TryGetValue(method, out var asyncKey)) + _asyncMethodFunctionDCKeys[loweredMethod] = asyncKey; + // Static generator methods route like a free function (no `this`), mirroring the class- // declaration static path (#765/#778). Async generator FIRST since it has both flags set. if (method.IsAsync && method.IsGenerator) { - EmitAsyncGeneratorMethodBody(methodBuilder, method, fieldsField: null, isInstanceMethod: false); + EmitAsyncGeneratorMethodBody(methodBuilder, loweredMethod, fieldsField: null, isInstanceMethod: false); return; } @@ -1121,19 +1078,20 @@ private void EmitClassExpressionStaticMethodBody(Expr.ClassExpr classExpr, Stmt. // EmitStaticAsyncMethodBody, which re-resolves from the declaration registry. if (method.IsAsync) { - EmitAsyncMethodBody(methodBuilder, method, fieldsField: null, isInstanceMethod: false, + EmitAsyncMethodBody(methodBuilder, loweredMethod, fieldsField: null, isInstanceMethod: false, currentClassName: _classExprs.Names[classExpr]); return; } if (method.IsGenerator) { - EmitGeneratorMethodBody(methodBuilder, method, fieldsField: null, isInstanceMethod: false); + EmitGeneratorMethodBody(methodBuilder, loweredMethod, fieldsField: null, isInstanceMethod: false); return; } var il = methodBuilder.GetILGenerator(); var ctx = CreateClassExpressionContext(il, classExpr, typeBuilder, null, methodBuilder); + ctx.ClassDefinitionParameterIndex = 0; ctx.IsInstanceMethod = false; SetupSyncMethodFunctionDisplayClass(ctx, il, method); @@ -1141,8 +1099,8 @@ private void EmitClassExpressionStaticMethodBody(Expr.ClassExpr classExpr, Stmt. var methodParams = methodBuilder.GetParameters(); for (int i = 0; i < method.Parameters.Count; i++) { - Type? paramType = i < methodParams.Length ? methodParams[i].ParameterType : null; - ctx.DefineParameter(method.Parameters[i].Name.Lexeme, i, paramType); + Type? paramType = i + 1 < methodParams.Length ? methodParams[i + 1].ParameterType : null; + ctx.DefineParameter(method.Parameters[i].Name.Lexeme, i + 1, paramType); } var emitter = new ILEmitter(ctx); @@ -1153,10 +1111,10 @@ private void EmitClassExpressionStaticMethodBody(Expr.ClassExpr classExpr, Stmt. emitter, method.Parameters, method.Body, - environmentParamTypes, - argumentOffset: 0); + environmentParamTypes.Skip(1).ToArray(), + argumentOffset: 1); InitializeSyncMethodCapturedParameters( - ctx, il, method, methodBuilder, argumentOffset: 0); + ctx, il, method, methodBuilder, argumentOffset: 1); if (method.Body != null) { @@ -1201,13 +1159,14 @@ private void EmitClassExpressionSymbolAccessors( var il = methodBuilder.GetILGenerator(); var ctx = CreateClassExpressionContext(il, classExpr, typeBuilder, accessor.IsStatic ? null : fieldsField, methodBuilder); ctx.IsInstanceMethod = !accessor.IsStatic; + if (accessor.IsStatic) ctx.ClassDefinitionParameterIndex = 0; if (accessor.Kind.Type == TokenType.SET && accessor.SetterParam != null) { var accessorParams = methodBuilder.GetParameters(); Type? paramType = accessorParams.Length > 0 ? accessorParams[0].ParameterType : null; // Instance setter: arg0 is `this`, value at index 1. Static: value at index 0. - int paramIndex = accessor.IsStatic ? 0 : 1; + int paramIndex = 1; ctx.DefineParameter(accessor.SetterParam.Name.Lexeme, paramIndex, paramType); } @@ -1268,7 +1227,8 @@ private void DefineClassExpressionSymbolMethods(Expr.ClassExpr classExpr, TypeBu if (method.IsStatic) attrs |= MethodAttributes.Static; - var mb = typeBuilder.DefineMethod(uniqueName, attrs, returnType, paramTypes); + var mb = typeBuilder.DefineMethod(uniqueName, attrs, returnType, + method.IsStatic ? [_runtime.ClassDefinitions.Type, .. paramTypes] : paramTypes); // Register under the unique name so EmitClassExpression(Static)MethodBody resolves the builder. if (method.IsStatic) diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionInitialization.cs b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionInitialization.cs new file mode 100644 index 000000000..f074b9458 --- /dev/null +++ b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionInitialization.cs @@ -0,0 +1,195 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public partial class ILCompiler +{ + private void EmitClassDefinitionInitializer(Expr.ClassExpr expression, TypeBuilder template) + { + var factory = _classExprs.Factories[expression]; + var il = factory.Initializer.GetILGenerator(); + var ctx = CreateClassExpressionContext(il, expression, template, null, factory.Initializer); + ctx.EmittingTypeBuilder = _programType; + ctx.ClassDefinitionParameterIndex = 0; + ctx.ClassDefinitionThisParameterIndex = 0; + var emitter = new ILEmitter(ctx); + var storage = _runtime.DescriptorStorage; + + var members = new List<(int Position, bool Static, Expr Key, MethodBuilder Method, bool? Getter, IReadOnlyList Parameters)>(); + foreach (var method in expression.Methods.Where(method => method.Body != null && + !method.IsPrivate && method.Name.Lexeme != "constructor" && method.ComputedKey == null)) + { + var builder = method.IsStatic ? _classExprs.StaticMethods[expression][method.Name.Lexeme] + : _classExprs.InstanceMethods[expression][method.Name.Lexeme]; + members.Add((method.Name.Start, method.IsStatic, new Expr.Literal(method.Name.Lexeme), builder, null, method.Parameters)); + } + foreach (var (method, key, builder) in _classes.ComputedMembers.GetMethods(template)) + members.Add((method.Name.Start, method.IsStatic, key, builder, null, method.Parameters)); + foreach (var (accessor, builder) in _classes.ComputedMembers.GetAccessors(template)) + members.Add((accessor.Name.Start, accessor.IsStatic, accessor.ComputedKey!, builder, accessor.Kind.Type == TokenType.GET, + accessor.SetterParam == null ? [] : [accessor.SetterParam])); + foreach (var accessor in (expression.Accessors ?? []).Where(accessor => !accessor.IsStatic && accessor.ComputedKey == null)) + { + var name = NamingConventions.ToPascalCase(accessor.Name.Lexeme); + var builder = accessor.Kind.Type == TokenType.GET ? _classExprs.Getters[expression][name] : _classExprs.Setters[expression][name]; + members.Add((accessor.Name.Start, false, new Expr.Literal(accessor.Name.Lexeme), builder, accessor.Kind.Type == TokenType.GET, + accessor.SetterParam == null ? [] : [accessor.SetterParam])); + } + foreach (var member in members.OrderBy(member => member.Position)) + Register(member.Static, member.Key, member.Method, member.Getter, member.Parameters); + + foreach (var initializer in expression.StaticInitializers ?? expression.Fields.Where(field => field.IsStatic).Cast().ToList()) + { + if (initializer is Stmt.Field field && field.IsStatic && !field.IsDeclare) + { + il.Emit(OpCodes.Ldarg_0); + EmitKey(field.ComputedKey ?? new Expr.Literal(field.Name.Lexeme)); + if (field.Initializer != null) + { + emitter.EmitExpression(field.Initializer); + emitter.EmitBoxIfNeeded(field.Initializer); + } + else il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + var value = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Stloc, value); + var key = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Stloc, key); + var owner = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Stloc, owner); + var descriptor = NewDescriptor(); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Ldloc, value); + il.Emit(OpCodes.Callvirt, storage.DescriptorValue.GetSetMethod()!); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Callvirt, storage.DescriptorEnumerable.GetSetMethod()!); + StoreDescriptor(owner, key, descriptor); + } + else if (initializer is Stmt.StaticBlock block) + foreach (var statement in block.Body) emitter.EmitStatement(statement); + } + il.Emit(OpCodes.Ret); + + void EmitKey(Expr key) + { + int index = Enumerable.Range(0, factory.Keys.Count).FirstOrDefault(index => ReferenceEquals(factory.Keys[index], key), -1); + if (index >= 0) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.Keys); + il.Emit(OpCodes.Ldc_I4, index); + il.Emit(OpCodes.Ldelem_Ref); + } + else if (key is Expr.Literal { Value: string name }) il.Emit(OpCodes.Ldstr, name); + else throw new InvalidOperationException("A computed definition key was not declared."); + } + + LocalBuilder NewDescriptor() + { + var result = il.DeclareLocal(storage.DescriptorType); + il.Emit(OpCodes.Newobj, storage.DescriptorConstructor); + il.Emit(OpCodes.Stloc, result); + foreach (var property in new[] { storage.DescriptorWritable, storage.DescriptorConfigurable }) + { + il.Emit(OpCodes.Ldloc, result); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Callvirt, property.GetSetMethod()!); + } + il.Emit(OpCodes.Ldloc, result); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Callvirt, storage.DescriptorEnumerable.GetSetMethod()!); + return result; + } + + void StoreDescriptor(LocalBuilder owner, LocalBuilder key, LocalBuilder descriptor) + { + var stringKey = il.DefineLabel(); + var done = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brtrue, stringKey); + il.Emit(OpCodes.Ldloc, owner); + il.Emit(OpCodes.Call, _runtime.Symbols.GetStorage); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "set_Item", _types.Object, _types.Object)); + il.Emit(OpCodes.Br, done); + il.MarkLabel(stringKey); + il.Emit(OpCodes.Ldloc, owner); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Call, storage.DefineProperty); + il.Emit(OpCodes.Pop); + il.MarkLabel(done); + } + + void Register(bool isStatic, Expr keyExpression, MethodBuilder method, bool? getter, IReadOnlyList parameters) + { + var owner = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); + if (!isStatic) il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.Prototype); + il.Emit(OpCodes.Stloc, owner); + var key = il.DeclareLocal(_types.Object); + EmitKey(keyExpression); + il.Emit(OpCodes.Stloc, key); + var descriptor = NewDescriptor(); + if (getter.HasValue) + { + // Preserve the other half when getter and setter share a key. + var noExisting = il.DefineLabel(); + var haveExisting = il.DefineLabel(); + var existing = il.DeclareLocal(storage.DescriptorType); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Isinst, _types.String); + il.Emit(OpCodes.Brfalse, noExisting); + il.Emit(OpCodes.Ldloc, owner); + il.Emit(OpCodes.Ldloc, key); + il.Emit(OpCodes.Castclass, _types.String); + il.Emit(OpCodes.Call, storage.GetPropertyDescriptor); + il.Emit(OpCodes.Stloc, existing); + il.Emit(OpCodes.Br, haveExisting); + il.MarkLabel(noExisting); + il.Emit(OpCodes.Ldloc, owner); + il.Emit(OpCodes.Call, _runtime.Symbols.GetStorage); + il.Emit(OpCodes.Ldloc, key); + var found = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloca, found); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "TryGetValue")); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldloc, found); + il.Emit(OpCodes.Isinst, storage.DescriptorType); + il.Emit(OpCodes.Stloc, existing); + il.MarkLabel(haveExisting); + var fresh = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, existing); + il.Emit(OpCodes.Brfalse, fresh); + il.Emit(OpCodes.Ldloc, existing); + il.Emit(OpCodes.Stloc, descriptor); + il.MarkLabel(fresh); + } + il.Emit(OpCodes.Ldloc, descriptor); + if (isStatic) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldtoken, factory.Template.IsGenericType ? EmitterTypeHelpers.ResolveMethod(factory.Template, method) : method); + il.Emit(OpCodes.Ldtoken, factory.Template); + il.Emit(OpCodes.Call, _types.MethodBaseGetMethodFromHandleWithType); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + } + else + { + var value = DefineDefinitionMethodValue(expression, method, parameters); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Newobj, value.Constructor); + EmitMethodInfoLiteral(il, value.Method, value.Type); + } + il.Emit(OpCodes.Newobj, _runtime.FunctionConstruction.Constructor); + il.Emit(OpCodes.Callvirt, (getter == true ? storage.DescriptorGetter : getter == false ? storage.DescriptorSetter : storage.DescriptorValue).GetSetMethod()!); + StoreDescriptor(owner, key, descriptor); + } + } + +} diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs new file mode 100644 index 000000000..a697e2e91 --- /dev/null +++ b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs @@ -0,0 +1,77 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public partial class ILCompiler +{ + private (TypeBuilder Type, ConstructorBuilder Constructor, MethodBuilder Method) DefineDefinitionMethodValue( + Expr.ClassExpr expression, MethodBuilder original, IReadOnlyList parameters) + { + var type = EmitTypeDefinitions.DefineType(_moduleBuilder, + $"$DefinitionMember_{_classExprs.Names[expression]}_{original.Name}", TypeAttributes.Public | TypeAttributes.Sealed, _types.Object); + var field = type.DefineField("Definition", _runtime.ClassDefinitions.Type, FieldAttributes.Assembly | FieldAttributes.InitOnly); + var constructor = type.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, [_runtime.ClassDefinitions.Type]); + var il = constructor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.GetDefaultConstructor(_types.Object)); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Stfld, field); + il.Emit(OpCodes.Ret); + var method = type.DefineMethod("Invoke", MethodAttributes.Public, original.ReturnType, + [_types.Object, .. parameters.Select(_ => _types.Object)]); + method.DefineParameter(1, ParameterAttributes.None, "__this"); + MarkExpectsThis(method); + MarkNonConstructible(method); + MarkPadsUndefined(method); + MarkFunctionLength(method, parameters); + MarkFunctionName(method, original.Name); + _classExprs.DefinitionMethods.Add(method, (expression, field)); + + Stmt.Function? source = expression.Methods.FirstOrDefault(source => + source.ComputedKey == null && source.Name.Lexeme == original.Name); + source ??= _classes.ComputedMembers.GetMethods(_classExprs.Builders[expression]) + .FirstOrDefault(entry => ReferenceEquals(entry.Builder, original)).Method; + if (source == null) + { + var accessor = _classes.ComputedMembers.GetAccessors(_classExprs.Builders[expression]) + .FirstOrDefault(entry => ReferenceEquals(entry.Method, original)).Accessor; + accessor ??= (expression.Accessors ?? []).First(accessor => + original.Name == $"{(accessor.Kind.Type == TokenType.GET ? "get" : "set")}_{NamingConventions.ToPascalCase(accessor.Name.Lexeme)}"); + source = new Stmt.Function(accessor.Name, null, null, parameters.ToList(), accessor.Body, null); + } + var guestThis = new Stmt.Parameter(new Token(TokenType.IDENTIFIER, "__this", null, 0), "any"); + var lowered = source with { Parameters = [guestThis, .. source.Parameters] }; + if (_asyncMethodFunctionDCKeys.TryGetValue(source, out var asyncKey)) _asyncMethodFunctionDCKeys[lowered] = asyncKey; + if (_generatorMethodFunctionDCKeys.TryGetValue(source, out var generatorKey)) _generatorMethodFunctionDCKeys[lowered] = generatorKey; + if (_asyncGeneratorMethodFunctionDCKeys.TryGetValue(source, out var asyncGeneratorKey)) _asyncGeneratorMethodFunctionDCKeys[lowered] = asyncGeneratorKey; + if (source.IsAsync && source.IsGenerator) EmitAsyncGeneratorMethodBody(method, lowered, null, currentClassName: _classExprs.Names[expression]); + else if (source.IsAsync) EmitAsyncMethodBody(method, lowered, null, currentClassName: _classExprs.Names[expression]); + else if (source.IsGenerator) EmitGeneratorMethodBody(method, lowered, null, currentClassName: _classExprs.Names[expression]); + else + { + il = method.GetILGenerator(); + var ctx = CreateClassExpressionContext(il, expression, _classExprs.Builders[expression], null, method); + ctx.EmittingTypeBuilder = type; + ctx.IsInstanceMethod = true; + ctx.ClassDefinitionOwnerField = field; + ctx.GuestThisVariableName = "__this"; + foreach (var parameter in expression.TypeParams ?? []) ctx.GenericTypeParameters[parameter.Name.Lexeme] = _types.Object; + ctx.DefineParameter("__this", 1, _types.Object); + SetupSyncMethodFunctionDisplayClass(ctx, il, source); + for (int i = 0; i < parameters.Count; i++) ctx.DefineParameter(parameters[i].Name.Lexeme, i + 2, _types.Object); + var emitter = new ILEmitter(ctx); + EmitFunctionEnvironmentPrologue(il, ctx, emitter, parameters.ToList(), source.Body, + parameters.Select(_ => _types.Object).ToArray(), argumentOffset: 2); + InitializeSyncMethodCapturedParameters(ctx, il, source, method, argumentOffset: 2); + foreach (var statement in source.Body ?? []) emitter.EmitStatement(statement); + if (emitter.HasDeferredReturns) emitter.FinalizeReturns(); + else { EmitDefaultReturnValue(il, method.ReturnType); il.Emit(OpCodes.Ret); } + } + type.CreateType(); + return (type, constructor, method); + } + +} diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs index 72592c0dc..d00855bae 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs @@ -55,6 +55,8 @@ private void DefineClassExpressionFactory(Expr.ClassExpr expression, TypeBuilder string name = expression.Name?.Lexeme ?? _classExprs.VarToClassExpr.FirstOrDefault(entry => ReferenceEquals(entry.Value, expression)).Key ?? ""; - _classExprs.Factories[expression] = new(method, template, name, GetClassConstructorLength(expression.Methods)); + var initializer = _programType.DefineMethod("$Initialize_" + _classExprs.Names[expression], + MethodAttributes.Assembly | MethodAttributes.Static, _types.Void, [_runtime.ClassDefinitions.Type]); + _classExprs.Factories[expression] = new(method, template, name, GetClassConstructorLength(expression.Methods), initializer, []); } } diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs b/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs index 2d721fb0e..847ce2f87 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs @@ -1011,6 +1011,33 @@ private void EmitClassPrototypeFieldFallback(ILGenerator il) il.Emit(OpCodes.Ldloc, prototypeLocal); il.Emit(OpCodes.Ceq); il.Emit(OpCodes.Brtrue, skipLabel); + var descriptor = il.DeclareLocal(_runtime.DescriptorStorage.DescriptorType); + var noDescriptor = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, _runtime.ClassDefinitions.InstanceInterface); + il.Emit(OpCodes.Brfalse, noDescriptor); + il.Emit(OpCodes.Ldloc, prototypeLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, _runtime.DescriptorStorage.GetPropertyDescriptor); + il.Emit(OpCodes.Stloc, descriptor); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Brfalse, noDescriptor); + var data = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, _runtime.DescriptorStorage.DescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Brfalse, data); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, _runtime.DescriptorStorage.DescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Call, _runtime.Invocation.Method); + il.Emit(OpCodes.Ret); + il.MarkLabel(data); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, _runtime.DescriptorStorage.DescriptorValue.GetGetMethod()!); + il.Emit(OpCodes.Ret); + il.MarkLabel(noDescriptor); il.Emit(OpCodes.Ldloc, prototypeLocal); il.Emit(OpCodes.Isinst, _runtime.ObjectFields.Interface); il.Emit(OpCodes.Brfalse, skipLabel); diff --git a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs index ae2ca5bc4..81dce0235 100644 --- a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs +++ b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs @@ -93,8 +93,9 @@ private CompilationContext CreateBaseCompilationContext(ILGenerator il, MethodBa TopLevelNumericConstantValues = _topLevelNumericConstantValues, ClassExprBuilders = _classExprs.Builders, ClassExprFactories = _classExprs.Factories, + ClassExprDefinitionFields = _classExprs.DefinitionFields, ClassExprStaticMethods = _classExprs.StaticMethods, - ClassExprCaptureFields = _classExprs.CaptureFields, + ClassExprCaptureSlots = _classExprs.CaptureSlots, DeferredClassDefinitions = _classes.DeferredDefinitions, BlockScopedClassBuilders = _classes.BlockScopedBuilders, ClassRegistry = GetClassRegistry(), @@ -170,6 +171,27 @@ private CompilationContext CreateModuleMemberContext(ILGenerator il, MethodBase? return ctx; } + private void ApplyClassDefinitionStateMachineContext(CompilationContext ctx, MethodBuilder method, bool isInstance) + { + if (_classExprs.DefinitionMethods.TryGetValue(method, out var owner)) + { + ctx.CurrentClassExpr = owner.Expression; + ctx.CurrentClassBuilder = _classExprs.Builders[owner.Expression]; + ctx.ClassDefinitionOwnerField = owner.Definition; + ctx.GuestThisVariableName = "__this"; + foreach (var parameter in owner.Expression.TypeParams ?? []) + ctx.GenericTypeParameters[parameter.Name.Lexeme] = _types.Object; + } + else + { + ctx.CurrentClassExpr = _classExprs.Builders.FirstOrDefault(entry => ReferenceEquals(entry.Value, method.DeclaringType)).Key; + } + if (ctx.CurrentClassExpr != null) + ctx.ArrowFunctionDCFields = _closures.ArrowFunctionDCFields.Count > 0 ? _closures.ArrowFunctionDCFields : null; + if (ctx.CurrentClassExpr != null && !isInstance) + ctx.ClassDefinitionVariableName = "__classDefinition"; + } + /// /// Creates the context that emits a module's (ESM or CommonJS) top-level statements. /// @@ -276,7 +298,8 @@ private CompilationContext CreateNestedAsyncArrowContext(ILGenerator il, Compila TypeEmitterRegistry = parentCtx.TypeEmitterRegistry, ClassExprBuilders = parentCtx.ClassExprBuilders, ClassExprFactories = parentCtx.ClassExprFactories, - ClassExprCaptureFields = parentCtx.ClassExprCaptureFields, + ClassExprDefinitionFields = parentCtx.ClassExprDefinitionFields, + ClassExprCaptureSlots = parentCtx.ClassExprCaptureSlots, BlockScopedClassBuilders = parentCtx.BlockScopedClassBuilders, DeferredClassDefinitions = parentCtx.DeferredClassDefinitions, IsStrictMode = parentCtx.IsStrictMode, diff --git a/src/SharpTS/Compilation/ILCompiler.Generators.cs b/src/SharpTS/Compilation/ILCompiler.Generators.cs index b1e60e9fd..4ee670ad0 100644 --- a/src/SharpTS/Compilation/ILCompiler.Generators.cs +++ b/src/SharpTS/Compilation/ILCompiler.Generators.cs @@ -572,6 +572,8 @@ private void EmitGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Function { // Analyze generator function to determine yield points and hoisted variables var analysis = _generators.Analyzer.Analyze(method); + if (isInstanceMethod && (_classExprs.DefinitionMethods.ContainsKey(methodBuilder) || _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, methodBuilder.DeclaringType)))) + analysis = analysis with { UsesThis = true }; // Build state machine type. A static generator method (#692) has no `this`/instance fields, so it // is set up like a free function (isInstanceMethod: false, static stub). The type name uses the @@ -581,7 +583,8 @@ private void EmitGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Function $"{methodBuilder.DeclaringType!.Name}_{methodBuilder.Name}", analysis, isInstanceMethod: isInstanceMethod, - runtime: _runtime + runtime: _runtime, + hasDynamicThis: !isInstanceMethod && _classExprs.Builders.Values.Any(builder => ReferenceEquals(builder, methodBuilder.DeclaringType)) ); RegisterStateMachine( methodBuilder, @@ -627,6 +630,7 @@ private void EmitGeneratorMethodBody(MethodBuilder methodBuilder, Stmt.Function // its QUALIFIED class name so nested private member access resolves under modules — #720). ctx.CurrentClassName = currentClassName ?? methodBuilder.DeclaringType?.Name; ctx.CurrentClassBuilder = methodBuilder.DeclaringType as TypeBuilder; + ApplyClassDefinitionStateMachineContext(ctx, methodBuilder, isInstanceMethod); // Captured outer variables are read live (by reference), not snapshotted (#541). // TopLevelStaticVars covers module-level vars that aren't in the entry-point display class. ApplyCapturedTopLevelVariableAccess(ctx); diff --git a/src/SharpTS/Compilation/ILCompiler.IteratorStubs.cs b/src/SharpTS/Compilation/ILCompiler.IteratorStubs.cs index 42632d4a3..d2bbcc0d3 100644 --- a/src/SharpTS/Compilation/ILCompiler.IteratorStubs.cs +++ b/src/SharpTS/Compilation/ILCompiler.IteratorStubs.cs @@ -125,10 +125,11 @@ private void EmitIteratorMethodStub( il.Emit(OpCodes.Newobj, smBuilder.Constructor); - if (isInstanceMethod && smBuilder.ThisField != null) + if (smBuilder.ThisField != null) { il.Emit(OpCodes.Dup); // Keep state machine reference on stack - il.Emit(OpCodes.Ldarg_0); // 'this' is at arg 0 for instance methods + if (isInstanceMethod) il.Emit(OpCodes.Ldarg_0); + else il.Emit(OpCodes.Ldsfld, _runtime.FunctionValues.CurrentThisField); il.Emit(OpCodes.Stfld, smBuilder.ThisField); } diff --git a/src/SharpTS/Compilation/ILCompiler.State.cs b/src/SharpTS/Compilation/ILCompiler.State.cs index 984223b1f..532214b93 100644 --- a/src/SharpTS/Compilation/ILCompiler.State.cs +++ b/src/SharpTS/Compilation/ILCompiler.State.cs @@ -419,7 +419,8 @@ private sealed class ClassExpressionCompilationState public Dictionary Factories { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary Superclass { get; } = new(ReferenceEqualityComparer.Instance); public Dictionary EnclosingClass { get; } = new(ReferenceEqualityComparer.Instance); - public Dictionary> CaptureFields { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary> CaptureSlots { get; } = new(ReferenceEqualityComparer.Instance); + public Dictionary DefinitionMethods { get; } = new(ReferenceEqualityComparer.Instance); } /// diff --git a/src/SharpTS/Compilation/ILCompiler.cs b/src/SharpTS/Compilation/ILCompiler.cs index 6028ca101..c1cd8b831 100644 --- a/src/SharpTS/Compilation/ILCompiler.cs +++ b/src/SharpTS/Compilation/ILCompiler.cs @@ -927,6 +927,8 @@ private void Phase4_DefineDeclarations(List statements) private void PredefineTopLevelClassExpressionTypes(IEnumerable statements) { + var collector = new ClassExpressionTemplateCollector(CollectClassExpression); + foreach (var statement in statements) collector.Visit(statement); foreach (var stmt in statements) { Expr.ClassExpr? classExpr = stmt switch @@ -946,6 +948,15 @@ private void PredefineTopLevelClassExpressionTypes(IEnumerable statements) } } + private sealed class ClassExpressionTemplateCollector(Action collect) : Parsing.Visitors.AstVisitorBase + { + protected override void VisitClassExpr(Expr.ClassExpr expression) + { + collect(expression); + base.VisitClassExpr(expression); + } + } + /// /// Phase 5: Collect all arrow functions and define class expressions. /// diff --git a/src/SharpTS/Compilation/ILEmitter.Calls.Closures.cs b/src/SharpTS/Compilation/ILEmitter.Calls.Closures.cs index e4b54475a..b850bff24 100644 --- a/src/SharpTS/Compilation/ILEmitter.Calls.Closures.cs +++ b/src/SharpTS/Compilation/ILEmitter.Calls.Closures.cs @@ -243,14 +243,14 @@ private void EmitDisplayInstanceFieldPopulation(Expr.ArrowFunction af, TypeBuild // Populate $functionDC field if this arrow captures function-level variables if (_ctx.ArrowFunctionDCFields?.TryGetValue(af, out var functionDCField) == true) { - if (_ctx.FunctionDisplayClassLocal != null) + if (_ctx.FunctionDisplayClassLocal?.LocalType == functionDCField.FieldType) { // In function body - use local variable IL.Emit(OpCodes.Dup); // Keep display class on stack IL.Emit(OpCodes.Ldloc, _ctx.FunctionDisplayClassLocal); IL.Emit(OpCodes.Stfld, functionDCField); } - else if (_ctx.CurrentArrowFunctionDCField != null) + else if (_ctx.CurrentArrowFunctionDCField?.FieldType == functionDCField.FieldType) { // In arrow body - get from parent arrow's $functionDC field IL.Emit(OpCodes.Dup); // Keep display class on stack @@ -258,6 +258,13 @@ private void EmitDisplayInstanceFieldPopulation(Expr.ArrowFunction af, TypeBuild IL.Emit(OpCodes.Ldfld, _ctx.CurrentArrowFunctionDCField); IL.Emit(OpCodes.Stfld, functionDCField); } + else + { + IL.Emit(OpCodes.Dup); + if (TryEmitClassDefinitionEnvironment(functionDCField.FieldType)) + IL.Emit(OpCodes.Stfld, functionDCField); + else IL.Emit(OpCodes.Pop); + } } // Populate $arrowDC field if this arrow captures arrow-scope variables. diff --git a/src/SharpTS/Compilation/ILEmitter.Expressions.cs b/src/SharpTS/Compilation/ILEmitter.Expressions.cs index 884f9aa8a..652a0068c 100644 --- a/src/SharpTS/Compilation/ILEmitter.Expressions.cs +++ b/src/SharpTS/Compilation/ILEmitter.Expressions.cs @@ -48,6 +48,7 @@ protected override void EmitVariable(Expr.Variable v) if (TryEmitDefaultParameterTdz(name)) return; if (TryEmitClassDefinitionSelf(name)) return; + if (TryEmitClassDefinitionCapture(name)) return; if (_ctx.LexicalInitializerTdzName == name) { @@ -404,6 +405,16 @@ protected override void EmitAssign(Expr.Assign a) // rest of the module body and ultimately trips PathStackDepth at the final ret. if (TryEmitCjsAssign(a)) return; + if (TryGetClassDefinitionCaptureSlot(a.Name.Lexeme, out _)) + { + EmitExpression(a.Value); + EnsureBoxed(); + IL.Emit(OpCodes.Dup); + TryEmitStoreClassDefinitionCapture(a.Name.Lexeme); + SetStackUnknown(); + return; + } + // Promoted string-accumulator append (#857): `s = s + E` where `s` is a StringBuilder slot. // Emit `sb.Append(E)` instead of evaluating `s + E` (String.Concat) and storing — turning the // O(n²) accumulation into O(n). The analyzer promotes `s` only when every such append is in @@ -845,6 +856,19 @@ private void EmitLexicalAssignmentTdzGuard(string name) protected override void EmitThis() { + if (TryEmitGuestThis()) return; + if (_ctx.ClassDefinitionThisParameterIndex is { } parameter) + { + IL.Emit(OpCodes.Ldarg, parameter); + SetStackUnknown(); + return; + } + if (_ctx.ClassDefinitionParameterIndex != null) + { + IL.Emit(OpCodes.Ldsfld, _ctx.Runtime!.FunctionValues.CurrentThisField); + SetStackUnknown(); + return; + } _resolver.LoadThis(); SetStackUnknown(); } diff --git a/src/SharpTS/Compilation/ILEmitter.Operators.cs b/src/SharpTS/Compilation/ILEmitter.Operators.cs index a7dc2f033..1cec3c4dc 100644 --- a/src/SharpTS/Compilation/ILEmitter.Operators.cs +++ b/src/SharpTS/Compilation/ILEmitter.Operators.cs @@ -746,6 +746,7 @@ private bool TryEmitExternalUnaryOperator(Expr.Unary unary) protected override void EmitCompoundAssign(Expr.CompoundAssign ca) { + if (TryGetClassDefinitionCaptureSlot(ca.Name.Lexeme, out _)) { base.EmitCompoundAssign(ca); return; } if (TryEmitExternalCompoundAssign(ca)) return; @@ -970,6 +971,7 @@ private void ConvertExternalOperatorStorage(Type storageType) protected override void EmitLogicalAssign(Expr.LogicalAssign la) { + if (TryGetClassDefinitionCaptureSlot(la.Name.Lexeme, out _)) { base.EmitLogicalAssign(la); return; } // An unresolvable reference throws before the logical operator can // inspect its value. Letting the generic store path handle this left // incompatible stack shapes at the join label (the failed store does @@ -1937,6 +1939,14 @@ private void EmitIntegerCounterIncrement(string name, bool isPlus, bool postfix) /// private void EmitStoreIncrementedVariable(string name, bool isTypedDouble, bool resultIsUnboxedDouble) { + if (TryGetClassDefinitionCaptureSlot(name, out _)) + { + if (isTypedDouble) IL.Emit(OpCodes.Box, _ctx.Types.Double); + TryEmitStoreClassDefinitionCapture(name); + if (resultIsUnboxedDouble) IL.Emit(OpCodes.Box, _ctx.Types.Double); + SetStackUnknown(); + return; + } // Per-iteration loop-binding cell (#650): write the new value through the StrongBox so closures // that captured this iteration's cell observe it. if (_ctx.CellBindingLocals.TryGetValue(name, out var cell)) diff --git a/src/SharpTS/Compilation/ILEmitter.Properties.cs b/src/SharpTS/Compilation/ILEmitter.Properties.cs index 7d6f8174a..f6b4d0e09 100644 --- a/src/SharpTS/Compilation/ILEmitter.Properties.cs +++ b/src/SharpTS/Compilation/ILEmitter.Properties.cs @@ -18,6 +18,7 @@ public partial class ILEmitter { protected override void EmitGet(Expr.Get g) { + if (TryEmitGuestThisGet(g)) return; if (TryEmitStableRecordDestructureGet(g)) return; @@ -218,7 +219,7 @@ protected override void EmitGet(Expr.Get g) // Handle static member access via 'this' in static context (static blocks, static methods) // In static blocks, 'this' refers to the class constructor, so this.property accesses static members - if (g.Object is Expr.This && !_ctx.IsInstanceMethod && _ctx.CurrentClassBuilder != null) + if (g.Object is Expr.This && !_ctx.IsInstanceMethod && _ctx.ClassDefinitionParameterIndex == null && _ctx.CurrentClassBuilder != null) { // Use cached CurrentClassName instead of linear search string? currentClassName = _ctx.CurrentClassName; @@ -315,18 +316,7 @@ protected override void EmitGet(Expr.Get g) } } - // Handle static member access via class expression variable - if (g.Object is Expr.Variable classExprVar && - _ctx.VarToClassExpr != null && - _ctx.VarToClassExpr.TryGetValue(classExprVar.Name.Lexeme, out var classExpr) && - _ctx.ClassExprStaticFields != null && - _ctx.ClassExprStaticFields.TryGetValue(classExpr, out var exprStaticFields) && - exprStaticFields.TryGetValue(g.Name.Lexeme, out var exprStaticField)) - { - IL.Emit(OpCodes.Ldsfld, exprStaticField); - SetStackUnknown(); - return; - } + // Handle static property access on external .NET types (@DotNetType) if (g.Object is Expr.Variable extVar && _ctx.TypeMapper.ExternalTypes.TryGetValue(extVar.Name.Lexeme, out var externalType)) @@ -1053,7 +1043,7 @@ protected override void EmitSet(Expr.Set s) } // Handle static property assignment via 'this' in static context (static blocks, static methods) - if (s.Object is Expr.This && !_ctx.IsInstanceMethod && _ctx.CurrentClassBuilder != null) + if (s.Object is Expr.This && !_ctx.IsInstanceMethod && _ctx.ClassDefinitionParameterIndex == null && _ctx.CurrentClassBuilder != null) { // First check for class expressions if (_ctx.CurrentClassExpr != null && diff --git a/src/SharpTS/Compilation/RuntimeEmitter.ClassCaptures.cs b/src/SharpTS/Compilation/RuntimeEmitter.ClassCaptures.cs new file mode 100644 index 000000000..bbfcf9cd1 --- /dev/null +++ b/src/SharpTS/Compilation/RuntimeEmitter.ClassCaptures.cs @@ -0,0 +1,95 @@ +using System.Reflection.Emit; + +namespace SharpTS.Compilation; + +public partial class RuntimeEmitter +{ + private void EmitClassCaptureBodies(TypeBuilder captureType, FieldBuilder owner, FieldBuilder field, + FieldBuilder captures, MethodBuilder read, MethodBuilder write, MethodBuilder find) + { + var il = read.GetILGenerator(); + var cell = il.DeclareLocal(captureType); + var snapshot = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, captureType); + il.Emit(OpCodes.Stloc, cell); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Brfalse, snapshot); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Ldfld, field); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Ldfld, owner); + il.Emit(OpCodes.Callvirt, _types.FieldInfoGetValue); + il.Emit(OpCodes.Ret); + il.MarkLabel(snapshot); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ret); + + il = write.GetILGenerator(); + cell = il.DeclareLocal(captureType); + snapshot = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, captures); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Isinst, captureType); + il.Emit(OpCodes.Stloc, cell); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Brfalse, snapshot); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Ldfld, field); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Ldfld, owner); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.FieldInfo, "SetValue", _types.Object, _types.Object)); + il.Emit(OpCodes.Ret); + il.MarkLabel(snapshot); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, captures); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Ret); + + il = find.GetILGenerator(); + var index = il.DeclareLocal(_types.Int32); + cell = il.DeclareLocal(captureType); + var loop = il.DefineLabel(); + var next = il.DefineLabel(); + var done = il.DefineLabel(); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, index); + il.MarkLabel(loop); + il.Emit(OpCodes.Ldloc, index); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, captures); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Bge, done); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, captures); + il.Emit(OpCodes.Ldloc, index); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Isinst, captureType); + il.Emit(OpCodes.Stloc, cell); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Brfalse, next); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Ldfld, owner); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "IsInstanceOfType", _types.Object)); + il.Emit(OpCodes.Brfalse, next); + il.Emit(OpCodes.Ldloc, cell); + il.Emit(OpCodes.Ldfld, owner); + il.Emit(OpCodes.Ret); + il.MarkLabel(next); + il.Emit(OpCodes.Ldloc, index); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, index); + il.Emit(OpCodes.Br, loop); + il.MarkLabel(done); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ret); + } +} diff --git a/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs index c760fb030..7a9eb43eb 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs @@ -23,15 +23,42 @@ private void DefineClassDefinitionTypes(ModuleBuilder module, EmittedClassDefini var template = type.DefineField("Template", _types.Type, FieldAttributes.Assembly); var factory = type.DefineField("Factory", factoryType, FieldAttributes.Assembly); var prototype = type.DefineField("Prototype", _types.Object, FieldAttributes.Assembly); + var keys = type.DefineField("Keys", _types.ObjectArray, FieldAttributes.Assembly); + var parent = type.DefineField("Parent", _types.Object, FieldAttributes.Assembly); + var captures = type.DefineField("Captures", _types.ObjectArray, FieldAttributes.Assembly); var create = type.DefineMethod("Create", MethodAttributes.Public | MethodAttributes.Static, - type, [_types.Type, factoryType, _types.String, _types.Double, _types.Object]); + type, [_types.Type, factoryType, _types.String, _types.Double, _types.Object, _types.ObjectArray, _types.ObjectArray]); var construct = type.DefineMethod("Construct", MethodAttributes.Public | MethodAttributes.Static, _types.Object, [type, _types.ObjectArray]); var read = type.DefineMethod("ReadProperty", MethodAttributes.Public | MethodAttributes.Static, _types.Object, [type, _types.String]); var invoke = type.DefineMethod("Invoke", MethodAttributes.Public, _types.Object, [_types.ObjectArray]); + var captureType = module.DefineType("$ClassCapture", TypeAttributes.Public | TypeAttributes.Sealed); + var captureOwner = captureType.DefineField("Owner", _types.Object, FieldAttributes.Assembly); + var captureField = captureType.DefineField("Field", _types.FieldInfo, FieldAttributes.Assembly); + var captureConstructor = captureType.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, + [_types.Object, _types.FieldInfo]); + il = captureConstructor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.GetDefaultConstructor(_types.Object)); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Stfld, captureOwner); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Stfld, captureField); + il.Emit(OpCodes.Ret); + var readCapture = type.DefineMethod("ReadCapture", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object]); + var writeCapture = type.DefineMethod("WriteCapture", MethodAttributes.Public | MethodAttributes.Static, + _types.Void, [type, _types.Int32, _types.Object]); + var findEnvironment = type.DefineMethod("FindEnvironment", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [type, _types.Type]); + EmitClassCaptureBodies(captureType, captureOwner, captureField, captures, readCapture, writeCapture, findEnvironment); + captureType.CreateType(); definitions.Declare(new(type, instanceInterface, instanceInterface.GetMethod(getter.Name)!, - constructor, template, factory, prototype, create, construct, read, invoke)); + constructor, template, factory, prototype, keys, parent, captures, create, construct, read, invoke, + captureType, captureConstructor, captureOwner, captureField, readCapture, writeCapture, findEnvironment)); } private void EmitClassDefinitionBodies(EmittedRuntime runtime) @@ -50,6 +77,15 @@ private void EmitClassDefinitionBodies(EmittedRuntime runtime) il.Emit(OpCodes.Ldloc, definition); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Stfld, definitions.Factory); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldarg_S, (byte)5); + il.Emit(OpCodes.Stfld, definitions.Keys); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldarg_S, (byte)4); + il.Emit(OpCodes.Stfld, definitions.Parent); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldarg_S, (byte)6); + il.Emit(OpCodes.Stfld, definitions.Captures); il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.DictionaryStringObject)); il.Emit(OpCodes.Newobj, runtime.ObjectStorage.Constructor); il.Emit(OpCodes.Stloc, prototype); @@ -240,29 +276,4 @@ void EmitDataDescriptor(LocalBuilder owner, string name, Action value, bool writ definitions.CompleteEmission(); } - private void EmitClassDefinitionTemplateReceiver(ILGenerator il, EmittedClassDefinitionRuntime definitions) - { - var ordinary = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Isinst, definitions.Type); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Brfalse, ordinary); - il.Emit(OpCodes.Ldfld, definitions.Template); - il.Emit(OpCodes.Starg_S, (byte)0); - var ready = il.DefineLabel(); - il.Emit(OpCodes.Br, ready); - il.MarkLabel(ordinary); - il.Emit(OpCodes.Pop); - il.MarkLabel(ready); - } - - private void EmitClassDefinitionSymbolReceiver(ILGenerator il, EmittedClassDefinitionRuntime definitions, Type symbolType) - { - var ordinary = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Isinst, symbolType); - il.Emit(OpCodes.Brfalse, ordinary); - EmitClassDefinitionTemplateReceiver(il, definitions); - il.MarkLabel(ordinary); - } } diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Iterator.cs b/src/SharpTS/Compilation/RuntimeEmitter.Iterator.cs index a395db1bc..6a41864de 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Iterator.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Iterator.cs @@ -32,6 +32,7 @@ MethodInfo GetIndex private readonly record struct IteratorFunctionInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedDescriptorStorageRuntime DescriptorStorage, EmittedFunctionConstructionRuntime FunctionConstruction, EmittedGeneratorRuntime Generators, @@ -879,7 +880,7 @@ private void EmitIteratorMethodsBasic(TypeBuilder typeBuilder, EmittedRuntime ru EmitGetIteratorFunction( typeBuilder, runtime.IteratorProtocol, - new IteratorFunctionInputs(runtime.DescriptorStorage, runtime.FunctionConstruction, runtime.Generators, runtime.NodeStreams, runtime.ObjectRead, runtime.SymbolAccessors, runtime.Symbols, runtime.Sentinels.UndefinedInstance) + new IteratorFunctionInputs(runtime.ClassDefinitions, runtime.DescriptorStorage, runtime.FunctionConstruction, runtime.Generators, runtime.NodeStreams, runtime.ObjectRead, runtime.SymbolAccessors, runtime.Symbols, runtime.Sentinels.UndefinedInstance) ); EmitIteratorClose( typeBuilder, @@ -1267,6 +1268,15 @@ private void EmitGetIteratorFunction(TypeBuilder typeBuilder, EmittedIteratorPro il.Emit(OpCodes.Ret); il.MarkLabel(tryRegistryLabel); + var ordinaryRegistry = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.InstanceInterface); + il.Emit(OpCodes.Brfalse, ordinaryRegistry); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, inputs.ObjectRead.Index); + il.Emit(OpCodes.Ret); + il.MarkLabel(ordinaryRegistry); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Call, inputs.SymbolAccessors.FindMethod); @@ -1274,7 +1284,6 @@ private void EmitGetIteratorFunction(TypeBuilder typeBuilder, EmittedIteratorPro il.Emit(OpCodes.Brtrue, registryValueLabel); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Br, returnUndefinedLabel); - il.MarkLabel(registryValueLabel); il.Emit(OpCodes.Ret); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs index 30d4b0d06..6de6d3af9 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs @@ -48,6 +48,7 @@ private readonly record struct GetIndexInputs( MethodBuilder InvokeMethodUnwrapped, MethodBuilder InvokeMethodValue, EmittedObjectDescriptorRuntime ObjectDescriptors, + EmittedObjectPrototypeRuntime ObjectPrototypes, EmittedObjectStorageRuntime ObjectStorage, EmittedRegExpRuntime RegExps, EmittedStringCoercionRuntime StringCoercion, @@ -120,7 +121,6 @@ private void EmitGetIndex(TypeBuilder typeBuilder, EmittedObjectReadRuntime obje objectRead.Index = method; var il = method.GetILGenerator(); - EmitClassDefinitionSymbolReceiver(il, inputs.ClassDefinitions, inputs.Symbols.Type); var arrayLabel = il.DefineLabel(); var stringLabel = il.DefineLabel(); var dictLabel = il.DefineLabel(); @@ -287,6 +287,29 @@ private void EmitGetIndex(TypeBuilder typeBuilder, EmittedObjectReadRuntime obje var symbolFoundLabel = il.DefineLabel(); il.Emit(OpCodes.Brtrue, symbolFoundLabel); + // Definition-owned computed members live on the guest prototype, + // independently of the shared CLR template's symbol registry. + var prototype = il.DeclareLocal(_types.Object); + var prototypeLoop = il.DefineLabel(); + var noPrototype = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, inputs.ObjectPrototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, prototype); + il.MarkLabel(prototypeLoop); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Brfalse, noPrototype); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Call, inputs.Symbols.GetStorage); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, symbolValueLocal); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "TryGetValue")); + il.Emit(OpCodes.Brtrue, symbolFoundLabel); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Call, inputs.ObjectPrototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, prototype); + il.Emit(OpCodes.Br, prototypeLoop); + il.MarkLabel(noPrototype); + // #266: symbol-keyed class accessor (`get [Symbol.x]() {...}`). After an // own symbol data property misses, consult the per-class accessor registry // (walking the base chain). A found getter is a MethodInfo invoked with the @@ -1194,7 +1217,6 @@ private void EmitSetIndex(TypeBuilder typeBuilder, EmittedObjectWriteRuntime obj objectWrite.Index = method; var il = method.GetILGenerator(); - EmitClassDefinitionSymbolReceiver(il, inputs.ClassDefinitions, inputs.Symbols.Type); var dictLabel = il.DefineLabel(); var dictStringKeyLabel = il.DefineLabel(); var dictNumericKeyLabel = il.DefineLabel(); @@ -1415,6 +1437,7 @@ private void EmitSetIndex(TypeBuilder typeBuilder, EmittedObjectWriteRuntime obj // strict). il.MarkLabel(symbolKeyLabel); { + EmitInheritedSymbolSetter(il, inputs.ObjectPrototypes, inputs.Symbols, inputs.DescriptorStorage, inputs.InvokeMethodValue); // #266: symbol-keyed class accessor setter (`set [Symbol.x](v) {...}`). // A registered setter takes the write (accessor semantics) instead of // storing a data property. Found setter is a MethodInfo invoked with the @@ -1943,6 +1966,64 @@ void SkipIfMathConst(string n) il.Emit(OpCodes.Ret); } + private void EmitInheritedSymbolSetter(ILGenerator il, EmittedObjectPrototypeRuntime prototypes, + EmittedSymbolRuntime symbols, EmittedDescriptorStorageRuntime storage, MethodBuilder invoke) + { + var done = il.DefineLabel(); + var prototype = il.DeclareLocal(_types.Object); + var value = il.DeclareLocal(_types.Object); + var descriptor = il.DeclareLocal(storage.DescriptorType); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, symbols.GetStorage); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, value); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "TryGetValue")); + il.Emit(OpCodes.Brtrue, done); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, prototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, prototype); + var loop = il.DefineLabel(); + var next = il.DefineLabel(); + il.MarkLabel(loop); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Brfalse, done); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Call, symbols.GetStorage); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloca, value); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryObjectObject, "TryGetValue")); + il.Emit(OpCodes.Brfalse, next); + il.Emit(OpCodes.Ldloc, value); + il.Emit(OpCodes.Isinst, storage.DescriptorType); + il.Emit(OpCodes.Stloc, descriptor); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Brfalse, done); + var noSetter = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, storage.DescriptorSetter.GetGetMethod()!); + il.Emit(OpCodes.Brfalse, noSetter); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, storage.DescriptorSetter.GetGetMethod()!); + 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.Call, invoke); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ret); + il.MarkLabel(noSetter); + il.Emit(OpCodes.Br, done); + il.MarkLabel(next); + il.Emit(OpCodes.Ldloc, prototype); + il.Emit(OpCodes.Call, prototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, prototype); + il.Emit(OpCodes.Br, loop); + il.MarkLabel(done); + } + /// /// Emits DeleteIndex(object obj, object key) -> bool /// Handles both symbol keys and string keys for delete operations. diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs index 5a56dc0a6..d3522e696 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.SetProperty.cs @@ -88,6 +88,7 @@ Type UndefinedType ); private readonly record struct SetPropertyStrictInputs( + EmittedClassDefinitionRuntime ClassDefinitions, EmittedArrayOperationsRuntime ArrayOperations, EmittedArrayStorageRuntime ArrayStorage, TypeBuilder BoundAnyFunctionType, @@ -787,7 +788,6 @@ private void EmitSetProperty(EmittedObjectWriteRuntime objectWrite, SetPropertyI var method = (MethodBuilder)objectWrite.Property; var il = method.GetILGenerator(); - EmitClassDefinitionTemplateReceiver(il, inputs.ClassDefinitions); var nullLabel = il.DefineLabel(); var dictLabel = il.DefineLabel(); var tsObjectLabel = il.DefineLabel(); @@ -968,6 +968,9 @@ private void EmitSetProperty(EmittedObjectWriteRuntime objectWrite, SetPropertyI // class-instance path that doesn't match $TSFunction. var tsFunctionSetLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Brtrue, tsFunctionSetLabel); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, inputs.TSFunctionType); il.Emit(OpCodes.Brtrue, tsFunctionSetLabel); il.Emit(OpCodes.Ldarg_0); @@ -2046,6 +2049,9 @@ SetPropertyStrictInputs inputs // user-assigned properties through PDSDefineProperty). var tsFunctionSetStrictLabel = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.Type); + il.Emit(OpCodes.Brtrue, tsFunctionSetStrictLabel); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, inputs.TSFunctionType); il.Emit(OpCodes.Brtrue, tsFunctionSetStrictLabel); il.Emit(OpCodes.Ldarg_0); diff --git a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs index 08add3f50..1045d231c 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs @@ -1363,6 +1363,7 @@ void EmitLinkProto(FieldBuilder child) typeBuilder, runtime.ObjectWrite, new SetPropertyStrictInputs( + runtime.ClassDefinitions, runtime.ArrayOperations, runtime.ArrayStorage, runtime.FunctionBindings.AnyType, @@ -1491,6 +1492,7 @@ void EmitLinkProto(FieldBuilder child) runtime.ReflectedMethods.InvokeUnwrapped, runtime.Invocation.Method, runtime.ObjectDescriptors, + runtime.ObjectPrototypes, runtime.ObjectStorage, runtime.RegExps, runtime.StringCoercion, diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Symbols.cs b/src/SharpTS/Compilation/RuntimeEmitter.Symbols.cs index cdcfbe882..a5acf0dfb 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Symbols.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Symbols.cs @@ -17,7 +17,7 @@ private void EmitGetSymbolDict(TypeBuilder typeBuilder, EmittedSymbolRuntime sym var method = typeBuilder.DefineMethod( "GetSymbolDict", - MethodAttributes.Private | MethodAttributes.Static, + MethodAttributes.Assembly | MethodAttributes.Static, symbolDictType, [_types.Object] ); diff --git a/tests/SharpTS.Tests/SharedTests/RepeatedClassStateTests.cs b/tests/SharpTS.Tests/SharedTests/RepeatedClassStateTests.cs new file mode 100644 index 000000000..e95bb1a0d --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/RepeatedClassStateTests.cs @@ -0,0 +1,248 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class RepeatedClassStateTests +{ + public static IEnumerable Cases() + { + yield return ["retained-generic-field-keys", """ + let count = 0; + function key() { count++; return "key" + count; } + function make() { return class { [key()] = 5; }; } + const First = make(); + const early: any = new First(); + const Second = make(); + const late: any = new First(); + const second: any = new Second(); + console.log(First === Second, count); + console.log(early.key1, late.key1, late.key2, second.key1, second.key2); + """, "false 2\n5 5 undefined undefined 5\n"]; + yield return ["symbol-field-keys", """ + const left = Symbol("key"), right = Symbol("key"); + function make(key: symbol) { return class { [key] = 7; }; } + const First = make(left), Second = make(right); + const first: any = new First(), second: any = new Second(); + console.log(first[left], first[right], second[left], second[right]); + console.log(Reflect.ownKeys(first)[0] === left, Reflect.ownKeys(second)[0] === right); + """, "7 undefined undefined 7\ntrue true\n"]; + yield return ["awaited-field-keys", """ + async function make(key: string) { return class { [await Promise.resolve(key)] = 5; }; } + async function run() { + const First = await make("left"), Second = await make("right"); + const first: any = new First(), second: any = new Second(); + console.log(first.left, first.right, second.left, second.right); + } + run(); + """, "5 undefined undefined 5\n"]; + yield return ["captured-method-values", """ + function make(value: number) { return class { read() { return value; } }; } + const First = make(7), Second = make(9); + console.log(new First().read(), new Second().read(), new First().read()); + """, "7 9 7\n"]; + yield return ["inherited-field-keys", """ + function make(key: string) { return class { [key] = 7; }; } + const First = make("left"), Second = make("right"); + const Child = class extends First {}; + const Other = class extends Second { constructor() { super(); } }; + const first: any = new Child(), second: any = new Other(); + console.log(first.left, first.right, second.left, second.right); + """, "7 undefined undefined 7\n"]; + yield return ["static-initialization", """ + let count = 0; + function make() { return class { static value = ++count; }; } + const First = make(), Second = make(); + console.log(count, First.value, Second.value); + const first = new First(), second = new Second(); + console.log(count, first.constructor === First, second.constructor === Second); + """, "2 1 2\n2 true true\n"]; + yield return ["computed-methods-and-accessors", """ + function make(key: string, value: number) { + return class { [key]() { return value; } get ["get" + key]() { return value + 1; } }; + } + const First = make("left", 7), Second = make("right", 9); + const first: any = new First(), second: any = new Second(); + console.log(first.left(), first.getleft, first.right, first.getright); + console.log(second.right(), second.getright, second.left, second.getleft); + const later: any = new First(); + console.log(later.left()); + """, "7 8 undefined undefined\n9 10 undefined undefined\n7\n"]; + yield return ["symbol-methods-and-accessors", """ + const left = Symbol("method"), right = Symbol("method"), leftGet = Symbol("getter"), rightGet = Symbol("getter"); + function make(methodKey: symbol, getterKey: symbol, value: number) { + return class { [methodKey]() { return value; } get [getterKey]() { return value + 1; } }; + } + const First = make(left, leftGet, 7), Second = make(right, rightGet, 9); + const first: any = new First(), second: any = new Second(); + console.log(first[left](), first[leftGet], first[right], first[rightGet]); + console.log(second[right](), second[rightGet], second[left], second[leftGet]); + """, "7 8 undefined undefined\n9 10 undefined undefined\n"]; + yield return ["static-captures-and-receivers", """ + function make(value: number) { + return class Named { + static value = value; + static read() { return value; } + static get captured() { return value; } + static receiver() { return this; } + static self() { return Named; } + }; + } + const First = make(7), Second = make(9); + First.value = 11; + console.log(First.value, Second.value, First.read(), Second.read(), First.captured, Second.captured); + console.log(First.receiver() === First, First.self() === First, Second.self() === Second); + const borrowed = First.receiver; + const other = {}; + console.log(borrowed.call(other) === other, First.read.call(other)); + """, "11 9 7 9 7 9\ntrue true true\ntrue 7\n"]; + yield return ["key-and-static-source-order", """ + const trace: string[] = []; + function key(label: string) { trace.push("key:" + label); return label; } + function value(label: string) { trace.push("init:" + label); return label; } + function make() { + return class { + static [key("first")] = value("first"); + static { trace.push("block"); this.block = 3; } + static [key("last")] = value("last"); + }; + } + const First: any = make(), Second: any = make(); + console.log(trace.join(",")); + console.log(First.first, First.last, First.block, Second.first, Second.last, Second.block); + """, "key:first,key:last,init:first,block,init:last,key:first,key:last,init:first,block,init:last\nfirst last 3 first last 3\n"]; + yield return ["interleaved-await-captures", """ + async function make(key: string, value: number) { + return class { [await Promise.resolve(key)] = value; read() { return value; } }; + } + async function run() { + const one = make("left", 7), two = make("right", 9); + const First = await one, Second = await two; + const first: any = new First(), second: any = new Second(); + console.log(first.left, first.right, first.read(), second.left, second.right, second.read()); + } + run(); + """, "7 undefined 7 undefined 9 9\n"]; + yield return ["abrupt-definition", """ + let count = 0; + function key(fail: boolean) { if (fail) throw new Error("key"); return "value"; } + function make(fail: boolean) { return class { static [key(fail)] = ++count; }; } + const First: any = make(false); + try { make(true); } catch (error) { console.log(error.message); } + const Second: any = make(false); + console.log(count, First.value, Second.value); + """, "key\n2 1 2\n"]; + yield return ["suspended-method-captures", """ + function make(value: number) { + return class { + *read() { yield value; } + async readAsync() { await Promise.resolve(0); return value; } + static *readStatic() { yield value; } + static async readStaticAsync() { await Promise.resolve(0); return value; } + }; + } + async function run() { + const First = make(7), Second = make(9); + console.log(new First().read().next().value, new Second().read().next().value); + console.log(First.readStatic().next().value, Second.readStatic().next().value); + console.log(await new First().readAsync(), await new Second().readAsync()); + console.log(await First.readStaticAsync(), await Second.readStaticAsync()); + } + run(); + """, "7 9\n7 9\n7 9\n7 9\n"]; + yield return ["parent-survives-awaited-key", """ + function make(key: string) { return class { [key] = 7; }; } + const First = make("left"); + async function makeChild() { return class extends First { [await Promise.resolve("right")] = 9; }; } + async function run() { + const Child = await makeChild(); + const Second = make("other"); + const child: any = new Child(); + console.log(child.left, child.right, child.other, child instanceof First, child instanceof Second); + } + run(); + """, "7 9 undefined true false\n"]; + yield return ["captured-method-closures", """ + function make(value: number) { return class { read() { return () => value; } }; } + const First = make(7), Second = make(9); + const first = new First().read(), second = new Second().read(); + console.log(first(), second(), first()); + """, "7 9 7\n"]; + yield return ["live-captured-bindings", """ + function make(value: number) { + const C = class { + read() { return value; } + write(next: number) { value = next; } + increment() { value++; value += 2; return value; } + closure() { return () => value; } + }; + value += 10; + return C; + } + const First = make(7), Second = make(9); + const first = new First(), second = new Second(), read = first.closure(); + console.log(first.read(), second.read(), read()); + first.write(21); + console.log(first.increment(), read(), second.read()); + """, "17 19 17\n24 24 19\n"]; + yield return ["suspended-method-closures", """ + function make(value: number) { + return class { async read() { await Promise.resolve(0); return () => value; } + *values() { yield () => value; } }; + } + async function run() { + const First = make(7), Second = make(9); + const one = await new First().read(), two = await new Second().read(); + console.log(one(), two(), new First().values().next().value()); + } + run(); + """, "7 9 7\n"]; + yield return ["borrowed-instance-environments", """ + function make(value: number) { + return class { field = value; read() { return value + this.field; } + async readAsync() { await Promise.resolve(0); return value; } + *values() { yield value; } }; + } + async function run() { + const First = make(7), Second = make(9), other = new Second(); + const first = new First(); + console.log(first.read.call(other), first.read.call({ field: 11 })); + console.log(await first.readAsync.call(other), first.values.call(other).next().value); + } + run(); + """, "16 18\n7 7\n"]; + } + + [Theory, ModeData] + public void SameNamedModuleFactoriesRetainTheirDefinitionState(ExecutionMode mode) + { + var files = new Dictionary + { + ["left.ts"] = "export function make(value: number) { return class Named { ['left'] = value; static value = value; read() { return value; } }; }", + ["right.ts"] = "export function make(value: number) { return class Named { ['right'] = value; static value = value; read() { return value; } }; }", + ["main.ts"] = """ + import { make as left } from './left'; + import { make as right } from './right'; + const First: any = left(7), Second: any = right(9), Third: any = left(11); + const first: any = new First(), second: any = new Second(), third: any = new Third(); + console.log(first.left, first.right, first.read(), First.value); + console.log(second.left, second.right, second.read(), Second.value); + console.log(third.left, third.right, third.read(), Third.value); + """ + }; + Assert.Equal("7 undefined 7 7\nundefined 9 9 9\n11 undefined 11 11\n", TestHarness.RunModules(files, "main.ts", mode)); + if (mode == ExecutionMode.Compiled) Assert.Empty(TestHarness.CompileModulesAndVerifyOnly(files, "main.ts")); + } + + [Theory, MemberData(nameof(Cases))] + public void DefinitionsRetainKeysCapturesAndInitialization(string name, string source, string expected) + { + Assert.NotEmpty(name); + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + Assert.Equal(expected, TestHarness.RunCompiledStandalone(source)); + } +} From ee85c69eb6f05ec5c0168aed5345f48e47072db6 Mon Sep 17 00:00:00 2001 From: nickna Date: Mon, 5 Oct 2026 21:53:18 -0700 Subject: [PATCH 06/16] Implement canonical unbound private method values (#1961) --- docs/plans/private-method-values.md | 48 ++++ .../AsyncMoveNextEmitter.Operators.cs | 8 + .../CompilationContext.ClassExpressions.cs | 1 + .../EmittedClassDefinitionRuntime.cs | 5 +- .../ExpressionEmitterBase.CallHelpers.cs | 1 + .../ExpressionEmitterBase.ClassDefinitions.cs | 42 +++- .../ExpressionEmitterBase.PrivateGenerics.cs | 4 +- ...pressionEmitterBase.PrivateMethodValues.cs | 210 +++++++++++++++++ .../Compilation/ExpressionEmitterBase.cs | 9 + .../Compilation/ILCompiler.ArrowFunctions.cs | 1 + src/SharpTS/Compilation/ILCompiler.Async.cs | 1 + .../ILCompiler.Classes.ClassExpressions.cs | 17 +- ...mpiler.Classes.DefinitionInitialization.cs | 21 ++ ...Compiler.Classes.DefinitionMethodValues.cs | 26 +-- .../Compilation/ILCompiler.Classes.Methods.cs | 1 + .../ILCompiler.Classes.PrivateMethodValues.cs | 182 +++++++++++++++ src/SharpTS/Compilation/ILCompiler.Classes.cs | 7 +- .../ILCompiler.ContextFactories.cs | 2 + src/SharpTS/Compilation/ILEmitter.Calls.cs | 1 + .../ILEmitter.Properties.Private.cs | 8 + .../Compilation/ILEmitter.Properties.cs | 1 + .../PrivateClassElementRegistry.cs | 30 +++ .../Compilation/Registries/ClassRegistry.cs | 3 + .../RuntimeEmitter.ClassDefinition.cs | 6 +- .../Execution/Interpreter.Expressions.cs | 28 ++- .../Execution/Interpreter.Properties.cs | 33 ++- .../Runtime/Types/SharpTSAsyncFunction.cs | 20 +- .../Types/SharpTSAsyncGeneratorFunction.cs | 10 +- src/SharpTS/Runtime/Types/SharpTSFunction.cs | 7 +- .../Runtime/Types/SharpTSGeneratorFunction.cs | 9 +- .../TypeSystem/TypeChecker.Properties.cs | 4 + .../PrivateClassElementRegistryTests.cs | 35 +++ .../SharedTests/PrivateMethodValueTests.cs | 216 ++++++++++++++++++ 33 files changed, 938 insertions(+), 59 deletions(-) create mode 100644 src/SharpTS/Compilation/ExpressionEmitterBase.PrivateMethodValues.cs create mode 100644 src/SharpTS/Compilation/ILCompiler.Classes.PrivateMethodValues.cs create mode 100644 tests/SharpTS.Tests/SharedTests/PrivateMethodValueTests.cs diff --git a/docs/plans/private-method-values.md b/docs/plans/private-method-values.md index 5da7dc61a..ffa0a8ac4 100644 --- a/docs/plans/private-method-values.md +++ b/docs/plans/private-method-values.md @@ -1,5 +1,53 @@ # Private-method values: bounded implementation design (#1901) +## Implementation evidence (#1961) + +Private method extraction now returns a canonical, unbound callable after checking +the receiver against the lexical declaration's brand. Ordinary declarations cache +their callable in a non-generic adapter; generic instance access uses the existing +private-instance bridge. Class expressions cache private callables and static +private state on each evaluated class definition. Repeated expression evaluations +therefore retain different brands, values, and captured environments. + +The adapter executes the source body with an explicit guest receiver. Public +property reads, writes, calls, and returned closures use that receiver. Private +access continues to use the lexical owner's storage, including after suspension. +Name, arity, defaults/rest, strict receiver semantics, and non-constructibility use +the emitted function contracts. `PrivateClassElementRegistry` checks adapter, +cache, constructor, and initializer ownership and requires their emitted bodies +before completion. No runtime type generation or SharpTS deployment dependency +is introduced. + +`PrivateMethodValueTests` executes eleven sources in interpretation, in-process +compilation, verified saved IL, and standalone deployment. The sources cover +generic identity, extraction evaluation once, borrowed receivers, public writes +and calls, private brands, static state, all four method kinds, returned closures, +defaults/rest, and a timer suspension. Separate tests cover same-named module +declarations and checker rejection of outside access, assignment, and undeclared +private names. Node v25.5.0 matches all eleven expected outputs; TypeScript 7.0.2 +accepts the two retained positive fixtures and rejects the outside-access control. + +A targeted Test262 comparison against unchanged `c782c870`, at the pinned corpus +revision `d5e73fc8d2c663554fb72e2380a8c2bc1a318a33`, improves +`private-method-get-and-call.js` from RuntimeError to Pass in both engines, with +no regressions in the eight selected files. Three metadata files retain their +parser errors (method-trailing semicolons), and three compiled inherited/inner +arrow controls retain their runtime errors. These outcomes are not counted as +passing conformance checks or silently removed from the comparison. + +Final verification passes 22 focused tests and 779 affected regressions with +strict compiled IL verification enabled. The retained positive CLI fixtures also +pass saved-assembly IL verification and exact-output standalone execution without +SharpTS.dll. The Release solution build and all code-quality gates pass; the +analyzer-aware restore/rebuild reports zero AOT/trim/single-file warnings and +matches the checked baseline. The bounded regression selection excludes the +pre-existing lock-decorator IL failure reproduced at `69f2b081`; the separate +async-arrow/inheritance IL failures recorded for #1965 remain unchanged. + +The remaining runtime-valued local declaration work is tracked separately by +#1967; this evidence does not claim that repeatedly executed local declarations +already receive the class-expression evaluation identity. + ## Evidence and disposition The notes linked by #1901 and #1854 report preserved generic and non-generic diff --git a/src/SharpTS/Compilation/AsyncMoveNextEmitter.Operators.cs b/src/SharpTS/Compilation/AsyncMoveNextEmitter.Operators.cs index 75f4d6200..a6facc75e 100644 --- a/src/SharpTS/Compilation/AsyncMoveNextEmitter.Operators.cs +++ b/src/SharpTS/Compilation/AsyncMoveNextEmitter.Operators.cs @@ -117,6 +117,10 @@ protected override void EmitTaggedTemplateLiteral(Expr.TaggedTemplateLiteral ttl protected override void EmitGetPrivate(Expr.GetPrivate gp) { + if (TryEmitPrivateMethodValue(gp)) return; + if (TryEmitDefinitionPrivateField(gp.Object, gp.Name)) return; + if (TryEmitGuestStaticPrivateField(gp.Object, gp.Name)) return; + if (TryEmitGuestInstancePrivateField(gp.Object, gp.Name)) return; if (TryEmitGenericPrivateGet(gp)) return; // Get the field name without the # prefix @@ -186,6 +190,9 @@ protected override void EmitGetPrivate(Expr.GetPrivate gp) protected override void EmitSetPrivate(Expr.SetPrivate sp) { + if (TryEmitDefinitionPrivateField(sp.Object, sp.Name, sp.Value)) return; + if (TryEmitGuestStaticPrivateField(sp.Object, sp.Name, sp.Value)) return; + if (TryEmitGuestInstancePrivateField(sp.Object, sp.Name, sp.Value)) return; if (TryEmitGenericPrivateSet(sp, SpillBoxed)) return; // Get the field name without the # prefix @@ -268,6 +275,7 @@ protected override void EmitSetPrivate(Expr.SetPrivate sp) protected override void EmitCallPrivate(Expr.CallPrivate cp) { + if (TryEmitDefinitionPrivateCall(cp)) return; if (TryEmitGenericPrivateCall(cp, SpillBoxed)) return; // Get the method name without the # prefix diff --git a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs index a813bf9c9..d1563d5a2 100644 --- a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs +++ b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs @@ -37,6 +37,7 @@ public partial class CompilationContext internal string? ClassDefinitionVariableName { get; set; } internal FieldBuilder? ClassDefinitionOwnerField { get; set; } internal string? GuestThisVariableName { get; set; } + internal bool HasGuestReceiver { get; set; } // Variable name to class expression mapping (for static member access) public Dictionary? VarToClassExpr { get; set; } diff --git a/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs index a6c41da36..c5fb9c942 100644 --- a/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs +++ b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs @@ -9,7 +9,7 @@ public sealed class EmittedClassDefinitionRuntime internal EmittedClassDefinitionRuntime() { } internal sealed record Declarations(TypeBuilder Type, Type InstanceInterface, MethodInfo GetDefinition, ConstructorBuilder Constructor, FieldBuilder Template, - FieldBuilder Factory, FieldBuilder Prototype, FieldBuilder Keys, FieldBuilder Parent, FieldBuilder Captures, MethodBuilder Create, MethodBuilder Construct, + FieldBuilder Factory, FieldBuilder Prototype, FieldBuilder Keys, FieldBuilder Parent, FieldBuilder Captures, FieldBuilder PrivateMembers, MethodBuilder Create, MethodBuilder Construct, MethodBuilder ReadProperty, MethodBuilder Invoke, TypeBuilder CaptureType, ConstructorBuilder CaptureConstructor, FieldBuilder CaptureOwner, FieldBuilder CaptureField, MethodBuilder ReadCapture, MethodBuilder WriteCapture, MethodBuilder FindEnvironment); @@ -28,6 +28,7 @@ internal sealed record Declarations(TypeBuilder Type, Type InstanceInterface, public FieldBuilder Keys => Required.Keys; public FieldBuilder Parent => Required.Parent; public FieldBuilder Captures => Required.Captures; + public FieldBuilder PrivateMembers => Required.PrivateMembers; public MethodBuilder Create => Required.Create; public MethodBuilder Construct => Required.Construct; public MethodBuilder ReadProperty => Required.ReadProperty; @@ -43,7 +44,7 @@ internal void Declare(Declarations declarations) if (_declarations is not null || IsComplete) throw new InvalidOperationException("Class-definition metadata has already been declared."); if (new MemberInfo[] { declarations.Constructor, declarations.Template, declarations.Factory, - declarations.Prototype, declarations.Keys, declarations.Parent, declarations.Captures, declarations.Create, declarations.Construct, + declarations.Prototype, declarations.Keys, declarations.Parent, declarations.Captures, declarations.PrivateMembers, declarations.Create, declarations.Construct, declarations.ReadProperty, declarations.Invoke, declarations.ReadCapture, declarations.WriteCapture, declarations.FindEnvironment } .Any(member => member.DeclaringType != declarations.Type) diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs index cecd392c8..8e5915078 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs @@ -30,6 +30,7 @@ public abstract partial class ExpressionEmitterBase /// protected virtual void EmitCall(Expr.Call c) { + if (TryEmitGuestThisCall(c)) return; if (TryEmitNumericRestCompanionCall(c)) return; if (TryEmitStaticIndirectEvalGlobal(c)) return; diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs index 797e05783..ceade2248 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs @@ -6,6 +6,40 @@ namespace SharpTS.Compilation; public abstract partial class ExpressionEmitterBase { + protected bool TryEmitGuestThisSet(Expr.Set set) + { + if (set.Object is not Expr.This || (!Ctx.HasGuestReceiver && Ctx.GuestThisVariableName == null)) return false; + var receiver = SpillBoxed(set.Object); + var value = SpillBoxed(set.Value); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Ldstr, set.Name.Lexeme); IL.Emit(OpCodes.Ldloc, value); + IL.Emit(OpCodes.Ldc_I4_1); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectWrite.PropertyStrict); + IL.Emit(OpCodes.Ldloc, value); + SetStackUnknown(); + return true; + } + + protected bool TryEmitGuestThisCall(Expr.Call call) + { + if (call.Callee is not Expr.Get { Object: Expr.This, Optional: false } get || call.Optional + || (!Ctx.HasGuestReceiver && Ctx.GuestThisVariableName == null)) return false; + var receiver = SpillBoxed(get.Object); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Ldstr, get.Name.Lexeme); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectRead.Property); + EmitGuestMethodInvocation(receiver, call.Arguments); + return true; + } + + protected void EmitGuestMethodInvocation(LocalBuilder receiver, IReadOnlyList argumentExpressions) + { + var callable = _helpers.SpillStoreObject(); + EmitArgsArrayWithSpread(argumentExpressions); + var arguments = _helpers.SpillStoreObject(); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Ldloc, callable); IL.Emit(OpCodes.Ldloc, arguments); + IL.Emit(OpCodes.Castclass, Types.ObjectArray); IL.Emit(OpCodes.Call, Ctx.Runtime!.Invocation.Method); + SetStackUnknown(); + } + protected bool TryEmitClassDefinitionSelf(string name) { var expression = Ctx.CurrentClassExpr ?? Ctx.ClassExprBuilders? @@ -62,7 +96,13 @@ protected bool TryEmitGuestThis() protected bool TryEmitGuestThisGet(Expr.Get get) { - if (get.Object is not Expr.This || get.Optional || !TryEmitGuestThis()) return false; + if (get.Object is not Expr.This || get.Optional) return false; + if (!TryEmitGuestThis()) + { + if (!Ctx.HasGuestReceiver) return false; + EmitThis(); + EnsureBoxed(); + } IL.Emit(OpCodes.Ldstr, get.Name.Lexeme); IL.Emit(OpCodes.Call, Ctx.Runtime!.ObjectRead.Property); SetStackUnknown(); diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateGenerics.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateGenerics.cs index 8640ad11c..6f90657ba 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateGenerics.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateGenerics.cs @@ -59,9 +59,7 @@ private LocalBuilder SpillGenericPrivateOperand(Expr expression) { EmitExpression(expression); EnsureBoxed(); - var local = IL.DeclareLocal(typeof(object)); - IL.Emit(OpCodes.Stloc, local); - return local; + return _helpers.SpillStoreObject(); } protected bool TryEmitGenericPrivateCall(Expr.CallPrivate call, Func? spill = null) diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateMethodValues.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateMethodValues.cs new file mode 100644 index 000000000..65c7af2d6 --- /dev/null +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateMethodValues.cs @@ -0,0 +1,210 @@ +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public abstract partial class ExpressionEmitterBase +{ + protected bool TryEmitGuestInstancePrivateField(Expr receiverExpression, Token token, Expr? assignedValue = null) + { + if ((!Ctx.HasGuestReceiver && Ctx.GuestThisVariableName == null) || Ctx.CurrentClassName is not { } current) return false; + string name = token.Lexeme.TrimStart('#'); + string owner = Ctx.ResolvePrivateFieldOwner(current, name); + var declaration = Ctx.ClassRegistry?.GetPrivateElements(owner); + if (declaration?.FieldNames.Contains(name) != true) return false; + var receiver = SpillGenericPrivateOperand(receiverExpression); + var value = assignedValue == null ? null : SpillGenericPrivateOperand(assignedValue); + var slots = declaration.InstanceBridge is { } bridge ? EmitGenericPrivateBrandCheck(bridge, receiver, name) + : EmitOrdinaryPrivateBrandCheck(owner, receiver, name); + IL.Emit(OpCodes.Ldloc, slots); IL.Emit(OpCodes.Ldstr, name); + if (value == null) IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.DictionaryStringObject, "get_Item", Types.String)); + else + { + IL.Emit(OpCodes.Ldloc, value); IL.Emit(OpCodes.Callvirt, Types.DictionaryStringObjectSetItem); IL.Emit(OpCodes.Ldloc, value); + } + SetStackUnknown(); + return true; + } + + private LocalBuilder EmitOrdinaryPrivateBrandCheck(string owner, LocalBuilder receiver, string name) + { + var table = typeof(System.Runtime.CompilerServices.ConditionalWeakTable>); + var slots = IL.DeclareLocal(Types.DictionaryStringObject); + var invalid = IL.DefineLabel(); + var valid = IL.DefineLabel(); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Brfalse, invalid); + IL.Emit(OpCodes.Ldsfld, Ctx.ClassRegistry!.GetPrivateFieldStorage(owner)!); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Ldloca, slots); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(table, "TryGetValue", Types.Object, Types.DictionaryStringObject.MakeByRefType())); + IL.Emit(OpCodes.Brtrue, valid); + IL.MarkLabel(invalid); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, $"Cannot access private member #{name} from an object whose class did not declare it"); + IL.MarkLabel(valid); + return slots; + } + + protected bool TryEmitGuestStaticPrivateField(Expr receiverExpression, Token token, Expr? assignedValue = null) + { + if (Ctx.GuestThisVariableName == null || Ctx.CurrentClassName is not { } current) return false; + string name = token.Lexeme.TrimStart('#'); + string owner = Ctx.ResolvePrivateFieldOwner(current, name); + if (Ctx.ClassRegistry?.GetPrivateElements(owner)?.StaticFields.ContainsKey(name) != true) return false; + var receiver = SpillGenericPrivateOperand(receiverExpression); + var value = assignedValue == null ? null : SpillGenericPrivateOperand(assignedValue); + EmitOrdinaryStaticPrivateBrandCheck(owner, receiver, name); + if (!Ctx.ClassRegistry.TryGetCallableStaticPrivateField(owner, name, out var field)) + throw new InvalidOperationException("Static private field has no callable owner."); + if (value == null) IL.Emit(OpCodes.Ldsfld, field!); + else { IL.Emit(OpCodes.Ldloc, value); IL.Emit(OpCodes.Stsfld, field!); IL.Emit(OpCodes.Ldloc, value); } + SetStackUnknown(); + return true; + } + + private void EmitOrdinaryStaticPrivateBrandCheck(string owner, LocalBuilder receiver, string name) + { + Ctx.ClassRegistry!.TryGetClass(owner, out var builder); + var invalid = IL.DefineLabel(); + var valid = IL.DefineLabel(); + IL.Emit(OpCodes.Ldloc, receiver); + if (builder!.IsGenericTypeDefinition) + { + var constructor = IL.DeclareLocal(Types.Type); + IL.Emit(OpCodes.Isinst, Types.Type); + IL.Emit(OpCodes.Stloc, constructor); + IL.Emit(OpCodes.Ldloc, constructor); IL.Emit(OpCodes.Brfalse, invalid); + IL.Emit(OpCodes.Ldloc, constructor); + IL.Emit(OpCodes.Callvirt, Types.GetProperty(Types.Type, "IsGenericType").GetGetMethod()!); + IL.Emit(OpCodes.Brfalse, invalid); + IL.Emit(OpCodes.Ldloc, constructor); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.Type, "GetGenericTypeDefinition")); + } + IL.Emit(OpCodes.Ldtoken, builder); + IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); + IL.Emit(OpCodes.Ceq); IL.Emit(OpCodes.Brtrue, valid); + IL.MarkLabel(invalid); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, $"Cannot access private member #{name} from an object whose class did not declare it"); + IL.MarkLabel(valid); + } + + protected bool TryEmitDefinitionPrivateField(Expr receiverExpression, Token token, Expr? assignedValue = null) + { + if (Ctx.CurrentClassName is not { } owner || Ctx.CurrentClassExpr is not { } expression) return false; + string name = token.Lexeme.TrimStart('#'); + var declaration = Ctx.ClassRegistry?.GetPrivateElements(owner); + if (declaration == null) return false; + bool isStatic = declaration.StaticFields.ContainsKey(name); + if (!isStatic && !declaration.FieldNames.Contains(name)) return false; + var receiver = SpillGenericPrivateOperand(receiverExpression); + var value = assignedValue == null ? null : SpillGenericPrivateOperand(assignedValue); + var definition = EmitDefinitionPrivateBrandCheck(receiver, name, isStatic); + if (isStatic) + { + IL.Emit(OpCodes.Ldloc, definition); + IL.Emit(OpCodes.Ldfld, Ctx.Runtime!.ClassDefinitions.PrivateMembers); + } + else if (declaration.InstanceBridge is { } bridge) + IL.Emit(OpCodes.Ldloc, EmitGenericPrivateBrandCheck(bridge, receiver, name)); + else + { + var slots = IL.DeclareLocal(Types.DictionaryStringObject); + var storage = declaration.Storage!; + var template = Ctx.ClassExprFactories![expression].Template; + IL.Emit(OpCodes.Ldsfld, template.IsGenericType ? EmitterTypeHelpers.ResolveField(template, storage) : storage); + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Ldloca, slots); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(storage.FieldType, "TryGetValue", Types.Object, Types.DictionaryStringObject.MakeByRefType())); + var valid = IL.DefineLabel(); + IL.Emit(OpCodes.Brtrue, valid); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, $"Cannot access private member #{name} before initialization"); + IL.MarkLabel(valid); + IL.Emit(OpCodes.Ldloc, slots); + } + IL.Emit(OpCodes.Ldstr, isStatic ? $"field:{name}" : name); + if (value == null) IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.DictionaryStringObject, "get_Item", Types.String)); + else + { + IL.Emit(OpCodes.Ldloc, value); + IL.Emit(OpCodes.Callvirt, Types.DictionaryStringObjectSetItem); + IL.Emit(OpCodes.Ldloc, value); + } + SetStackUnknown(); + return true; + } + + protected bool TryEmitPrivateMethodValue(Expr.GetPrivate get) + { + if (Ctx.CurrentClassName is not { } current) return false; + string name = get.Name.Lexeme.TrimStart('#'); + string owner = Ctx.ResolvePrivateMethodOwner(current, name); + var value = Ctx.ClassRegistry?.GetPrivateMethodValue(owner, name); + if (value is null) return false; + var receiver = SpillGenericPrivateOperand(get.Object); + EmitPrivateMethodValue(value, owner, name, receiver); + return true; + } + + protected bool TryEmitDefinitionPrivateCall(Expr.CallPrivate call) + { + if (Ctx.CurrentClassName is not { } current) return false; + string name = call.Name.Lexeme.TrimStart('#'); + string owner = Ctx.ResolvePrivateMethodOwner(current, name); + var value = Ctx.ClassRegistry?.GetPrivateMethodValue(owner, name); + if (value?.Definition == null) return false; + var receiver = SpillGenericPrivateOperand(call.Object); + EmitPrivateMethodValue(value, owner, name, receiver); + EmitGuestMethodInvocation(receiver, call.Arguments); + return true; + } + + private void EmitPrivateMethodValue(PrivateMethodValue value, string owner, string name, LocalBuilder receiver) + { + var definition = value.Definition == null ? null : EmitDefinitionPrivateBrandCheck(receiver, name, value.Source.IsStatic); + if (value.Source.IsStatic && definition == null) + { + EmitOrdinaryStaticPrivateBrandCheck(owner, receiver, name); + } + else if (!value.Source.IsStatic && Ctx.ClassRegistry!.GetPrivateInstanceBridge(owner) is { } bridge) + EmitGenericPrivateBrandCheck(bridge, receiver, name); + else if (!value.Source.IsStatic) + EmitOrdinaryPrivateBrandCheck(owner, receiver, name); + if (definition == null) IL.Emit(OpCodes.Ldsfld, value.Cache!); + else + { + IL.Emit(OpCodes.Ldloc, definition); + IL.Emit(OpCodes.Ldfld, Ctx.Runtime!.ClassDefinitions.PrivateMembers); + IL.Emit(OpCodes.Ldstr, $"method:{name}"); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.DictionaryStringObject, "get_Item", Types.String)); + } + SetStackUnknown(); + } + + private LocalBuilder EmitDefinitionPrivateBrandCheck(LocalBuilder receiver, string name, bool isStatic) + { + if (!TryEmitOwnedClassDefinition()) throw new InvalidOperationException("Private access requires its lexical class definition."); + var definition = IL.DeclareLocal(Ctx.Runtime!.ClassDefinitions.Type); + IL.Emit(OpCodes.Stloc, definition); + var invalid = IL.DefineLabel(); + var valid = IL.DefineLabel(); + if (isStatic) IL.Emit(OpCodes.Ldloc, receiver); + else + { + var expression = Ctx.CurrentClassExpr ?? Ctx.ClassExprBuilders!.First(entry => ReferenceEquals(entry.Value, Ctx.CurrentClassBuilder)).Key; + var factory = Ctx.ClassExprFactories![expression]; + var field = Ctx.ClassExprDefinitionFields![expression]; + var reference = factory.Template.IsGenericType ? EmitterTypeHelpers.ResolveField(factory.Template, field) : field; + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Isinst, reference.DeclaringType!); + IL.Emit(OpCodes.Brfalse, invalid); + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Castclass, reference.DeclaringType!); + IL.Emit(OpCodes.Ldfld, reference); + } + IL.Emit(OpCodes.Ldloc, definition); + IL.Emit(OpCodes.Ceq); + IL.Emit(OpCodes.Brtrue, valid); + IL.MarkLabel(invalid); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, $"Cannot access private member #{name} from an object whose class did not declare it"); + IL.MarkLabel(valid); + return definition; + } +} diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.cs index 8f32dfdfe..7f26fd78d 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.cs @@ -957,6 +957,7 @@ protected bool TryEmitBuiltInModulePropertySet(Expr.Set s) protected virtual void EmitSet(Expr.Set s) { + if (TryEmitGuestThisSet(s)) return; // CommonJS: `module.exports = X` → stsfld $exports. if (TryEmitCjsSet(s)) return; @@ -1021,6 +1022,10 @@ protected virtual void EmitSet(Expr.Set s) /// protected virtual void EmitGetPrivate(Expr.GetPrivate gp) { + if (TryEmitPrivateMethodValue(gp)) return; + if (TryEmitDefinitionPrivateField(gp.Object, gp.Name)) return; + if (TryEmitGuestStaticPrivateField(gp.Object, gp.Name)) return; + if (TryEmitGuestInstancePrivateField(gp.Object, gp.Name)) return; if (TryEmitGenericPrivateGet(gp)) return; string fieldName = gp.Name.Lexeme; @@ -1089,6 +1094,9 @@ protected virtual void EmitGetPrivate(Expr.GetPrivate gp) /// protected virtual void EmitSetPrivate(Expr.SetPrivate sp) { + if (TryEmitDefinitionPrivateField(sp.Object, sp.Name, sp.Value)) return; + if (TryEmitGuestStaticPrivateField(sp.Object, sp.Name, sp.Value)) return; + if (TryEmitGuestInstancePrivateField(sp.Object, sp.Name, sp.Value)) return; if (TryEmitGenericPrivateSet(sp)) return; string fieldName = sp.Name.Lexeme; @@ -1169,6 +1177,7 @@ protected virtual void EmitSetPrivate(Expr.SetPrivate sp) /// protected virtual void EmitCallPrivate(Expr.CallPrivate cp) { + if (TryEmitDefinitionPrivateCall(cp)) return; if (TryEmitGenericPrivateCall(cp)) return; string methodName = cp.Name.Lexeme; diff --git a/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs b/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs index 650c9ea79..6e6533230 100644 --- a/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs +++ b/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs @@ -1511,6 +1511,7 @@ private void EmitArrowBody(Expr.ArrowFunction arrow, MethodBuilder method, TypeB ctx.CurrentClassName = _async.ArrowEnclosingClassNames.TryGetValue(arrow, out var enclosingClassName) ? enclosingClassName : null; + ctx.HasGuestReceiver = _guestReceiverArrows.Contains(arrow); if (ctx.CurrentClassName != null) ctx.CurrentClassBuilder = _classes.Builders.GetValueOrDefault(ctx.CurrentClassName); ctx.CurrentSuperclassIsAnonymousEmptyClass = _arrowsInAnonymousEmptyDerivedClass.Contains(arrow); diff --git a/src/SharpTS/Compilation/ILCompiler.Async.cs b/src/SharpTS/Compilation/ILCompiler.Async.cs index b58acb6a7..1a14718ba 100644 --- a/src/SharpTS/Compilation/ILCompiler.Async.cs +++ b/src/SharpTS/Compilation/ILCompiler.Async.cs @@ -1848,6 +1848,7 @@ private void EmitTopLevelAsyncArrowBodies() // ES2022 Private Class Elements support ctx.CurrentClassName = enclosingClassName; ctx.CurrentClassBuilder = enclosingClassBuilder; + ctx.HasGuestReceiver = _guestReceiverArrows.Contains(arrow); ApplyCapturedTopLevelVariableAccess(ctx); // #1222: lets the shadow-capture check distinguish a #1201-lifted binding // (home = entry-DC field, must stay live) from a block-scoped shadow diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs index 1da0b3203..41c1e021b 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs @@ -194,7 +194,7 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) // parameter) cannot be widened to / narrowed from the object-typed $IHasFields slots // without box/unbox of `T`, which yields unverifiable IL. Like class declarations // (ILCompiler.Classes.cs), these fields live in the `_fields` dictionary instead (#291). - foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsDeclare && f.ComputedKey == null)) + foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsPrivate && !f.IsDeclare && f.ComputedKey == null)) { bool isGenericField = classGenericParams != null && field.TypeAnnotation != null && @@ -205,7 +205,7 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) } // Define static fields (use object type for compatibility with existing emission code) - foreach (var field in classExpr.Fields.Where(f => f.IsStatic)) + foreach (var field in classExpr.Fields.Where(f => f.IsStatic && !f.IsPrivate)) { var staticField = typeBuilder.DefineField( field.Name.Lexeme, @@ -481,6 +481,8 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) .Select(field => (field.ComputedKey!, field.Name.Start))) .OrderBy(entry => entry.Start).Select(entry => entry.Item1).ToArray(); _classExprs.Factories[classExpr] = _classExprs.Factories[classExpr] with { Keys = keys }; + DefinePrivateClassElements(typeBuilder, className, ClassExpressionDeclaration(classExpr), + _classes.GenericParameters.Require(typeBuilder).ToArray(), classExpr); } /// @@ -550,6 +552,7 @@ private void EmitClassExpressionBody(Expr.ClassExpr classExpr) // Emit symbol-keyed computed accessor bodies (#281) EmitClassExpressionSymbolAccessors(classExpr, typeBuilder, fieldsField); + EmitClassExpressionPrivateMethodBodies(classExpr, typeBuilder); // Emit $IHasFields interface method bodies (now that method builders are available) EmitHasFieldsInterfaceMethodBodies(className, classExpr); @@ -624,6 +627,11 @@ private void EmitClassExpressionStaticConstructor(Expr.ClassExpr classExpr, Type { var cctor = typeBuilder.DefineTypeInitializer(); var il = cctor.GetILGenerator(); + if (_classes.PrivateElements.Require(_classExprs.Names[classExpr]).Storage is { } privateStorage) + { + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(privateStorage.FieldType)); + il.Emit(OpCodes.Stsfld, EmitterTypeHelpers.SelfFieldReference(privateStorage)); + } EmitClassPrototypeRegistration(il, typeBuilder, GetClassConstructorLength(classExpr.Methods)); il.Emit(OpCodes.Ret); EmitClassDefinitionInitializer(classExpr, typeBuilder); @@ -797,7 +805,10 @@ private void EmitClassExpressionConstructor(Expr.ClassExpr classExpr, TypeBuilde void EmitInstanceFieldInitializers() { - foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsDeclare && (f.Initializer != null || f.ComputedKey != null))) + var savedFields = ctx.FieldsField; + EmitPrivateFieldInitialization(il, className, ClassExpressionDeclaration(classExpr), ctx); + ctx.FieldsField = savedFields; + foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsPrivate && !f.IsDeclare && (f.Initializer != null || f.ComputedKey != null))) { if (field.ComputedKey != null) { diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionInitialization.cs b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionInitialization.cs index f074b9458..d053acced 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionInitialization.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionInitialization.cs @@ -16,6 +16,17 @@ private void EmitClassDefinitionInitializer(Expr.ClassExpr expression, TypeBuild ctx.ClassDefinitionThisParameterIndex = 0; var emitter = new ILEmitter(ctx); var storage = _runtime.DescriptorStorage; + foreach (var value in _classes.PrivateElements.Require(_classExprs.Names[expression]).MethodValues.Values) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.PrivateMembers); + il.Emit(OpCodes.Ldstr, $"method:{value.Source.Name.Lexeme.TrimStart('#')}"); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Newobj, value.Constructor); + EmitMethodInfoLiteral(il, value.Method, value.Type); + il.Emit(OpCodes.Newobj, _runtime.FunctionConstruction.Constructor); + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); + } var members = new List<(int Position, bool Static, Expr Key, MethodBuilder Method, bool? Getter, IReadOnlyList Parameters)>(); foreach (var method in expression.Methods.Where(method => method.Body != null && @@ -44,6 +55,16 @@ private void EmitClassDefinitionInitializer(Expr.ClassExpr expression, TypeBuild { if (initializer is Stmt.Field field && field.IsStatic && !field.IsDeclare) { + if (field.IsPrivate) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.PrivateMembers); + il.Emit(OpCodes.Ldstr, $"field:{field.Name.Lexeme.TrimStart('#')}"); + if (field.Initializer != null) { emitter.EmitExpression(field.Initializer); emitter.EmitBoxIfNeeded(field.Initializer); } + else il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); + continue; + } il.Emit(OpCodes.Ldarg_0); EmitKey(field.ComputedKey ?? new Expr.Literal(field.Name.Lexeme)); if (field.Initializer != null) diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs index a697e2e91..af3c3a192 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs @@ -42,34 +42,16 @@ public partial class ILCompiler original.Name == $"{(accessor.Kind.Type == TokenType.GET ? "get" : "set")}_{NamingConventions.ToPascalCase(accessor.Name.Lexeme)}"); source = new Stmt.Function(accessor.Name, null, null, parameters.ToList(), accessor.Body, null); } - var guestThis = new Stmt.Parameter(new Token(TokenType.IDENTIFIER, "__this", null, 0), "any"); - var lowered = source with { Parameters = [guestThis, .. source.Parameters] }; - if (_asyncMethodFunctionDCKeys.TryGetValue(source, out var asyncKey)) _asyncMethodFunctionDCKeys[lowered] = asyncKey; - if (_generatorMethodFunctionDCKeys.TryGetValue(source, out var generatorKey)) _generatorMethodFunctionDCKeys[lowered] = generatorKey; - if (_asyncGeneratorMethodFunctionDCKeys.TryGetValue(source, out var asyncGeneratorKey)) _asyncGeneratorMethodFunctionDCKeys[lowered] = asyncGeneratorKey; - if (source.IsAsync && source.IsGenerator) EmitAsyncGeneratorMethodBody(method, lowered, null, currentClassName: _classExprs.Names[expression]); - else if (source.IsAsync) EmitAsyncMethodBody(method, lowered, null, currentClassName: _classExprs.Names[expression]); - else if (source.IsGenerator) EmitGeneratorMethodBody(method, lowered, null, currentClassName: _classExprs.Names[expression]); - else + EmitGuestMethodValueBody(method, source, () => { - il = method.GetILGenerator(); - var ctx = CreateClassExpressionContext(il, expression, _classExprs.Builders[expression], null, method); + var ctx = CreateClassExpressionContext(method.GetILGenerator(), expression, _classExprs.Builders[expression], null, method); ctx.EmittingTypeBuilder = type; ctx.IsInstanceMethod = true; ctx.ClassDefinitionOwnerField = field; ctx.GuestThisVariableName = "__this"; foreach (var parameter in expression.TypeParams ?? []) ctx.GenericTypeParameters[parameter.Name.Lexeme] = _types.Object; - ctx.DefineParameter("__this", 1, _types.Object); - SetupSyncMethodFunctionDisplayClass(ctx, il, source); - for (int i = 0; i < parameters.Count; i++) ctx.DefineParameter(parameters[i].Name.Lexeme, i + 2, _types.Object); - var emitter = new ILEmitter(ctx); - EmitFunctionEnvironmentPrologue(il, ctx, emitter, parameters.ToList(), source.Body, - parameters.Select(_ => _types.Object).ToArray(), argumentOffset: 2); - InitializeSyncMethodCapturedParameters(ctx, il, source, method, argumentOffset: 2); - foreach (var statement in source.Body ?? []) emitter.EmitStatement(statement); - if (emitter.HasDeferredReturns) emitter.FinalizeReturns(); - else { EmitDefaultReturnValue(il, method.ReturnType); il.Emit(OpCodes.Ret); } - } + return ctx; + }, _classExprs.Names[expression]); type.CreateType(); return (type, constructor, method); } diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs index 099f2a61d..8a0a43d26 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs @@ -780,6 +780,7 @@ private void EmitPrivateMethodBodies(TypeBuilder typeBuilder, Stmt.Class classSt throw new InvalidOperationException($"Private method '{qualifiedClassName}.{methodName}' has not been declared."); EmitPrivateMethodBody(typeBuilder, methodBuilder, method, fieldsField, qualifiedClassName, method.IsStatic); } + EmitPrivateMethodValueBodies(qualifiedClassName); _classes.PrivateElements.MarkBodiesEmitted(qualifiedClassName); } diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.PrivateMethodValues.cs b/src/SharpTS/Compilation/ILCompiler.Classes.PrivateMethodValues.cs new file mode 100644 index 000000000..f38a24cd4 --- /dev/null +++ b/src/SharpTS/Compilation/ILCompiler.Classes.PrivateMethodValues.cs @@ -0,0 +1,182 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Parsing; +using SharpTS.Parsing.Visitors; + +namespace SharpTS.Compilation; + +public partial class ILCompiler +{ + private Stmt.Class ClassExpressionDeclaration(Expr.ClassExpr expression) => new( + new Token(TokenType.IDENTIFIER, _classExprs.Names[expression], null, 0), expression.TypeParams, + expression.SuperclassExpr, expression.SuperclassTypeArgs, expression.Methods, expression.Fields, + expression.Accessors, StaticInitializers: expression.StaticInitializers); + + private void EmitClassExpressionPrivateMethodBodies(Expr.ClassExpr expression, TypeBuilder template) + { + string name = _classExprs.Names[expression]; + var declaration = _classes.PrivateElements.Require(name); + foreach (var value in declaration.MethodValues.Values) + { + var original = (value.Source.IsStatic ? declaration.StaticMethods : declaration.Methods)[value.Source.Name.Lexeme.TrimStart('#')]; + var il = original.GetILGenerator(); + if (value.Source.IsStatic) il.Emit(OpCodes.Ldarg_0); + else + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, EmitterTypeHelpers.SelfFieldReference(_classExprs.DefinitionFields[expression])); + il.Emit(OpCodes.Castclass, _runtime.ClassDefinitions.Type); + } + var definition = il.DeclareLocal(_runtime.ClassDefinitions.Type); + il.Emit(OpCodes.Stloc, definition); + if (value.Source.IsStatic) il.Emit(OpCodes.Ldloc, definition); + else il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Ldfld, _runtime.ClassDefinitions.PrivateMembers); + il.Emit(OpCodes.Ldstr, $"method:{value.Source.Name.Lexeme.TrimStart('#')}"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.DictionaryStringObject, "get_Item", _types.String)); + il.Emit(OpCodes.Ldc_I4, value.Source.Parameters.Count); + il.Emit(OpCodes.Newarr, _types.Object); + for (int i = 0; i < value.Source.Parameters.Count; i++) + { + il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldc_I4, i); il.Emit(OpCodes.Ldarg, i + 1); il.Emit(OpCodes.Stelem_Ref); + } + il.Emit(OpCodes.Call, _runtime.Invocation.Method); + if (original.ReturnType != _types.Object) il.Emit(OpCodes.Castclass, original.ReturnType); + il.Emit(OpCodes.Ret); + } + EmitPrivateMethodValueBodies(name); + _classes.PrivateElements.MarkBodiesEmitted(name); + } + + private readonly Dictionary _privateMethodValueOwners = []; + private readonly HashSet _guestReceiverArrows = new(ReferenceEqualityComparer.Instance); + + private sealed class GuestReceiverArrowCollector(HashSet arrows) : AstVisitorBase + { + protected override void VisitArrowFunction(Expr.ArrowFunction expression) + { + if (expression.HasOwnThis) return; + arrows.Add(expression); + base.VisitArrowFunction(expression); + } + protected override void VisitClassExpr(Expr.ClassExpr expression) { } + protected override void VisitFunction(Stmt.Function statement) { } + } + + private void DefinePrivateMethodValues(string className, TypeBuilder owner, Stmt.Class declaration, Expr.ClassExpr? expression = null) + { + foreach (var source in declaration.Methods.Where(method => method.IsPrivate && method.Body != null)) + { + var collector = new GuestReceiverArrowCollector(_guestReceiverArrows); + foreach (var statement in source.Body!) collector.Visit(statement); + string name = source.Name.Lexeme.TrimStart('#'); + var type = EmitTypeDefinitions.DefineType(_moduleBuilder, $"$PrivateMember_{className}_{name}", + TypeAttributes.Public | TypeAttributes.Sealed, _types.Object); + var definition = expression == null ? null : type.DefineField("Definition", _runtime.ClassDefinitions.Type, FieldAttributes.Assembly | FieldAttributes.InitOnly); + var constructor = type.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, + expression == null ? Type.EmptyTypes : [_runtime.ClassDefinitions.Type]); + var il = constructor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.GetDefaultConstructor(_types.Object)); + if (definition != null) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Stfld, definition); + } + il.Emit(OpCodes.Ret); + var method = type.DefineMethod("Invoke", MethodAttributes.Public, + ResolvePrivateMethodReturnType(source, source.IsStatic), + [_types.Object, .. source.Parameters.Select(parameter => parameter.IsRest ? typeof(List) : _types.Object)]); + method.DefineParameter(1, ParameterAttributes.None, "__this"); + MarkExpectsThis(method); + MarkNonConstructible(method); + MarkPadsUndefined(method); + MarkFunctionLength(method, source.Parameters); + MarkFunctionName(method, $"#{name}"); + FieldBuilder? cache = null; + ConstructorBuilder? initializer = null; + if (expression == null) + { + cache = type.DefineField("Value", _types.Object, FieldAttributes.Assembly | FieldAttributes.Static | FieldAttributes.InitOnly); + initializer = type.DefineTypeInitializer(); + il = initializer.GetILGenerator(); + il.Emit(OpCodes.Newobj, constructor); + EmitMethodInfoLiteral(il, method, type); + il.Emit(OpCodes.Newobj, _runtime.FunctionConstruction.Constructor); + il.Emit(OpCodes.Stsfld, cache); + il.Emit(OpCodes.Ret); + } + else _classExprs.DefinitionMethods.Add(method, (expression, definition!)); + _classes.PrivateElements.DeclareMethodValue(className, name, + new PrivateMethodValue(type, constructor, method, cache, initializer, source, definition)); + _privateMethodValueOwners.Add(method, (className, owner, declaration)); + } + } + + private void EmitPrivateMethodValueBodies(string className) + { + foreach (var value in _classes.PrivateElements.Require(className).MethodValues.Values) + { + var owner = _privateMethodValueOwners[value.Method]; + EmitGuestMethodValueBody(value.Method, value.Source, () => + { + var ctx = _classExprs.DefinitionMethods.TryGetValue(value.Method, out var evaluated) + ? CreateClassExpressionContext(value.Method.GetILGenerator(), evaluated.Expression, owner.Owner, null, value.Method) + : CreateModuleMemberContext(value.Method.GetILGenerator(), value.Method); + ctx.ClassDefinitionOwnerField = value.Definition; + ApplyPrivateMethodValueContext(ctx, value.Method); + ctx.EmittingTypeBuilder = value.Type; + ctx.ArrowEntryPointDCFields = _closures.ArrowEntryPointDCFields.Count > 0 ? _closures.ArrowEntryPointDCFields : null; + ctx.ArrowFunctionDCFields = _closures.ArrowFunctionDCFields.Count > 0 ? _closures.ArrowFunctionDCFields : null; + ctx.ArrowScopeDCFields = _closures.ArrowScopeDCFields.Count > 0 ? _closures.ArrowScopeDCFields : null; + ctx.ArrowScopeDCExtraFieldsByArrow = _arrowScopeDCExtraFields.Count > 0 ? _arrowScopeDCExtraFields : null; + ApplyCapturedTopLevelVariableAccess(ctx); + ApplyCommonJsModuleAccess(ctx); + return ctx; + }, owner.Name); + value.Type.CreateType(); + } + } + + private void ApplyPrivateMethodValueContext(CompilationContext ctx, MethodBuilder method) + { + if (!_privateMethodValueOwners.TryGetValue(method, out var owner)) return; + ctx.CurrentClassName = owner.Name; + ctx.CurrentClassBuilder = owner.Owner; + ctx.IsStrictMode = true; + ctx.IsInstanceMethod = !_classes.PrivateElements.Require(owner.Name).MethodValues.Values.First(value => value.Method == method).Source.IsStatic; + ctx.GuestThisVariableName = "__this"; + ctx.HasGuestReceiver = true; + foreach (var parameter in owner.Source.TypeParams ?? []) ctx.GenericTypeParameters[parameter.Name.Lexeme] = _types.Object; + } + + private void EmitGuestMethodValueBody(MethodBuilder method, Stmt.Function source, + Func createContext, string className) + { + var guestThis = new Stmt.Parameter(new Token(TokenType.IDENTIFIER, "__this", null, 0), "any"); + var lowered = source with { Parameters = [guestThis, .. source.Parameters] }; + if (_asyncMethodFunctionDCKeys.TryGetValue(source, out var asyncKey)) _asyncMethodFunctionDCKeys[lowered] = asyncKey; + if (_generatorMethodFunctionDCKeys.TryGetValue(source, out var generatorKey)) _generatorMethodFunctionDCKeys[lowered] = generatorKey; + if (_asyncGeneratorMethodFunctionDCKeys.TryGetValue(source, out var asyncGeneratorKey)) _asyncGeneratorMethodFunctionDCKeys[lowered] = asyncGeneratorKey; + if (source.IsAsync && source.IsGenerator) EmitAsyncGeneratorMethodBody(method, lowered, null, currentClassName: className); + else if (source.IsAsync) EmitAsyncMethodBody(method, lowered, null, currentClassName: className); + else if (source.IsGenerator) EmitGeneratorMethodBody(method, lowered, null, currentClassName: className); + else + { + var il = method.GetILGenerator(); + var ctx = createContext(); + ctx.DefineParameter("__this", 1, _types.Object); + SetupSyncMethodFunctionDisplayClass(ctx, il, source); + var parameterTypes = method.GetParameters().Skip(1).Select(parameter => parameter.ParameterType).ToArray(); + for (int i = 0; i < source.Parameters.Count; i++) ctx.DefineParameter(source.Parameters[i].Name.Lexeme, i + 2, parameterTypes[i]); + var emitter = new ILEmitter(ctx); + EmitFunctionEnvironmentPrologue(il, ctx, emitter, source.Parameters, source.Body, parameterTypes, argumentOffset: 2); + InitializeSyncMethodCapturedParameters(ctx, il, source, method, argumentOffset: 2); + foreach (var statement in source.Body ?? []) emitter.EmitStatement(statement); + if (emitter.HasDeferredReturns) emitter.FinalizeReturns(); + else { EmitDefaultReturnValue(il, method.ReturnType); il.Emit(OpCodes.Ret); } + } + } +} diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.cs b/src/SharpTS/Compilation/ILCompiler.Classes.cs index a7d59360c..699120749 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.cs @@ -449,7 +449,8 @@ private void DefinePrivateClassElements( TypeBuilder typeBuilder, string className, Stmt.Class classStmt, - GenericTypeParameterBuilder[]? classGenericParams) + GenericTypeParameterBuilder[]? classGenericParams, + Expr.ClassExpr? expression = null) { // Collect private fields (IsPrivate flag indicates #field syntax) var instancePrivateFields = classStmt.Fields.Where(f => f.IsPrivate && !f.IsStatic).ToList(); @@ -538,7 +539,8 @@ private void DefinePrivateClassElements( if (methodName.StartsWith('#')) methodName = methodName[1..]; - var paramTypes = method.Parameters.Select(_ => typeof(object)).ToArray(); + var paramTypes = (expression == null ? Array.Empty() : new Type[] { _runtime.ClassDefinitions.Type }) + .Concat(method.Parameters.Select(_ => typeof(object))).ToArray(); Type returnType = ResolvePrivateMethodReturnType(method, isStatic: true); // Use Assembly (internal) visibility so nested async/generator state machines can access this method @@ -555,6 +557,7 @@ private void DefinePrivateClassElements( ? DefinePrivateInstanceBridge(typeBuilder, privateStorage, privateMethods) : null; _classes.PrivateElements.Declare(className, typeBuilder, privateStorage, privateFieldNames, privateStaticFields, privateMethods, privateStaticMethods, instanceBridge); + DefinePrivateMethodValues(className, typeBuilder, classStmt, expression); } private PrivateInstanceBridge DefinePrivateInstanceBridge(TypeBuilder owner, FieldBuilder storage, diff --git a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs index 81dce0235..44e783449 100644 --- a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs +++ b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs @@ -173,6 +173,7 @@ private CompilationContext CreateModuleMemberContext(ILGenerator il, MethodBase? private void ApplyClassDefinitionStateMachineContext(CompilationContext ctx, MethodBuilder method, bool isInstance) { + ApplyPrivateMethodValueContext(ctx, method); if (_classExprs.DefinitionMethods.TryGetValue(method, out var owner)) { ctx.CurrentClassExpr = owner.Expression; @@ -306,6 +307,7 @@ private CompilationContext CreateNestedAsyncArrowContext(ILGenerator il, Compila // ES2022 Private Class Elements support - inherit from parent context CurrentClassName = parentCtx.CurrentClassName, CurrentClassBuilder = parentCtx.CurrentClassBuilder, + HasGuestReceiver = parentCtx.HasGuestReceiver, // Registry services ClassRegistry = parentCtx.ClassRegistry, // Entry-point display class for captured top-level variables diff --git a/src/SharpTS/Compilation/ILEmitter.Calls.cs b/src/SharpTS/Compilation/ILEmitter.Calls.cs index 0f3cd0b88..dcc9b5792 100644 --- a/src/SharpTS/Compilation/ILEmitter.Calls.cs +++ b/src/SharpTS/Compilation/ILEmitter.Calls.cs @@ -164,6 +164,7 @@ protected override void VisitClassExpr(Expr.ClassExpr expr) protected override void EmitCall(Expr.Call c) { + if (TryEmitGuestThisCall(c)) return; if (TryEmitPromotedObjectConsumer(c)) return; if (TryEmitNumericRestCompanionCall(c)) return; // A lexical super() inside an immediately invoked arrow must still be diff --git a/src/SharpTS/Compilation/ILEmitter.Properties.Private.cs b/src/SharpTS/Compilation/ILEmitter.Properties.Private.cs index 04278bac4..fc050c8d3 100644 --- a/src/SharpTS/Compilation/ILEmitter.Properties.Private.cs +++ b/src/SharpTS/Compilation/ILEmitter.Properties.Private.cs @@ -13,6 +13,10 @@ public partial class ILEmitter /// protected override void EmitGetPrivate(Expr.GetPrivate gp) { + if (TryEmitPrivateMethodValue(gp)) return; + if (TryEmitDefinitionPrivateField(gp.Object, gp.Name)) return; + if (TryEmitGuestStaticPrivateField(gp.Object, gp.Name)) return; + if (TryEmitGuestInstancePrivateField(gp.Object, gp.Name)) return; if (TryEmitGenericPrivateGet(gp)) return; // Get the field name without the # prefix @@ -111,6 +115,9 @@ protected override void EmitGetPrivate(Expr.GetPrivate gp) /// protected override void EmitSetPrivate(Expr.SetPrivate sp) { + if (TryEmitDefinitionPrivateField(sp.Object, sp.Name, sp.Value)) return; + if (TryEmitGuestStaticPrivateField(sp.Object, sp.Name, sp.Value)) return; + if (TryEmitGuestInstancePrivateField(sp.Object, sp.Name, sp.Value)) return; if (TryEmitGenericPrivateSet(sp)) return; // Get the field name without the # prefix @@ -225,6 +232,7 @@ protected override void EmitSetPrivate(Expr.SetPrivate sp) /// protected override void EmitCallPrivate(Expr.CallPrivate cp) { + if (TryEmitDefinitionPrivateCall(cp)) return; if (TryEmitGenericPrivateCall(cp)) return; // Get the method name without the # prefix diff --git a/src/SharpTS/Compilation/ILEmitter.Properties.cs b/src/SharpTS/Compilation/ILEmitter.Properties.cs index f6b4d0e09..837e6983a 100644 --- a/src/SharpTS/Compilation/ILEmitter.Properties.cs +++ b/src/SharpTS/Compilation/ILEmitter.Properties.cs @@ -959,6 +959,7 @@ private void SetStackTypeForFieldType(Type fieldType) protected override void EmitSet(Expr.Set s) { + if (TryEmitGuestThisSet(s)) return; // CommonJS: `module.exports = X` writes → stsfld $exports if (TryEmitCjsSet(s)) return; diff --git a/src/SharpTS/Compilation/PrivateClassElementRegistry.cs b/src/SharpTS/Compilation/PrivateClassElementRegistry.cs index 05bcad475..eccf18c44 100644 --- a/src/SharpTS/Compilation/PrivateClassElementRegistry.cs +++ b/src/SharpTS/Compilation/PrivateClassElementRegistry.cs @@ -53,11 +53,34 @@ internal void MarkBodiesEmitted(string name) EnsureMutable(); var declaration = Require(name); if (declaration.Methods.Values.Concat(declaration.StaticMethods.Values).Any(method => method.GetILGenerator().ILOffset == 0) + || declaration.MethodValues.Values.Any(value => value.Method.GetILGenerator().ILOffset == 0 + || value.Constructor.GetILGenerator().ILOffset == 0 || value.Initializer?.GetILGenerator().ILOffset == 0) || (declaration.InstanceBridge is { } bridge && bridge.StorageImplementation.GetILGenerator().ILOffset == 0) || !_emitted.Add(_names[name])) throw new InvalidOperationException("Private class method bodies are empty or already emitted."); } + internal void DeclareMethodValue(string owner, string name, PrivateMethodValue value) + { + EnsureMutable(); + var declaration = Require(owner); + var methods = value.Source.IsStatic ? declaration.StaticMethods : declaration.Methods; + if (_emitted.Contains(_names[owner]) || !value.Source.IsPrivate || !methods.ContainsKey(name) + || value.Type.Module != _names[owner].Module || value.Type.IsGenericType || value.Method.IsGenericMethod + || value.Method.DeclaringType != value.Type + || value.Method.IsStatic || value.Constructor.DeclaringType != value.Type + || value.Method.GetParameters().FirstOrDefault()?.ParameterType != typeof(object) + || value.Method.GetParameters().Length != value.Source.Parameters.Count + 1 + || value.Constructor.GetParameters().Length != (value.Definition == null ? 0 : 1) + || (value.Definition != null && value.Constructor.GetParameters()[0].ParameterType != value.Definition.FieldType) + || (value.Cache != null) != (value.Initializer != null) + || (value.Cache != null && (value.Cache.DeclaringType != value.Type || !value.Cache.IsStatic)) + || (value.Initializer != null && (value.Initializer.DeclaringType != value.Type || !value.Initializer.IsStatic)) + || (value.Cache == null && (value.Definition?.DeclaringType != value.Type || value.Definition.IsStatic)) + || !declaration.MutableMethodValues.TryAdd(name, value)) + throw new InvalidOperationException("Invalid or duplicate private method value declaration."); + } + internal void CompleteEmission() { EnsureMutable(); @@ -76,6 +99,8 @@ private void EnsureMutable() /// Immutable declaration views; field order and builder identity are preserved. public sealed class PrivateClassElements { + internal Dictionary MutableMethodValues { get; } = new(StringComparer.Ordinal); + public IReadOnlyDictionary MethodValues { get; } public PrivateInstanceBridge? InstanceBridge { get; } public FieldBuilder? Storage { get; } public IReadOnlyList FieldNames { get; } @@ -87,6 +112,7 @@ internal PrivateClassElements(FieldBuilder? storage, IEnumerable fieldNa IReadOnlyDictionary staticFields, IReadOnlyDictionary methods, IReadOnlyDictionary staticMethods, PrivateInstanceBridge? instanceBridge) { + MethodValues = new ReadOnlyDictionary(MutableMethodValues); InstanceBridge = instanceBridge; Storage = storage; FieldNames = Array.AsReadOnly(fieldNames.ToArray()); @@ -96,6 +122,10 @@ internal PrivateClassElements(FieldBuilder? storage, IEnumerable fieldNa } } +public sealed record PrivateMethodValue(TypeBuilder Type, ConstructorBuilder Constructor, + MethodBuilder Method, FieldBuilder? Cache, ConstructorBuilder? Initializer, SharpTS.Parsing.Stmt.Function Source, + FieldBuilder? Definition = null); + /// Non-generic dispatch surface for a generic class's private instance elements. public sealed class PrivateInstanceBridge { diff --git a/src/SharpTS/Compilation/Registries/ClassRegistry.cs b/src/SharpTS/Compilation/Registries/ClassRegistry.cs index 3acbdb2db..33870e62a 100644 --- a/src/SharpTS/Compilation/Registries/ClassRegistry.cs +++ b/src/SharpTS/Compilation/Registries/ClassRegistry.cs @@ -14,6 +14,9 @@ namespace SharpTS.Compilation.Registries; /// public sealed class ClassRegistry { + public PrivateMethodValue? GetPrivateMethodValue(string owner, string name) => + _privateElements.TryGet(owner, out var declaration) && declaration.MethodValues.TryGetValue(name, out var value) ? value : null; + public PrivateClassElements? GetPrivateElements(string owner) => _privateElements.TryGet(owner, out var declaration) ? declaration : null; // Core class builders private readonly Dictionary _builders; private readonly Dictionary _superclass; diff --git a/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs index 7a9eb43eb..ff35c0348 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs @@ -26,6 +26,7 @@ private void DefineClassDefinitionTypes(ModuleBuilder module, EmittedClassDefini var keys = type.DefineField("Keys", _types.ObjectArray, FieldAttributes.Assembly); var parent = type.DefineField("Parent", _types.Object, FieldAttributes.Assembly); var captures = type.DefineField("Captures", _types.ObjectArray, FieldAttributes.Assembly); + var privateMembers = type.DefineField("PrivateMembers", _types.DictionaryStringObject, FieldAttributes.Assembly); var create = type.DefineMethod("Create", MethodAttributes.Public | MethodAttributes.Static, type, [_types.Type, factoryType, _types.String, _types.Double, _types.Object, _types.ObjectArray, _types.ObjectArray]); var construct = type.DefineMethod("Construct", MethodAttributes.Public | MethodAttributes.Static, @@ -57,7 +58,7 @@ private void DefineClassDefinitionTypes(ModuleBuilder module, EmittedClassDefini EmitClassCaptureBodies(captureType, captureOwner, captureField, captures, readCapture, writeCapture, findEnvironment); captureType.CreateType(); definitions.Declare(new(type, instanceInterface, instanceInterface.GetMethod(getter.Name)!, - constructor, template, factory, prototype, keys, parent, captures, create, construct, read, invoke, + constructor, template, factory, prototype, keys, parent, captures, privateMembers, create, construct, read, invoke, captureType, captureConstructor, captureOwner, captureField, readCapture, writeCapture, findEnvironment)); } @@ -86,6 +87,9 @@ private void EmitClassDefinitionBodies(EmittedRuntime runtime) il.Emit(OpCodes.Ldloc, definition); il.Emit(OpCodes.Ldarg_S, (byte)6); il.Emit(OpCodes.Stfld, definitions.Captures); + il.Emit(OpCodes.Ldloc, definition); + il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.DictionaryStringObject)); + il.Emit(OpCodes.Stfld, definitions.PrivateMembers); il.Emit(OpCodes.Newobj, _types.GetDefaultConstructor(_types.DictionaryStringObject)); il.Emit(OpCodes.Newobj, runtime.ObjectStorage.Constructor); il.Emit(OpCodes.Stloc, prototype); diff --git a/src/SharpTS/Execution/Interpreter.Expressions.cs b/src/SharpTS/Execution/Interpreter.Expressions.cs index 0f9c8b664..4f2816374 100644 --- a/src/SharpTS/Execution/Interpreter.Expressions.cs +++ b/src/SharpTS/Execution/Interpreter.Expressions.cs @@ -2099,6 +2099,10 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon Dictionary staticMethods = []; Dictionary staticProperties = []; List instanceFields = []; + List instancePrivateFields = []; + Dictionary privateMethods = []; + Dictionary staticPrivateMethods = []; + Dictionary staticPrivateFields = []; // Check if we have static initializers for proper ordering bool hasStaticInitializers = classExpr.StaticInitializers != null && classExpr.StaticInitializers.Count > 0; @@ -2115,7 +2119,12 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon foreach (Stmt.Field field in classExpr.Fields) { - if (field.IsStatic) + if (field.IsPrivate) + { + if (!field.IsStatic) instancePrivateFields.Add(field); + else if (!hasStaticInitializers) staticPrivateFields[field.Name.Lexeme] = field.Initializer == null ? SharpTSUndefined.Instance : Evaluate(field.Initializer); + } + else if (field.IsStatic) { if (!hasStaticInitializers) { @@ -2166,7 +2175,9 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon continue; } - if (method.IsStatic) + if (method.IsPrivate) + (method.IsStatic ? staticPrivateMethods : privateMethods)[method.Name.Lexeme] = func; + else if (method.IsStatic) { staticMethods[method.Name.Lexeme] = func; } @@ -2237,7 +2248,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon classExpr.IsAbstract, instanceFields, staticGetters: staticGetters.Count > 0 ? staticGetters : null, - staticSetters: staticSetters.Count > 0 ? staticSetters : null) + staticSetters: staticSetters.Count > 0 ? staticSetters : null, instancePrivateFields: instancePrivateFields, privateMethods: privateMethods, staticPrivateFields: staticPrivateFields, staticPrivateMethods: staticPrivateMethods) : superclass is SharpTSArrayClass arraySuper ? new SharpTSArrayClass( className, @@ -2250,7 +2261,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon classExpr.IsAbstract, instanceFields, staticGetters: staticGetters.Count > 0 ? staticGetters : null, - staticSetters: staticSetters.Count > 0 ? staticSetters : null) + staticSetters: staticSetters.Count > 0 ? staticSetters : null, instancePrivateFields: instancePrivateFields, privateMethods: privateMethods, staticPrivateFields: staticPrivateFields, staticPrivateMethods: staticPrivateMethods) : superclass is SharpTSPromiseClass promiseSuper ? new SharpTSPromiseClass( className, @@ -2263,7 +2274,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon classExpr.IsAbstract, instanceFields, staticGetters: staticGetters.Count > 0 ? staticGetters : null, - staticSetters: staticSetters.Count > 0 ? staticSetters : null) + staticSetters: staticSetters.Count > 0 ? staticSetters : null, instancePrivateFields: instancePrivateFields, privateMethods: privateMethods, staticPrivateFields: staticPrivateFields, staticPrivateMethods: staticPrivateMethods) : new SharpTSClass( className, (SharpTSClass?)superclass, @@ -2275,7 +2286,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon classExpr.IsAbstract, instanceFields, staticGetters: staticGetters.Count > 0 ? staticGetters : null, - staticSetters: staticSetters.Count > 0 ? staticSetters : null); + staticSetters: staticSetters.Count > 0 ? staticSetters : null, instancePrivateFields: instancePrivateFields, privateMethods: privateMethods, staticPrivateFields: staticPrivateFields, staticPrivateMethods: staticPrivateMethods); klass.BindPrivateMemberOwners(); klass.InitializerEnvironment = classEnv; @@ -2301,7 +2312,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon { // Create temporary environment with 'this' bound to the class // Also make the class name available so code like Foo.x works - var staticEnv = new RuntimeEnvironment(_environment); + var staticEnv = new RuntimeEnvironment(_environment, strictMode: true) { PrivateClass = klass }; staticEnv.Define("this", klass); if (classExpr.Name != null) { @@ -2318,7 +2329,8 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon object? fieldValue = field.Initializer != null ? Evaluate(field.Initializer) : field.ComputedKey != null ? SharpTSUndefined.Instance : null; - if (field.ComputedKey != null && computedMemberKeys[field.ComputedKey] is SharpTSSymbol symbol) + 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.ComputedKey != null diff --git a/src/SharpTS/Execution/Interpreter.Properties.cs b/src/SharpTS/Execution/Interpreter.Properties.cs index b60a8cad3..1bb03f6f1 100644 --- a/src/SharpTS/Execution/Interpreter.Properties.cs +++ b/src/SharpTS/Execution/Interpreter.Properties.cs @@ -128,7 +128,7 @@ private static SharpTSObject CreateConstructedThis(object? prototype) // binding `this` to a fresh object whose prototype is Foo.prototype. // Without this, `self instanceof Yallist` is false and packages // recurse infinitely. - if (klass is SharpTSFunction userFn) + if (klass is SharpTSFunction { IsPrivateMethod: false } userFn) { // Build a new `this` object backed by the function's prototype. if (!userFn.TryGetProperty("prototype", out var protoObj)) @@ -362,7 +362,7 @@ private RuntimeValue EvaluateNew(Expr.New newExpr) // Build a fresh `this` with prototype linkage, bind it, then call — // so `this instanceof Foo` returns true (Node CJS pattern used by // e.g. yallist, EventEmitter sub-classes). - if (klass is SharpTSFunction userFn) + if (klass is SharpTSFunction { IsPrivateMethod: false } userFn) { if (!userFn.TryGetProperty("prototype", out var protoObj)) { @@ -1552,6 +1552,7 @@ private static object BindBuiltInMember(object member, object receiver) /// internal static bool IsNonConstructorWrapper(object? callable) => callable is ISharpTSNonConstructorCallable + or SharpTSFunction { IsPrivateMethod: true } or SharpTSArrowFunction { HasOwnThis: false } or SharpTSAsyncFunction or SharpTSAsyncArrowFunction @@ -3102,6 +3103,24 @@ private async Task EvaluateGetPrivateAsync(Expr.GetPrivate expr) private RuntimeValue GetPrivateCore(object? obj, string fieldName) { + var owner = _environment.PrivateClass; + if (owner?.HasStaticPrivateField(fieldName) == true) + { + if (!ReferenceEquals(obj, owner)) throw new ThrowException(new SharpTSTypeError($"Cannot access private member {fieldName} from an object whose class did not declare it")); + return owner.GetStaticPrivateFieldRV(fieldName); + } + if (owner?.GetPrivateMethod(fieldName) is { } privateMethod) + { + if (obj == null || !owner.HasPrivateBrand(obj)) + throw new ThrowException(new SharpTSTypeError($"Cannot access private member {fieldName} from an object whose class did not declare it")); + return RuntimeValue.FromBoxed(privateMethod); + } + if (owner?.GetStaticPrivateMethod(fieldName) is { } staticMethod) + { + if (!ReferenceEquals(obj, owner)) + throw new ThrowException(new SharpTSTypeError($"Cannot access private member {fieldName} from an object whose class did not declare it")); + return RuntimeValue.FromBoxed(staticMethod); + } // Handle static private field access on class if (obj is SharpTSClass klass) { @@ -3122,7 +3141,7 @@ private RuntimeValue GetPrivateCore(object? obj, string fieldName) return declaringClass.GetPrivateFieldRV(instance, fieldName); } - throw new InterpreterException($"Cannot read private field '{fieldName}' from non-class value."); + throw new ThrowException(new SharpTSTypeError($"Cannot read private field '{fieldName}' from non-class value.")); } /// @@ -3145,6 +3164,12 @@ private async Task EvaluateSetPrivateAsync(Expr.SetPrivate expr) private RuntimeValue SetPrivateCore(object? obj, RuntimeValue value, string fieldName) { + if (_environment.PrivateClass is { } owner && owner.HasStaticPrivateField(fieldName)) + { + if (!ReferenceEquals(obj, owner)) throw new ThrowException(new SharpTSTypeError($"Cannot access private member {fieldName} from an object whose class did not declare it")); + owner.SetStaticPrivateField(fieldName, value.ToObject()); + return value; + } // Handle static private field assignment on class if (obj is SharpTSClass klass) { @@ -3215,7 +3240,7 @@ private RuntimeValue CallPrivateCore(object? obj, List arguments, strin throw new InterpreterException($"Static private method '{methodName}' does not exist on class '{klass.Name}'."); } - return RuntimeValue.FromBoxed(method.CallBoxed(this, arguments)); + return RuntimeValue.FromBoxed(SharpTSClass.BindStaticMethod(method, klass).CallBoxed(this, arguments)); } // Instance private method call diff --git a/src/SharpTS/Runtime/Types/SharpTSAsyncFunction.cs b/src/SharpTS/Runtime/Types/SharpTSAsyncFunction.cs index 0f001d98d..33289808a 100644 --- a/src/SharpTS/Runtime/Types/SharpTSAsyncFunction.cs +++ b/src/SharpTS/Runtime/Types/SharpTSAsyncFunction.cs @@ -39,14 +39,17 @@ public class SharpTSAsyncFunction : ISharpTSAsyncCallable, ITypeCategorized private readonly RuntimeEnvironment _closure; private readonly int _arity; internal SharpTSClass? PrivateOwner { get; set; } + private readonly bool _receiverBound; // JS: functions (including async) are objects and support property assignment. private Dictionary? _properties; - public SharpTSAsyncFunction(Stmt.Function declaration, RuntimeEnvironment closure) + public SharpTSAsyncFunction(Stmt.Function declaration, RuntimeEnvironment closure, bool receiverBound = false) { _declaration = declaration; _closure = closure; - _arity = declaration.Parameters.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional); + _receiverBound = receiverBound; + _arity = declaration.IsPrivate ? declaration.Parameters.TakeWhile(p => p.DefaultValue == null && !p.IsRest).Count() + : declaration.Parameters.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional); } public bool TryGetProperty(string name, out object? value) @@ -117,7 +120,12 @@ public void SetProperty(string name, object? value) /// public async Task CallAsync(Interpreter interpreter, List arguments) { - RuntimeEnvironment environment = new(_closure) { PrivateClass = PrivateOwner }; + RuntimeEnvironment environment = new(_closure, strictMode: _declaration.IsPrivate ? true : null) { PrivateClass = PrivateOwner }; + if (_declaration.IsPrivate) + { + environment.Define("this", _receiverBound && _closure.TryGet("this", out var receiver) + ? receiver.ToObject() : SharpTSUndefined.Instance); + } await ParameterBinder.BindAsync(_declaration.Parameters, arguments, environment, interpreter); if (_declaration.Body == null) @@ -164,7 +172,7 @@ public SharpTSAsyncFunction Bind(SharpTSInstance instance) // 'super' not in scope - ignore } - return new SharpTSAsyncFunction(_declaration, environment) { PrivateOwner = PrivateOwner }; + return new SharpTSAsyncFunction(_declaration, environment, receiverBound: true) { PrivateOwner = PrivateOwner }; } /// @@ -176,7 +184,7 @@ public SharpTSAsyncFunction BindThisValue(object? thisObject) { RuntimeEnvironment environment = new(_closure); environment.Define("this", thisObject); - return new SharpTSAsyncFunction(_declaration, environment) { PrivateOwner = PrivateOwner }; + return new SharpTSAsyncFunction(_declaration, environment, receiverBound: true) { PrivateOwner = PrivateOwner }; } public SharpTSAsyncFunction BindStatic(SharpTSClass klass) @@ -185,7 +193,7 @@ public SharpTSAsyncFunction BindStatic(SharpTSClass klass) environment.Define("this", klass); if (klass.Superclass != null) environment.Define("super", klass.Superclass); - return new SharpTSAsyncFunction(_declaration, environment) { PrivateOwner = PrivateOwner }; + return new SharpTSAsyncFunction(_declaration, environment, receiverBound: true) { PrivateOwner = PrivateOwner }; } public override string ToString() => $""; diff --git a/src/SharpTS/Runtime/Types/SharpTSAsyncGeneratorFunction.cs b/src/SharpTS/Runtime/Types/SharpTSAsyncGeneratorFunction.cs index 1a431b90e..aee447740 100644 --- a/src/SharpTS/Runtime/Types/SharpTSAsyncGeneratorFunction.cs +++ b/src/SharpTS/Runtime/Types/SharpTSAsyncGeneratorFunction.cs @@ -39,8 +39,8 @@ public SharpTSAsyncGeneratorFunction(Stmt.Function declaration, RuntimeEnvironme _thisBound = thisBound; _boundThis = boundThis; _boundSuper = boundSuper; - _arity = declaration.Parameters?.Count( - p => p.DefaultValue == null && !p.IsRest && !p.IsOptional) ?? 0; + _arity = declaration.IsPrivate ? declaration.Parameters.TakeWhile(p => p.DefaultValue == null && !p.IsRest).Count() + : declaration.Parameters?.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional) ?? 0; } public int Arity() => _arity; @@ -48,7 +48,11 @@ public SharpTSAsyncGeneratorFunction(Stmt.Function declaration, RuntimeEnvironme public object? Call(Interpreter interpreter, List arguments) { // Create a new environment for this generator invocation - RuntimeEnvironment environment = new(_closure) { PrivateClass = PrivateOwner }; + RuntimeEnvironment environment = new(_closure, strictMode: _declaration.IsPrivate ? true : null) { PrivateClass = PrivateOwner }; + if (_declaration.IsPrivate) + { + environment.Define("this", _thisBound ? _boundThis : SharpTSUndefined.Instance); + } // #775: an async generator expression / object method binds its own dynamic `this`. The bound // receiver (or globalThis for a plain call) is defined in the generator's OWN body environment, diff --git a/src/SharpTS/Runtime/Types/SharpTSFunction.cs b/src/SharpTS/Runtime/Types/SharpTSFunction.cs index a2f5ea531..011855c47 100644 --- a/src/SharpTS/Runtime/Types/SharpTSFunction.cs +++ b/src/SharpTS/Runtime/Types/SharpTSFunction.cs @@ -59,6 +59,7 @@ public class SharpTSFunction : ISharpTSCallable, ITypeCategorized private readonly Stmt.Function _declaration; private readonly RuntimeEnvironment _closure; private readonly int _arity; + internal bool IsPrivateMethod => _declaration.IsPrivate; internal SharpTSClass? PrivateOwner { get; set; } // `this` value stored on the function itself (from BindThis) rather than in an // extra closure scope — otherwise the resolver's scope-distance count wouldn't @@ -81,7 +82,9 @@ private SharpTSFunction(Stmt.Function declaration, RuntimeEnvironment closure, o _closure = closure; _boundThis = boundThis; _hasBoundThis = hasBoundThis; - _arity = declaration.Parameters.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional); + _arity = declaration.IsPrivate + ? declaration.Parameters.TakeWhile(p => p.DefaultValue == null && !p.IsRest).Count() + : declaration.Parameters.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional); InitializeIntrinsicProperties(declaration.Name.Lexeme); } @@ -217,7 +220,7 @@ public bool TryGetAccessor(string name, out ISharpTSCallable? getter, out ISharp public int Arity() => _arity; internal bool IsStrict - => _closure.IsStrictMode + => _declaration.IsPrivate || _closure.IsStrictMode || (_declaration.Body is not null && Parsing.DirectivePrologue.HasUseStrict(_declaration.Body)); diff --git a/src/SharpTS/Runtime/Types/SharpTSGeneratorFunction.cs b/src/SharpTS/Runtime/Types/SharpTSGeneratorFunction.cs index 6774754e5..8c9b0c5da 100644 --- a/src/SharpTS/Runtime/Types/SharpTSGeneratorFunction.cs +++ b/src/SharpTS/Runtime/Types/SharpTSGeneratorFunction.cs @@ -42,7 +42,8 @@ public SharpTSGeneratorFunction(Stmt.Function declaration, RuntimeEnvironment cl _thisBound = thisBound; _boundThis = boundThis; _boundSuper = boundSuper; - _arity = declaration.Parameters.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional); + _arity = declaration.IsPrivate ? declaration.Parameters.TakeWhile(p => p.DefaultValue == null && !p.IsRest).Count() + : declaration.Parameters.Count(p => p.DefaultValue == null && !p.IsRest && !p.IsOptional); } public int Arity() => _arity; @@ -53,7 +54,11 @@ public SharpTSGeneratorFunction(Stmt.Function declaration, RuntimeEnvironment cl public object? Call(Interpreter interpreter, List arguments) { // Create environment and bind parameters (like a regular function) - RuntimeEnvironment environment = new(_closure) { PrivateClass = PrivateOwner }; + RuntimeEnvironment environment = new(_closure, strictMode: _declaration.IsPrivate ? true : null) { PrivateClass = PrivateOwner }; + if (_declaration.IsPrivate) + { + environment.Define("this", _thisBound ? _boundThis : SharpTSUndefined.Instance); + } // #775: a generator expression / object generator method binds its own dynamic `this`. The bound // receiver (or globalThis for a plain call) is defined in the generator's OWN body environment — // NOT a new parent scope inserted above the closure — so a captured enclosing-function local keeps diff --git a/src/SharpTS/TypeSystem/TypeChecker.Properties.cs b/src/SharpTS/TypeSystem/TypeChecker.Properties.cs index 26602996f..97535ef43 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Properties.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Properties.cs @@ -1300,6 +1300,10 @@ private TypeInfo CheckGetPrivate(Expr.GetPrivate get) { return fieldType; } + if (_currentClass.PrivateMethodTypes.TryGetValue(fieldName, out var methodType)) + return methodType; + if (_currentClass.StaticPrivateMethodTypes.TryGetValue(fieldName, out var staticMethodType)) + return staticMethodType; // Check static private fields if accessing on the class itself if (IsStaticPrivateConstructor(objType) && _currentClass.StaticPrivateFieldTypes.TryGetValue(fieldName, out var staticFieldType)) diff --git a/tests/SharpTS.Tests/CompilerTests/PrivateClassElementRegistryTests.cs b/tests/SharpTS.Tests/CompilerTests/PrivateClassElementRegistryTests.cs index 3ea1582f4..17eea6574 100644 --- a/tests/SharpTS.Tests/CompilerTests/PrivateClassElementRegistryTests.cs +++ b/tests/SharpTS.Tests/CompilerTests/PrivateClassElementRegistryTests.cs @@ -8,6 +8,41 @@ namespace SharpTS.Tests.CompilerTests; public sealed class PrivateClassElementRegistryTests { + [Fact] + public void MethodValueRejectsForeignCacheAndRequiresAllBodies() + { + var module = new PersistedAssemblyBuilder(new AssemblyName(Guid.NewGuid().ToString("N")), typeof(object).Assembly).DefineDynamicModule("Main"); + var owner = module.DefineType("Box", TypeAttributes.Public); + var adapter = module.DefineType("Value", TypeAttributes.Public); + var foreign = module.DefineType("Foreign", TypeAttributes.Public); + var original = owner.DefineMethod("read", MethodAttributes.Public, typeof(object), Type.EmptyTypes); + var storage = owner.DefineField("brand", typeof(object), FieldAttributes.Static); + var registry = new PrivateClassElementRegistry(); + registry.Declare("Box", owner, storage, [], new Dictionary(), + new Dictionary { ["read"] = original }, new Dictionary()); + var source = new SharpTS.Parsing.Stmt.Function(new SharpTS.Parsing.Token(SharpTS.Parsing.TokenType.IDENTIFIER, "#read", null, 0), + null, null, [], [], null, IsPrivate: true); + var method = adapter.DefineMethod("Invoke", MethodAttributes.Public, typeof(object), [typeof(object)]); + var constructor = adapter.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes); + var initializer = adapter.DefineTypeInitializer(); + var cache = adapter.DefineField("Value", typeof(object), FieldAttributes.Static); + var value = new PrivateMethodValue(adapter, constructor, method, cache, initializer, source); + Assert.Throws(() => registry.DeclareMethodValue("Box", "read", + value with { Cache = foreign.DefineField("Value", typeof(object), FieldAttributes.Static) })); + registry.DeclareMethodValue("Box", "read", value); + Assert.Throws(() => ((IDictionary)registry.Require("Box").MethodValues).Clear()); + Assert.Throws(() => registry.MarkBodiesEmitted("Box")); + original.GetILGenerator().Emit(OpCodes.Ldnull); original.GetILGenerator().Emit(OpCodes.Ret); + method.GetILGenerator().Emit(OpCodes.Ldnull); method.GetILGenerator().Emit(OpCodes.Ret); + constructor.GetILGenerator().Emit(OpCodes.Ret); + Assert.Throws(() => registry.MarkBodiesEmitted("Box")); + initializer.GetILGenerator().Emit(OpCodes.Ret); + registry.MarkBodiesEmitted("Box"); + Assert.Throws(() => registry.DeclareMethodValue("Box", "read", value)); + registry.CompleteEmission(); + Assert.Throws(() => registry.DeclareMethodValue("Box", "read", value)); + } + [Fact] public void GenericBridgeRejectsForeignOwnerAndRequiresItsStorageBody() { diff --git a/tests/SharpTS.Tests/SharedTests/PrivateMethodValueTests.cs b/tests/SharpTS.Tests/SharedTests/PrivateMethodValueTests.cs new file mode 100644 index 000000000..381f38b9e --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/PrivateMethodValueTests.cs @@ -0,0 +1,216 @@ +using SharpTS.Tests.Infrastructure; +using SharpTS.TypeSystem.Exceptions; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class PrivateMethodValueTests +{ + public static IEnumerable Cases() + { + yield return ["public-property-write-and-call", """ + class Box { + value = 3; + read() { return this.value; } + #change() { this.value = this.value + 1; return this.read(); } + extract() { return this.#change; } + } + const box = new Box(), change = box.extract(); + const object = { value: 7, read() { return this.value + 10; } }; + console.log(change.call(object), object.value, box.value); + """, "18 8 3\n"]; + yield return ["real-suspension-and-receiver-order", """ + let reads = 0; + class Box { + value = 3; + #read(amount = 2) { return this.value + amount; } + async #later() { await new Promise(resolve => setTimeout(() => resolve(0), 1)); return this.value; } + async extract(object: any) { return (await Promise.resolve(object)).#read; } + later() { return this.#later; } + } + function receiver(object: any) { reads++; return object; } + async function run() { + const box = new Box(), read = await box.extract(receiver(box)), later = box.later(); + console.log(reads, read === await box.extract(box)); + console.log(read.call({ value: 7 }, await new Promise(resolve => setTimeout(() => resolve(5), 1)))); + console.log(await later.call({ value: 11 })); + } + run().catch(error => console.log("error", error)); + """, "1 true\n12\n11\n"]; + yield return ["generic-static-values", """ + class Box { + static #value = 7; + static async #read() { await Promise.resolve(0); return this.#value; } + static extract() { return Box.#read; } + } + class Other { static #value = 9; } + async function run() { + const read = Box.extract(); + console.log(read === Box.extract(), await read.call(Box)); + try { await read.call(Other); } catch (error) { console.log(error instanceof TypeError); } + } + run().catch(error => console.log("error", error)); + """, "true 7\ntrue\n"]; + yield return ["repeated-static-private-state", """ + function make(value: number) { + return class { + static #value = value; + static #read() { return this.#value; } + static extract() { return this.#read; } + }; + } + const First = make(3), Second = make(7), one = First.extract(), two = Second.extract(); + console.log(one === First.extract(), one === two, one.call(First), two.call(Second)); + try { one.call(Second); } catch (error) { console.log(error instanceof TypeError); } + """, "true false 3 7\ntrue\n"]; + yield return ["repeated-class-evaluations", """ + function make(value: number) { + return class Named { + #value = value; + #read() { return this.#value; } + #captured() { return value; } + extract(object: any) { return object.#read; } + captured() { return this.#captured; } + call() { return this.#read(); } + }; + } + const First = make(3), Second = make(7), first = new First(), second = new Second(); + const read = first.extract(first), other = second.extract(second); + console.log(read === first.extract(new First()), read === other, read.call(first), first.call()); + console.log(first.captured().call(second), second.captured().call({})); + try { first.extract(second); } catch (error) { console.log(error instanceof TypeError); } + try { read.call(second); } catch (error) { console.log(error instanceof TypeError); } + """, "true false 3 3\n3 7\ntrue\ntrue\n"]; + yield return ["closures-and-static-suspension", """ + class Box { + value = 3; + #read(): () => number { return () => this.value; } + async extract(object: any) { await Promise.resolve(0); return () => object.#read; } + static async #later() { await Promise.resolve(0); return this; } + static *#values() { yield this; } + static async *#more() { await Promise.resolve(0); yield this; } + static values() { return [this.#later, this.#values, this.#more]; } + } + async function run() { + const first = new Box(), read = (await first.extract(first))(); + console.log(read.call({ value: 11 })()); + const values: any = Box.values(); + const later = values[0], stream = values[1], more = values[2]; + console.log(await later() === undefined, stream().next().value === undefined); + console.log((await more().next()).value === undefined); + } + run().catch(error => console.log("error", error)); + """, "11\ntrue true\ntrue\n"]; + yield return ["identity-and-receivers", """ + class Box { + value: number; + constructor(value: number) { this.value = value; } + #read(): number { return this.value; } + extract() { return this.#read; } + } + const first = new Box(3), second = new Box(7), read = first.extract(); + console.log(read === first.extract(), read === second.extract()); + console.log(read.call(second), read.apply({ value: 11 }, []), read.bind(second)()); + console.log(read.name, read.length); + try { new (read as any)(); } catch (error) { console.log(error instanceof TypeError); } + """, "true true\n7 11 7\n#read 0\ntrue\n"]; + yield return ["generic-identity", """ + class Box { + value: T; + constructor(value: T) { this.value = value; } + #read(): T { return this.value; } + extract() { return this.#read; } + } + const first = new Box(3), second = new Box("seven"), read = first.extract(); + console.log(read === first.extract(), (read as any) === second.extract()); + console.log(read.call(second), read.call({ value: 11 })); + """, "true true\nseven 11\n"]; + yield return ["brands-and-strict-this", """ + let reads = 0; + class Box { + #value = 3; + #read() { return this.#value; } + #receiver() { return this; } + extract(object: any) { return object.#read; } + receiver() { return this.#receiver; } + } + const first = new Box(), read = first.extract(first); + console.log(read.call(first)); + try { first.extract((reads++, {})); } catch (error) { console.log(error instanceof TypeError, reads); } + try { read.call({}); } catch (error) { console.log(error instanceof TypeError); } + try { read.call(null); } catch (error) { console.log(error instanceof TypeError); } + const receiver = first.receiver(); + console.log(receiver() === undefined, receiver.call(null) === null); + """, "3\ntrue 1\ntrue\ntrue\ntrue true\n"]; + yield return ["static-and-defaults", """ + class Box { + static value = 7; + static #read(first = 2, ...rest: any[]) { return this.value + first + rest.length; } + static extract(object: any) { return object.#read; } + } + const read = Box.extract(Box); + console.log(read === Box.extract(Box), read.name, read.length); + console.log(read.call({ value: 11 }), read.apply({ value: 11 }, [3, 4, 5])); + try { Box.extract({}); } catch (error) { console.log(error instanceof TypeError); } + """, "true #read 0\n13 16\ntrue\n"]; + yield return ["suspended-method-kinds", """ + class Box { + value: T; + constructor(value: T) { this.value = value; } + async #read() { await Promise.resolve(0); return this.value; } + *#values() { yield this.value; } + async *#later() { await Promise.resolve(0); yield this.value; } + extract() { return [this.#read, this.#values, this.#later]; } + } + async function run() { + const first = new Box(3), second = new Box("seven"); + const values: any = first.extract(), other: any = second.extract(); + console.log(values[0] === other[0], values[1] === other[1], values[2] === other[2]); + console.log(await values[0].call({ value: 11 })); + console.log(values[1].call(second).next().value); + console.log((await values[2].call({ value: 13 }).next()).value); + } + run(); + """, "true true true\n11\nseven\n13\n"]; + } + + [Theory, MemberData(nameof(Cases))] + public void PrivateMethodsHaveCanonicalUnboundValues(string name, string source, string expected) + { + Assert.NotEmpty(name); + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + Assert.Equal(expected, TestHarness.RunCompiledStandalone(source)); + } + + [Theory, ModeData] + public void ModulePrivateMethodValuesBelongToTheirDeclaration(ExecutionMode mode) + { + var files = new Dictionary + { + ["left.ts"] = "export class Box { #read() { return 3; } extract() { return this.#read; } }", + ["right.ts"] = "export class Box { #read() { return 7; } extract() { return this.#read; } }", + ["main.ts"] = """ + import { Box as Left } from './left'; + import { Box as Right } from './right'; + const left = new Left().extract(), other = new Left().extract(), right = new Right().extract(); + console.log(left === other, left === right, left(), right()); + """ + }; + Assert.Equal("true false 3 7\n", TestHarness.RunModules(files, "main.ts", mode)); + if (mode == ExecutionMode.Compiled) Assert.Empty(TestHarness.CompileModulesAndVerifyOnly(files, "main.ts")); + } + + [Theory] + [InlineData("class Box { #read() {} } new Box().#read;")] + [InlineData("class Box { #read() {} replace() { this.#read = () => {}; } }")] + [InlineData("class Box { extract() { return this.#missing; } }")] + public void InvalidPrivateMethodAccessIsRejected(string source) + { + Assert.Throws(() => TestHarness.RunInterpreted(source)); + Assert.Throws(() => TestHarness.RunCompiled(source)); + } +} From 1d32071deb990d83a915f9e3b6e0269febf3f07d Mon Sep 17 00:00:00 2001 From: nickna Date: Tue, 6 Oct 2026 02:48:00 -0700 Subject: [PATCH 07/16] Retain and dispatch runtime-selected user-class parents (#1967) --- docs/plans/runtime-valued-superclass.md | 36 ++++ .../CallHandlers/SuperConstructorHandler.cs | 11 + .../CompilationContext.ClassExpressions.cs | 1 + .../EmittedClassDefinitionRuntime.cs | 16 +- .../ExpressionEmitterBase.CallHelpers.cs | 1 + .../ExpressionEmitterBase.ClassDefinitions.cs | 28 +++ .../ILCompiler.Classes.ClassExpressions.cs | 26 ++- ...Compiler.Classes.DefinitionMethodValues.cs | 11 +- .../ILCompiler.Classes.Definitions.cs | 69 ++++--- .../ILCompiler.Classes.HasFields.cs | 27 +-- .../Compilation/ILCompiler.Classes.Methods.cs | 4 + .../ILCompiler.Classes.PrivateMethodValues.cs | 3 +- .../ILCompiler.Classes.Prototypes.cs | 38 +++- .../ILCompiler.Classes.RuntimeParents.cs | 195 ++++++++++++++++++ src/SharpTS/Compilation/ILCompiler.Classes.cs | 3 +- .../ILCompiler.ContextFactories.cs | 2 + src/SharpTS/Compilation/ILEmitter.Calls.cs | 1 + .../Compilation/ILEmitter.Statements.cs | 12 ++ ...eEmitter.ClassDefinition.RuntimeParents.cs | 103 +++++++++ .../RuntimeEmitter.ClassDefinition.cs | 26 ++- .../Compilation/RuntimeEmitter.Operators.cs | 21 ++ .../RuntimeEmitter.RuntimeClass.cs | 1 + .../Compilation/StatementEmitterBase.cs | 17 ++ .../Execution/Interpreter.Expressions.cs | 2 +- .../Execution/Interpreter.Statements.cs | 2 +- .../SharedTests/RuntimeSuperclassTests.cs | 159 ++++++++++++++ 26 files changed, 746 insertions(+), 69 deletions(-) create mode 100644 src/SharpTS/Compilation/ILCompiler.Classes.RuntimeParents.cs create mode 100644 src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.RuntimeParents.cs create mode 100644 tests/SharpTS.Tests/SharedTests/RuntimeSuperclassTests.cs diff --git a/docs/plans/runtime-valued-superclass.md b/docs/plans/runtime-valued-superclass.md index 962346419..534a4579d 100644 --- a/docs/plans/runtime-valued-superclass.md +++ b/docs/plans/runtime-valued-superclass.md @@ -35,6 +35,42 @@ in-process harness. No behavior repair is claimed by this investigation. The finite implementation successor is [#1967](https://github.com/nickna/SharpTS/issues/1967), dependent on #1964. +## Implementation successor (#1967) + +The original failing outputs above remain investigation evidence. The bounded +repair retains each evaluated parent in #1964's guest class definition and uses +explicit-receiver entries on emitted user templates for initialization and +ordinary methods. The child constructor and instance prototype link to the +selected guest parent; inherited instance/static lookup and `instanceof` follow +those links. A runtime child allocates one receiver and initializes its parent on +that receiver. Checked known-parent declarations keep their CLR inheritance path. + +Runtime validation requires an emitted ordinary-user-class entry before static +initialization. The adapter boundary excludes generic, private, computed, +accessor, suspended-method, destructured/parameter-property and constructor-return +replacement shapes; their existing known-parent paths are retained. Arbitrary +host/CLR parents, exotic constructors and proxy construction are not added. +Constructor defaults, rest arguments and the caller's `arguments` list are +preserved for the supported ordinary path. + +On 2026-10-06, `RuntimeSuperclassTests`' 13 cases pass: the three investigation +fixtures, alternating Left/Right selections with real pending suspension at both +an expression and local declaration, forwarding, original receiver/`super` +behavior, expression-parent identity, defaults/rest/arguments/static receiver, +and invalid/rejected heritage before static initialization. Runtime execution and +serialized IL verification are asserted separately. The fixtures also pass CLI +standalone and hosted execution with both default declarations and `--noLib`; +these deployment builds use `--verify` and omit a `SharpTS.dll` payload. For the +deployment controls only, the already-called `main` runner is renamed to avoid +the EXE/hosted automatic entry-point convention calling it twice. + +The focused Release run passes 169 tests across runtime superclass, arguments, +async local declarations, expression owner identity, repeated class state and +namespace class ownership. This includes the retained known-parent controls and +generic/computed identity controls. The Release test-project build succeeds with +the existing NU1902 package warning. Consolidated quality/AOT gates are recorded +with the issues 1961–1968 PR; this section does not claim Native AOT publication. + ## Representation decision **Store and use the evaluated superclass in the guest class definition, using diff --git a/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs b/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs index 8ad9454e7..85ae18d0b 100644 --- a/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs +++ b/src/SharpTS/Compilation/CallHandlers/SuperConstructorHandler.cs @@ -22,6 +22,17 @@ public bool TryHandle(IEmitterContext emitter, Expr.Call call) return false; var ctx = emitter.Context; + if (ctx.HasGuestReceiver && ctx.ClassDefinitionParameterIndex is { } definitionParameter) + { + emitter.IL.Emit(OpCodes.Ldarg, definitionParameter); + emitter.IL.Emit(OpCodes.Call, ctx.Runtime!.ClassDefinitions.GetParent); + emitter.IL.Emit(OpCodes.Ldarg_0); + emitter.EmitArgsArrayWithSpread(call.Arguments); + emitter.IL.Emit(OpCodes.Call, ctx.Runtime.ClassDefinitions.InitializeReceiver); + emitter.IL.Emit(OpCodes.Ldarg_0); + emitter.SetStackUnknown(); + return true; + } // The emitted base is chosen from checked declaration identity. Prefer its // expression-owned constructor before any same-named lexical declaration. diff --git a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs index d1563d5a2..8ef3b6cef 100644 --- a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs +++ b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs @@ -13,6 +13,7 @@ public partial class CompilationContext public Dictionary? ClassExprBuilders { get; set; } internal Dictionary? ClassExprFactories { get; set; } internal Dictionary? ClassExprDefinitionFields { get; set; } + internal Dictionary? RuntimeClassDeclarations { get; set; } // Class expression extended tracking public Dictionary>? ClassExprBackingFields { get; set; } diff --git a/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs index c5fb9c942..e42cb99eb 100644 --- a/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs +++ b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs @@ -12,7 +12,8 @@ internal sealed record Declarations(TypeBuilder Type, Type InstanceInterface, FieldBuilder Factory, FieldBuilder Prototype, FieldBuilder Keys, FieldBuilder Parent, FieldBuilder Captures, FieldBuilder PrivateMembers, MethodBuilder Create, MethodBuilder Construct, MethodBuilder ReadProperty, MethodBuilder Invoke, TypeBuilder CaptureType, ConstructorBuilder CaptureConstructor, FieldBuilder CaptureOwner, FieldBuilder CaptureField, MethodBuilder ReadCapture, MethodBuilder WriteCapture, - MethodBuilder FindEnvironment); + MethodBuilder FindEnvironment, MethodBuilder ValidateParent, MethodBuilder InitializeReceiver, + MethodBuilder GetParent, MethodBuilder ReadArgument, MethodBuilder ReadPrototypeProperty, MethodBuilder ReadSuper); private Declarations? _declarations; private Declarations Required => _declarations @@ -37,6 +38,12 @@ internal sealed record Declarations(TypeBuilder Type, Type InstanceInterface, public MethodBuilder ReadCapture => Required.ReadCapture; public MethodBuilder WriteCapture => Required.WriteCapture; public MethodBuilder FindEnvironment => Required.FindEnvironment; + public MethodBuilder ValidateParent => Required.ValidateParent; + public MethodBuilder InitializeReceiver => Required.InitializeReceiver; + public MethodBuilder GetParent => Required.GetParent; + public MethodBuilder ReadArgument => Required.ReadArgument; + public MethodBuilder ReadPrototypeProperty => Required.ReadPrototypeProperty; + public MethodBuilder ReadSuper => Required.ReadSuper; internal void Declare(Declarations declarations) { @@ -46,7 +53,8 @@ internal void Declare(Declarations declarations) if (new MemberInfo[] { declarations.Constructor, declarations.Template, declarations.Factory, declarations.Prototype, declarations.Keys, declarations.Parent, declarations.Captures, declarations.PrivateMembers, declarations.Create, declarations.Construct, declarations.ReadProperty, declarations.Invoke, declarations.ReadCapture, declarations.WriteCapture, - declarations.FindEnvironment } + declarations.FindEnvironment, declarations.ValidateParent, declarations.InitializeReceiver, + declarations.GetParent, declarations.ReadArgument, declarations.ReadPrototypeProperty, declarations.ReadSuper } .Any(member => member.DeclaringType != declarations.Type) || declarations.GetDefinition.DeclaringType != declarations.InstanceInterface || declarations.InstanceInterface.Module != declarations.Type.Module @@ -62,7 +70,9 @@ internal void CompleteEmission() var declarations = Required; if (IsComplete) throw new InvalidOperationException("Class-definition emission is already complete."); if (new[] { declarations.Create, declarations.Construct, declarations.ReadProperty, declarations.Invoke, - declarations.ReadCapture, declarations.WriteCapture, declarations.FindEnvironment } + declarations.ReadCapture, declarations.WriteCapture, declarations.FindEnvironment, + declarations.ValidateParent, declarations.InitializeReceiver, declarations.GetParent, + declarations.ReadArgument, declarations.ReadPrototypeProperty, declarations.ReadSuper } .Any(method => method.GetILGenerator().ILOffset == 0)) throw new InvalidOperationException("Class-definition method bodies have not all been emitted."); IsComplete = true; diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs index 8e5915078..738c0f436 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs @@ -30,6 +30,7 @@ public abstract partial class ExpressionEmitterBase /// protected virtual void EmitCall(Expr.Call c) { + if (TryEmitGuestSuperCall(c)) return; if (TryEmitGuestThisCall(c)) return; if (TryEmitNumericRestCompanionCall(c)) return; if (TryEmitStaticIndirectEvalGlobal(c)) diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs index ceade2248..55cd6f8fd 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs @@ -6,6 +6,31 @@ namespace SharpTS.Compilation; public abstract partial class ExpressionEmitterBase { + protected bool TryEmitGuestSuperCall(Expr.Call call) + { + if (!Ctx.HasGuestReceiver || call.Callee is not Expr.Super { Method: not null } super + || super.Method.Lexeme == "constructor") return false; + var receiver = SpillBoxed(new Expr.This(new Token(TokenType.THIS, "this", null, 0))); + EmitGuestSuperValue(super.Method.Lexeme, receiver); + EmitGuestMethodInvocation(receiver, call.Arguments); + return true; + } + + protected void EmitGuestSuperValue(string name, LocalBuilder receiver) + { + if (Ctx.CurrentClassExpr != null && TryEmitOwnedClassDefinition()) { } + else + { + IL.Emit(OpCodes.Ldtoken, Ctx.CurrentClassBuilder!); + IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); + } + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Ldstr, name); + IL.Emit(Ctx.IsInstanceMethod ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1); + IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassDefinitions.ReadSuper); + SetStackUnknown(); + } + protected bool TryEmitGuestThisSet(Expr.Set set) { if (set.Object is not Expr.This || (!Ctx.HasGuestReceiver && Ctx.GuestThisVariableName == null)) return false; @@ -162,6 +187,9 @@ protected void EmitGuestClassDefinition(Expr.ClassExpr expression) { EmitClassHeritageValue(heritage, expression.Name?.Lexeme); EnsureBoxed(); + if ((factory!.Template.BaseType == null || factory.Template.BaseType == Types.Object) + && heritage is not Expr.ClassExpr { Methods.Count: 0, Fields.Count: 0 }) + IL.Emit(OpCodes.Call, Ctx.Runtime!.ClassDefinitions.ValidateParent); } else IL.Emit(OpCodes.Ldnull); var parent = _helpers.SpillStoreObject(); diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs index 41c1e021b..6c403a59c 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs @@ -97,7 +97,10 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) // NOW resolve superclass (may use our generic params for type arguments) Type? baseType = null; - if (superclassName != null) + bool runtimeHeritage = classExpr.SuperclassExpr != null && _typeMap.Get(classExpr.SuperclassExpr) is TSTypeInfo.Any + && superclassName is not ("Array" or "Promise") + && !Runtime.BuiltIns.BuiltInNames.IsErrorTypeName(superclassName ?? ""); + if (superclassName != null && !runtimeHeritage) { // Check class declarations first (with module resolution) var resolvedSuperName = GetDefinitionContext().ResolveClassName(superclassName); @@ -360,6 +363,7 @@ private void DefineClassExpressionMethodSignatures(Expr.ClassExpr classExpr) CallingConventions.Standard, [.. ctorParamTypes, _runtime.ClassDefinitions.Type] ); + if (HasRuntimeParent(classExpr)) _usesRuntimeParents = true; if (constructor == null && superclassName == "Array") MarkJsVariadicConstructor(ctorBuilder); _classExprs.Constructors[classExpr] = ctorBuilder; @@ -507,6 +511,10 @@ private void EmitClassExpressionBody(Expr.ClassExpr classExpr) string className = _classExprs.Names[classExpr]; var fieldsField = _classes.InstanceFieldsField[className]; + if (_usesRuntimeParents && !typeBuilder.IsGenericTypeDefinition && !_classes.ErrorSubclasses.Contains(className) + && typeBuilder.BaseType is not { Name: "$Array" or "$Promise" } + && SupportsReceiverInitialization(ClassExpressionDeclaration(classExpr))) + EmitReceiverInitializer(typeBuilder, ClassExpressionDeclaration(classExpr), classExpr); // Emit the constructor-free prototype path before the .cctor that uses it. EmitClassPrototypeConstructor(typeBuilder); @@ -647,6 +655,15 @@ private void EmitClassExpressionConstructor(Expr.ClassExpr classExpr, TypeBuilde var constructor = classExpr.Methods.FirstOrDefault(m => !m.IsStatic && m.Name.Lexeme == "constructor" && m.Body != null); var il = ctorBuilder.GetILGenerator(); + if (HasRuntimeParent(classExpr)) + { + // The definition factory allocates through the bookkeeping constructor + // and uses the explicit-receiver adapter for guest initialization. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.ObjectDefaultCtor); + il.Emit(OpCodes.Ret); + return; + } il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg, ctorBuilder.GetParameters().Length); il.Emit(OpCodes.Stfld, EmitterTypeHelpers.SelfFieldReference(_classExprs.DefinitionFields[classExpr])); @@ -920,8 +937,11 @@ void EmitInstanceFieldInitializers() return _classExprs.ToDefine.FirstOrDefault(candidate => _typeMap.GetClassExprType(candidate)?.Core.DeclarationId == parentId); - // Untyped heritage retains the existing fallback until its runtime value - // can be represented as a CLR base; checked declarations never use it. + if (classExpr.SuperclassExpr != null && _typeMap.Get(classExpr.SuperclassExpr) is TSTypeInfo.Any) + return null; + + // Only checked heritage can select a CLR base. Untyped values are retained + // in the guest definition and dispatched through its runtime parent. return _classExprs.Superclass.GetValueOrDefault(classExpr) is { } name ? _classExprs.VarToClassExpr.GetValueOrDefault(name) : null; } diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs index af3c3a192..84a48619a 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.DefinitionMethodValues.cs @@ -10,7 +10,7 @@ public partial class ILCompiler Expr.ClassExpr expression, MethodBuilder original, IReadOnlyList parameters) { var type = EmitTypeDefinitions.DefineType(_moduleBuilder, - $"$DefinitionMember_{_classExprs.Names[expression]}_{original.Name}", TypeAttributes.Public | TypeAttributes.Sealed, _types.Object); + $"$DefinitionMember_{_classExprs.Names[expression]}_{original.Name}_{(original.IsStatic ? "Static" : "Instance")}", TypeAttributes.Public | TypeAttributes.Sealed, _types.Object); var field = type.DefineField("Definition", _runtime.ClassDefinitions.Type, FieldAttributes.Assembly | FieldAttributes.InitOnly); var constructor = type.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, [_runtime.ClassDefinitions.Type]); var il = constructor.GetILGenerator(); @@ -21,7 +21,7 @@ public partial class ILCompiler il.Emit(OpCodes.Stfld, field); il.Emit(OpCodes.Ret); var method = type.DefineMethod("Invoke", MethodAttributes.Public, original.ReturnType, - [_types.Object, .. parameters.Select(_ => _types.Object)]); + [_types.Object, .. parameters.Select(parameter => parameter.IsRest ? _types.ListOfObject : _types.Object)]); method.DefineParameter(1, ParameterAttributes.None, "__this"); MarkExpectsThis(method); MarkNonConstructible(method); @@ -31,7 +31,7 @@ public partial class ILCompiler _classExprs.DefinitionMethods.Add(method, (expression, field)); Stmt.Function? source = expression.Methods.FirstOrDefault(source => - source.ComputedKey == null && source.Name.Lexeme == original.Name); + source.ComputedKey == null && source.IsStatic == original.IsStatic && source.Name.Lexeme == original.Name); source ??= _classes.ComputedMembers.GetMethods(_classExprs.Builders[expression]) .FirstOrDefault(entry => ReferenceEquals(entry.Builder, original)).Method; if (source == null) @@ -40,15 +40,16 @@ public partial class ILCompiler .FirstOrDefault(entry => ReferenceEquals(entry.Method, original)).Accessor; accessor ??= (expression.Accessors ?? []).First(accessor => original.Name == $"{(accessor.Kind.Type == TokenType.GET ? "get" : "set")}_{NamingConventions.ToPascalCase(accessor.Name.Lexeme)}"); - source = new Stmt.Function(accessor.Name, null, null, parameters.ToList(), accessor.Body, null); + source = new Stmt.Function(accessor.Name, null, null, parameters.ToList(), accessor.Body, null, IsStatic: accessor.IsStatic); } EmitGuestMethodValueBody(method, source, () => { var ctx = CreateClassExpressionContext(method.GetILGenerator(), expression, _classExprs.Builders[expression], null, method); ctx.EmittingTypeBuilder = type; - ctx.IsInstanceMethod = true; + ctx.IsInstanceMethod = !source.IsStatic; ctx.ClassDefinitionOwnerField = field; ctx.GuestThisVariableName = "__this"; + ctx.HasGuestReceiver = true; foreach (var parameter in expression.TypeParams ?? []) ctx.GenericTypeParameters[parameter.Name.Lexeme] = _types.Object; return ctx; }, _classExprs.Names[expression]); diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs index d00855bae..4722f6334 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Definitions.cs @@ -21,37 +21,60 @@ private void DefineClassExpressionFactory(Expr.ClassExpr expression, TypeBuilder var method = _programType.DefineMethod("$Create_" + _classExprs.Names[expression], MethodAttributes.Public | MethodAttributes.Static, _types.Object, [_types.ObjectArray, _types.Object]); var il = method.GetILGenerator(); - if (_functions.MethodsCapturingArguments.Contains(constructor)) + if (HasRuntimeParent(expression)) { + var receiver = il.DeclareLocal(template); + ConstructorInfo allocation = _classes.PrototypeConstructors[builder]; + if (template != builder) allocation = EmitterTypeHelpers.ResolveConstructor(template, allocation); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Newobj, allocation); + il.Emit(OpCodes.Stloc, receiver); + il.Emit(OpCodes.Ldloc, receiver); + il.Emit(OpCodes.Ldarg_1); + FieldInfo definitionField = _classExprs.DefinitionFields[expression]; + if (template != builder) definitionField = EmitterTypeHelpers.ResolveField(template, definitionField); + il.Emit(OpCodes.Stfld, definitionField); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, receiver); il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Stsfld, _runtime.Arguments.CurrentField); + il.Emit(OpCodes.Call, _runtime.ClassDefinitions.InitializeReceiver); + il.Emit(OpCodes.Ldloc, receiver); + il.Emit(OpCodes.Ret); } - for (int i = 0; i < parameterTypes.Length; i++) + else { - if (parameterTypes[i] == _types.ObjectArray) + if (_functions.MethodsCapturingArguments.Contains(constructor)) { il.Emit(OpCodes.Ldarg_0); - continue; + il.Emit(OpCodes.Stsfld, _runtime.Arguments.CurrentField); } - var missing = il.DefineLabel(); - var ready = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Conv_I4); - il.Emit(OpCodes.Ldc_I4, i); - il.Emit(OpCodes.Ble, missing); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldc_I4, i); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Br, ready); - il.MarkLabel(missing); - il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); - il.MarkLabel(ready); + for (int i = 0; i < parameterTypes.Length; i++) + { + if (parameterTypes[i] == _types.ObjectArray) + { + il.Emit(OpCodes.Ldarg_0); + continue; + } + var missing = il.DefineLabel(); + var ready = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ble, missing); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Br, ready); + il.MarkLabel(missing); + il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + il.MarkLabel(ready); + } + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Castclass, _runtime.ClassDefinitions.Type); + il.Emit(OpCodes.Newobj, target); + il.Emit(OpCodes.Ret); } - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Castclass, _runtime.ClassDefinitions.Type); - il.Emit(OpCodes.Newobj, target); - il.Emit(OpCodes.Ret); string name = expression.Name?.Lexeme ?? _classExprs.VarToClassExpr.FirstOrDefault(entry => ReferenceEquals(entry.Value, expression)).Key ?? ""; diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs b/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs index 847ce2f87..410e6d883 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.HasFields.cs @@ -1011,31 +1011,20 @@ private void EmitClassPrototypeFieldFallback(ILGenerator il) il.Emit(OpCodes.Ldloc, prototypeLocal); il.Emit(OpCodes.Ceq); il.Emit(OpCodes.Brtrue, skipLabel); - var descriptor = il.DeclareLocal(_runtime.DescriptorStorage.DescriptorType); var noDescriptor = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, _runtime.ClassDefinitions.InstanceInterface); il.Emit(OpCodes.Brfalse, noDescriptor); + var inherited = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldloc, prototypeLocal); il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Call, _runtime.DescriptorStorage.GetPropertyDescriptor); - il.Emit(OpCodes.Stloc, descriptor); - il.Emit(OpCodes.Ldloc, descriptor); - il.Emit(OpCodes.Brfalse, noDescriptor); - var data = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, descriptor); - il.Emit(OpCodes.Callvirt, _runtime.DescriptorStorage.DescriptorGetter.GetGetMethod()!); - il.Emit(OpCodes.Brfalse, data); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldloc, descriptor); - il.Emit(OpCodes.Callvirt, _runtime.DescriptorStorage.DescriptorGetter.GetGetMethod()!); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Newarr, _types.Object); - il.Emit(OpCodes.Call, _runtime.Invocation.Method); - il.Emit(OpCodes.Ret); - il.MarkLabel(data); - il.Emit(OpCodes.Ldloc, descriptor); - il.Emit(OpCodes.Callvirt, _runtime.DescriptorStorage.DescriptorValue.GetGetMethod()!); + il.Emit(OpCodes.Call, _runtime.ClassDefinitions.ReadPrototypeProperty); + il.Emit(OpCodes.Stloc, inherited); + il.Emit(OpCodes.Ldloc, inherited); + il.Emit(OpCodes.Isinst, _runtime.Sentinels.UndefinedType); + il.Emit(OpCodes.Brtrue, noDescriptor); + il.Emit(OpCodes.Ldloc, inherited); il.Emit(OpCodes.Ret); il.MarkLabel(noDescriptor); il.Emit(OpCodes.Ldloc, prototypeLocal); diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs index 8a0a43d26..c84ba9b3a 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Methods.cs @@ -30,6 +30,7 @@ private void EmitAllHasFieldsInterfaceMethodBodies(IEnumerable statements) { foreach (var classStmt in CollectClassDeclarations(statements)) { + if (_runtimeClassDeclarations.ContainsKey(classStmt)) continue; using var namespaceScope = new ClassNamespaceScope(this, classStmt); if (classStmt.IsDeclare) continue; @@ -588,6 +589,9 @@ private void EmitClassMethods(Stmt.Class classStmt) // Emit constructor EmitConstructor(typeBuilder, classStmt, fieldsField); + if (_usesRuntimeParents && !typeBuilder.IsGenericTypeDefinition && typeBuilder.BaseType is not { Name: "$Array" or "$Promise" } + && !_classes.ErrorSubclasses.Contains(qualifiedClassName) && SupportsReceiverInitialization(classStmt)) + EmitReceiverInitializer(typeBuilder, classStmt); // Emit the compiler-only constructor used to create Constructor.prototype // without running JavaScript constructor bodies or field initializers. diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.PrivateMethodValues.cs b/src/SharpTS/Compilation/ILCompiler.Classes.PrivateMethodValues.cs index f38a24cd4..09c294917 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.PrivateMethodValues.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.PrivateMethodValues.cs @@ -172,7 +172,8 @@ private void EmitGuestMethodValueBody(MethodBuilder method, Stmt.Function source var parameterTypes = method.GetParameters().Skip(1).Select(parameter => parameter.ParameterType).ToArray(); for (int i = 0; i < source.Parameters.Count; i++) ctx.DefineParameter(source.Parameters[i].Name.Lexeme, i + 2, parameterTypes[i]); var emitter = new ILEmitter(ctx); - EmitFunctionEnvironmentPrologue(il, ctx, emitter, source.Parameters, source.Body, parameterTypes, argumentOffset: 2); + EmitFunctionEnvironmentPrologue(il, ctx, emitter, source.Parameters, source.Body, parameterTypes, + argumentOffset: 2, publishedArgsLeadingSkip: 1); InitializeSyncMethodCapturedParameters(ctx, il, source, method, argumentOffset: 2); foreach (var statement in source.Body ?? []) emitter.EmitStatement(statement); if (emitter.HasDeferredReturns) emitter.FinalizeReturns(); diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Prototypes.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Prototypes.cs index 87551dcf5..b05737d1c 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Prototypes.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Prototypes.cs @@ -12,7 +12,7 @@ private void DefineClassPrototypeConstructor(TypeBuilder typeBuilder) return; _classes.PrototypeConstructors[typeBuilder] = typeBuilder.DefineConstructor( - MethodAttributes.Family, + MethodAttributes.FamORAssem, CallingConventions.Standard, [_runtime.ClassPrototypes.MarkerType]); } @@ -270,11 +270,19 @@ void EmitPrototypeDataDescriptor(string propertyName, Action emitValue) EmitPrototypeDataDescriptor(methodName, () => { - il.Emit(OpCodes.Ldloc, prototypeLocal); - il.Emit(OpCodes.Ldtoken, methodBuilder); - il.Emit(OpCodes.Ldtoken, selfType); - il.Emit(OpCodes.Call, _types.MethodBaseGetMethodFromHandleWithType); - il.Emit(OpCodes.Castclass, _types.MethodInfo); + if (DefineDeclarationReceiverMethod(typeBuilder, methodBuilder) is { } adapter) + { + il.Emit(OpCodes.Newobj, adapter.Constructor); + EmitMethodInfoLiteral(il, adapter.Method, adapter.Type); + } + else + { + il.Emit(OpCodes.Ldloc, prototypeLocal); + il.Emit(OpCodes.Ldtoken, methodBuilder); + il.Emit(OpCodes.Ldtoken, selfType); + il.Emit(OpCodes.Call, _types.MethodBaseGetMethodFromHandleWithType); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + } il.Emit(OpCodes.Newobj, _runtime.FunctionConstruction.Constructor); }); } @@ -354,11 +362,19 @@ void EmitStaticDataDescriptor(string propertyName, MethodBuilder methodBuilder) void EmitStaticFunction(MethodBuilder methodBuilder) { - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Ldtoken, methodBuilder); - il.Emit(OpCodes.Ldtoken, selfType); - il.Emit(OpCodes.Call, _types.MethodBaseGetMethodFromHandleWithType); - il.Emit(OpCodes.Castclass, _types.MethodInfo); + if (DefineDeclarationReceiverMethod(typeBuilder, methodBuilder) is { } adapter) + { + il.Emit(OpCodes.Newobj, adapter.Constructor); + EmitMethodInfoLiteral(il, adapter.Method, adapter.Type); + } + else + { + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ldtoken, methodBuilder); + il.Emit(OpCodes.Ldtoken, selfType); + il.Emit(OpCodes.Call, _types.MethodBaseGetMethodFromHandleWithType); + il.Emit(OpCodes.Castclass, _types.MethodInfo); + } il.Emit(OpCodes.Newobj, _runtime.FunctionConstruction.Constructor); } } diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.RuntimeParents.cs b/src/SharpTS/Compilation/ILCompiler.Classes.RuntimeParents.cs new file mode 100644 index 000000000..75dc8917c --- /dev/null +++ b/src/SharpTS/Compilation/ILCompiler.Classes.RuntimeParents.cs @@ -0,0 +1,195 @@ +using System.Reflection; +using System.Reflection.Emit; +using SharpTS.Parsing; +using SharpTS.Parsing.Visitors; + +namespace SharpTS.Compilation; + +public partial class ILCompiler +{ + private readonly Dictionary _runtimeClassDeclarations = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary _receiverInitializers = new(ReferenceEqualityComparer.Instance); + private bool _usesRuntimeParents; + + private bool TryDefineRuntimeClassDeclaration(Stmt.Class declaration) + { + if (declaration.SuperclassExpr == null || declaration.IsDeclare) return false; + var leaf = Expr.GetSuperclassLeafName(declaration.SuperclassExpr); + if (leaf != null && ((_typeMap.Get(declaration.SuperclassExpr) is not TypeSystem.TypeInfo.Any + && (_classes.Builders.ContainsKey(GetDefinitionContext().ResolveClassName(leaf)) || _classExprs.VarToClassExpr.ContainsKey(leaf))) + || Runtime.BuiltIns.BuiltInNames.IsErrorTypeName(leaf) + || leaf is "Array" or "Promise" or "Map" or "Set" or "WeakMap" or "WeakSet" or "Date" or "RegExp" or "Buffer")) return false; + if (declaration.SuperclassExpr is Expr.ClassExpr { Methods.Count: 0, Fields.Count: 0 }) return false; + var expression = new Expr.ClassExpr(declaration.Name, declaration.TypeParams, + declaration.SuperclassExpr, declaration.SuperclassTypeArgs, declaration.Methods, + declaration.Fields, declaration.Accessors, declaration.AutoAccessors, declaration.Interfaces, + declaration.InterfaceTypeArgs, declaration.IsAbstract, declaration.StaticInitializers); + _runtimeClassDeclarations.Add(declaration, expression); + _usesRuntimeParents = true; + if (_typeMap.GetClassType(declaration) is { } checkedType) _typeMap.SetClassExprType(expression, checkedType); + CollectClassExpression(expression); + DefineClassExpression(expression); + _classes.BlockScopedBuilders[declaration] = _classExprs.Builders[expression]; + _classes.DeclarationNames[declaration] = _classExprs.Names[expression]; + _classes.DeclarationNamespaces[declaration] = _currentNamespacePath; + return true; + } + + private bool HasRuntimeParent(Expr.ClassExpr expression) => expression.SuperclassExpr != null + && (_classExprs.Builders[expression].BaseType == null || _classExprs.Builders[expression].BaseType == _types.Object) + && expression.SuperclassExpr is not Expr.ClassExpr { Methods.Count: 0, Fields.Count: 0 }; + + private static bool SupportsReceiverInitialization(Stmt.Class declaration) + { + // The finite runtime-parent path is ordinary fields and synchronous + // methods. Other templates keep their existing CLR construction path. + if (declaration.TypeParams is { Count: > 0 } || declaration.Accessors is { Count: > 0 } + || declaration.AutoAccessors is { Count: > 0 } + || declaration.Fields.Any(field => field.IsPrivate || field.ComputedKey != null) + || declaration.Methods.Any(method => method.IsPrivate || method.IsAsync || method.IsGenerator + || method.TypeParams is { Count: > 0 } || method.ComputedKey != null + || method.Parameters.Any(parameter => parameter.IsParameterProperty || parameter.DestructuredProperties != null))) return false; + var returns = new ConstructorReplacementVisitor(); + foreach (var statement in declaration.Methods.FirstOrDefault(method => method.Name.Lexeme == "constructor")?.Body ?? []) + returns.Visit(statement); + return !returns.HasReplacement; + } + + private sealed class ConstructorReplacementVisitor : AstVisitorBase + { + public bool HasReplacement { get; private set; } + protected override void VisitReturn(Stmt.Return statement) => HasReplacement |= statement.Value != null; + protected override void VisitFunction(Stmt.Function statement) { } + protected override void VisitArrowFunction(Expr.ArrowFunction expression) { } + protected override void VisitClass(Stmt.Class statement) { } + protected override void VisitClassExpr(Expr.ClassExpr expression) { } + } + + private MethodBuilder DefineReceiverInitializer(TypeBuilder owner) + { + if (_receiverInitializers.TryGetValue(owner, out var existing)) return existing; + var method = owner.DefineMethod("$InitializeReceiver", MethodAttributes.Public | MethodAttributes.Static, + _types.Void, [_types.Object, _types.ObjectArray, _types.Object]); + _receiverInitializers.Add(owner, method); + return method; + } + + private void EmitReceiverInitializer(TypeBuilder owner, Stmt.Class declaration, Expr.ClassExpr? expression = null) + { + // These entries deliberately use an explicit guest receiver. A runtime-selected + // child need not be a CLR subtype of this template. + var method = DefineReceiverInitializer(owner); + var il = method.GetILGenerator(); + var ctx = expression == null ? CreateModuleMemberContext(il, method) + : CreateClassExpressionContext(il, expression, owner, null, method); + ctx.IsStrictMode = true; + ctx.CurrentClassBuilder = owner; + ctx.CurrentClassName = expression == null ? GetQualifiedClassDeclarationName(declaration) : _classExprs.Names[expression]; + ctx.EmittingTypeBuilder = owner; + ctx.IsInstanceMethod = true; + ctx.HasGuestReceiver = true; + ctx.GuestThisVariableName = "__this"; + ctx.ClassDefinitionParameterIndex = 2; + ctx.DefineParameter("__this", 0, _types.Object); + ApplyCapturedTopLevelVariableAccess(ctx); + var emitter = new ILEmitter(ctx); + var constructor = declaration.Methods.FirstOrDefault(source => source.Body != null && source.Name.Lexeme == "constructor" && !source.IsStatic); + if (constructor?.Body != null && ReferencesArgumentsIdentifier(constructor.Body) + && !constructor.Parameters.Any(parameter => parameter.Name.Lexeme == "arguments")) + { + var arguments = ctx.Locals.DeclareLocal("arguments", _types.ListOfObject); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Newobj, _runtime.Arguments.EnumerableCtor); + il.Emit(OpCodes.Stloc, arguments); + } + foreach (var (parameter, index) in (constructor?.Parameters ?? []).Select((parameter, index) => (parameter, index))) + { + var local = ctx.Locals.DeclareLocal(parameter.Name.Lexeme, _types.Object); + il.Emit(OpCodes.Ldarg_1); + if (parameter.IsRest) + { + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.ListOfObject, _types.IEnumerableOfObject)); + il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldlen); il.Emit(OpCodes.Conv_I4); il.Emit(OpCodes.Ldc_I4, index); + il.Emit(OpCodes.Call, _types.GetMethod(typeof(Math), "Min", _types.Int32, _types.Int32)); + il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldlen); il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Ldc_I4, index); il.Emit(OpCodes.Sub); il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Call, _types.GetMethod(typeof(Math), "Max", _types.Int32, _types.Int32)); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.ListOfObject, "GetRange", _types.Int32, _types.Int32)); + } + else + { + il.Emit(OpCodes.Ldc_I4, index); + il.Emit(OpCodes.Call, _runtime.ClassDefinitions.ReadArgument); + } + il.Emit(OpCodes.Stloc, local); + } + foreach (var parameter in (constructor?.Parameters ?? []).Where(parameter => parameter.DefaultValue != null)) + { + var local = ctx.Locals.GetLocal(parameter.Name.Lexeme)!; + var present = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, local); il.Emit(OpCodes.Isinst, _runtime.Sentinels.UndefinedType); il.Emit(OpCodes.Brfalse, present); + emitter.EmitExpression(parameter.DefaultValue!); emitter.EmitBoxIfNeeded(parameter.DefaultValue!); il.Emit(OpCodes.Stloc, local); + il.MarkLabel(present); + } + bool explicitSuper = constructor?.Body?.Any(ContainsSuperCall) == true; + if (!explicitSuper && declaration.SuperclassExpr != null) + { + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, _runtime.ClassDefinitions.GetParent); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, _runtime.ClassDefinitions.InitializeReceiver); + } + bool initialized = false; + if (!explicitSuper) InitializeFields(); + foreach (var statement in constructor?.Body ?? []) + { + emitter.EmitStatement(statement); + if (!initialized && ContainsSuperCall(statement)) InitializeFields(); + } + if (!initialized) InitializeFields(); + emitter.FinalizeReturns(); + il.Emit(OpCodes.Ret); + + void InitializeFields() + { + initialized = true; + foreach (var field in declaration.Fields.Where(field => !field.IsStatic && !field.IsDeclare && !field.IsPrivate)) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, field.Name.Lexeme); + 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 (TypeBuilder Type, ConstructorBuilder Constructor, MethodBuilder Method)? DefineDeclarationReceiverMethod( + TypeBuilder owner, MethodBuilder original) + { + if (!_usesRuntimeParents) return null; + var declaration = _classes.Declarations.FirstOrDefault(source => _classes.DeclarationNames.GetValueOrDefault(source) == owner.FullName); + var source = declaration?.Methods.FirstOrDefault(source => source.Body != null && source.IsStatic == original.IsStatic && source.Name.Lexeme == original.Name); + if (source == null || !SupportsReceiverInitialization(declaration!)) return null; + var type = EmitTypeDefinitions.DefineType(_moduleBuilder, $"$ReceiverMember_{owner.FullName}_{original.Name}_{(source.IsStatic ? "Static" : "Instance")}", TypeAttributes.Public | TypeAttributes.Sealed, _types.Object); + var constructor = type.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes); + var il = constructor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, _types.ObjectDefaultCtor); il.Emit(OpCodes.Ret); + var method = type.DefineMethod("Invoke", MethodAttributes.Public, original.ReturnType, + [_types.Object, .. source.Parameters.Select(parameter => parameter.IsRest ? _types.ListOfObject : _types.Object)]); + MarkExpectsThis(method); MarkNonConstructible(method); MarkPadsUndefined(method); + MarkFunctionLength(method, source.Parameters); MarkFunctionName(method, original.Name); + EmitGuestMethodValueBody(method, source, () => + { + var ctx = CreateModuleMemberContext(method.GetILGenerator(), method); + ctx.CurrentClassBuilder = owner; ctx.CurrentClassName = owner.FullName; + ctx.EmittingTypeBuilder = type; ctx.IsStrictMode = true; ctx.IsInstanceMethod = !source.IsStatic; + ctx.HasGuestReceiver = true; ctx.GuestThisVariableName = "__this"; + ApplyCapturedTopLevelVariableAccess(ctx); + return ctx; + }, owner.FullName!); + type.CreateType(); + return (type, constructor, method); + } +} diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.cs b/src/SharpTS/Compilation/ILCompiler.Classes.cs index 699120749..bd003340e 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.cs @@ -46,6 +46,7 @@ private static void MarkJsVariadicConstructor(ConstructorBuilder constructor) private void DefineClass(Stmt.Class classStmt) { _classes.Declarations.Add(classStmt); + if (TryDefineRuntimeClassDeclaration(classStmt)) return; var ctx = GetDefinitionContext(); // Get qualified class name (includes module prefix and .NET namespace if set) @@ -237,7 +238,7 @@ private void DefineClass(Stmt.Class classStmt) } // Track Error subclass status (direct or transitive) - if (classStmt.SuperclassExpr != null && Runtime.BuiltIns.BuiltInNames.IsErrorTypeName(Expr.GetSuperclassLeafName(classStmt.SuperclassExpr)!)) + if (Expr.GetSuperclassLeafName(classStmt.SuperclassExpr) is { } errorParentName && Runtime.BuiltIns.BuiltInNames.IsErrorTypeName(errorParentName)) { _classes.ErrorSubclasses.Add(qualifiedClassName); } diff --git a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs index 44e783449..11e8fdd0c 100644 --- a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs +++ b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs @@ -94,6 +94,7 @@ private CompilationContext CreateBaseCompilationContext(ILGenerator il, MethodBa ClassExprBuilders = _classExprs.Builders, ClassExprFactories = _classExprs.Factories, ClassExprDefinitionFields = _classExprs.DefinitionFields, + RuntimeClassDeclarations = _runtimeClassDeclarations, ClassExprStaticMethods = _classExprs.StaticMethods, ClassExprCaptureSlots = _classExprs.CaptureSlots, DeferredClassDefinitions = _classes.DeferredDefinitions, @@ -300,6 +301,7 @@ private CompilationContext CreateNestedAsyncArrowContext(ILGenerator il, Compila ClassExprBuilders = parentCtx.ClassExprBuilders, ClassExprFactories = parentCtx.ClassExprFactories, ClassExprDefinitionFields = parentCtx.ClassExprDefinitionFields, + RuntimeClassDeclarations = parentCtx.RuntimeClassDeclarations, ClassExprCaptureSlots = parentCtx.ClassExprCaptureSlots, BlockScopedClassBuilders = parentCtx.BlockScopedClassBuilders, DeferredClassDefinitions = parentCtx.DeferredClassDefinitions, diff --git a/src/SharpTS/Compilation/ILEmitter.Calls.cs b/src/SharpTS/Compilation/ILEmitter.Calls.cs index dcc9b5792..d1f753286 100644 --- a/src/SharpTS/Compilation/ILEmitter.Calls.cs +++ b/src/SharpTS/Compilation/ILEmitter.Calls.cs @@ -164,6 +164,7 @@ protected override void VisitClassExpr(Expr.ClassExpr expr) protected override void EmitCall(Expr.Call c) { + if (TryEmitGuestSuperCall(c)) return; if (TryEmitGuestThisCall(c)) return; if (TryEmitPromotedObjectConsumer(c)) return; if (TryEmitNumericRestCompanionCall(c)) return; diff --git a/src/SharpTS/Compilation/ILEmitter.Statements.cs b/src/SharpTS/Compilation/ILEmitter.Statements.cs index ccdc1fb47..3119ec07f 100644 --- a/src/SharpTS/Compilation/ILEmitter.Statements.cs +++ b/src/SharpTS/Compilation/ILEmitter.Statements.cs @@ -3279,6 +3279,18 @@ static void CollectDirectLexicalNames(Stmt statement, HashSet names) private void EmitBlockScopedClassDeclaration(Stmt.Class classStmt) { + if (_ctx.RuntimeClassDeclarations?.TryGetValue(classStmt, out var runtimeExpression) == true) + { + EmitGuestClassDefinition(runtimeExpression); + var definition = IL.DeclareLocal(_ctx.Types.Object); + IL.Emit(OpCodes.Stloc, definition); + if (_ctx.Locals.GetLocal(classStmt.Name.Lexeme) is { } binding) + { + IL.Emit(OpCodes.Ldloc, definition); + IL.Emit(OpCodes.Stloc, binding); + } + return; + } var scopedClasses = _ctx.BlockScopedClassBuilders; TypeBuilder? builder = null; bool isBlockScoped = scopedClasses != null diff --git a/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.RuntimeParents.cs b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.RuntimeParents.cs new file mode 100644 index 000000000..357deb644 --- /dev/null +++ b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.RuntimeParents.cs @@ -0,0 +1,103 @@ +using System.Reflection; +using System.Reflection.Emit; + +namespace SharpTS.Compilation; + +public partial class RuntimeEmitter +{ + private void EmitRuntimeParentBodies(EmittedRuntime runtime) + { + var definitions = runtime.ClassDefinitions; + var il = definitions.ValidateParent.GetILGenerator(); + var supported = il.DefineLabel(); + var parentType = il.DeclareLocal(_types.Type); + var plainType = il.DefineLabel(); var inspectType = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, definitions.Type); il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, plainType); il.Emit(OpCodes.Ldfld, definitions.Template); il.Emit(OpCodes.Br, inspectType); + il.MarkLabel(plainType); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, _types.Type); + il.MarkLabel(inspectType); il.Emit(OpCodes.Stloc, parentType); + var unsupported = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, parentType); il.Emit(OpCodes.Brfalse, unsupported); + il.Emit(OpCodes.Ldloc, parentType); il.Emit(OpCodes.Ldstr, "$InitializeReceiver"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetMethod", _types.String)); + il.Emit(OpCodes.Brtrue, supported); + il.MarkLabel(unsupported); + GuestErrorEmitter.ThrowError(il, runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor, + "Runtime superclass must be an ordinary user class constructor"); + il.MarkLabel(supported); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ret); + + il = definitions.GetParent.GetILGenerator(); + var staticOwner = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, definitions.Type); il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, staticOwner); il.Emit(OpCodes.Ldfld, definitions.Parent); il.Emit(OpCodes.Ret); + il.MarkLabel(staticOwner); il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Castclass, _types.Type); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Type, "BaseType")!.GetGetMethod()!); il.Emit(OpCodes.Ret); + + il = definitions.ReadArgument.GetILGenerator(); + var missing = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldlen); il.Emit(OpCodes.Conv_I4); + il.Emit(OpCodes.Bge, missing); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldelem_Ref); il.Emit(OpCodes.Ret); + il.MarkLabel(missing); il.Emit(OpCodes.Ldsfld, runtime.Sentinels.UndefinedInstance); il.Emit(OpCodes.Ret); + + il = definitions.InitializeReceiver.GetILGenerator(); + var nothing = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Brfalse, nothing); + var template = il.DeclareLocal(_types.Type); + var useType = il.DefineLabel(); var haveType = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Isinst, definitions.Type); il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brfalse, useType); il.Emit(OpCodes.Ldfld, definitions.Template); il.Emit(OpCodes.Br, haveType); + il.MarkLabel(useType); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Castclass, _types.Type); + il.MarkLabel(haveType); il.Emit(OpCodes.Stloc, template); + var adapter = il.DeclareLocal(_types.MethodInfo); + il.Emit(OpCodes.Ldloc, template); il.Emit(OpCodes.Ldstr, "$InitializeReceiver"); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.Type, "GetMethod", _types.String)); il.Emit(OpCodes.Stloc, adapter); + var haveAdapter = il.DefineLabel(); + il.Emit(OpCodes.Ldloc, adapter); il.Emit(OpCodes.Brtrue, haveAdapter); + il.Emit(OpCodes.Ldloc, template); il.Emit(OpCodes.Ldtoken, _types.Object); il.Emit(OpCodes.Call, _types.TypeGetTypeFromHandle); + il.Emit(OpCodes.Beq, nothing); + GuestErrorEmitter.ThrowError(il, runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor, + "Runtime superclass initialization requires an ordinary user class constructor"); + il.MarkLabel(haveAdapter); + il.BeginExceptionBlock(); + il.Emit(OpCodes.Ldloc, adapter); il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ldc_I4_3); il.Emit(OpCodes.Newarr, _types.Object); + il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldc_I4_1); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldc_I4_2); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Stelem_Ref); + il.Emit(OpCodes.Callvirt, _types.GetMethod(_types.MethodInfo, "Invoke", _types.Object, _types.ObjectArray)); il.Emit(OpCodes.Pop); + il.BeginCatchBlock(typeof(TargetInvocationException)); + il.Emit(OpCodes.Callvirt, _types.GetProperty(_types.Exception, "InnerException")!.GetGetMethod()!); + il.Emit(OpCodes.Throw); + il.EndExceptionBlock(); il.MarkLabel(nothing); il.Emit(OpCodes.Ret); + + il = definitions.ReadPrototypeProperty.GetILGenerator(); + var prototype = il.DeclareLocal(_types.Object); var descriptor = il.DeclareLocal(runtime.DescriptorStorage.DescriptorType); + il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Stloc, prototype); + var loop = il.DefineLabel(); var next = il.DefineLabel(); var absent = il.DefineLabel(); var data = il.DefineLabel(); + il.MarkLabel(loop); il.Emit(OpCodes.Ldloc, prototype); il.Emit(OpCodes.Brfalse, absent); + il.Emit(OpCodes.Ldloc, prototype); il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.DescriptorStorage.GetPropertyDescriptor); il.Emit(OpCodes.Stloc, descriptor); + il.Emit(OpCodes.Ldloc, descriptor); il.Emit(OpCodes.Brfalse, next); + il.Emit(OpCodes.Ldloc, descriptor); il.Emit(OpCodes.Callvirt, runtime.DescriptorStorage.DescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Brfalse, data); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, runtime.DescriptorStorage.DescriptorGetter.GetGetMethod()!); + il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Newarr, _types.Object); il.Emit(OpCodes.Call, runtime.Invocation.Method); il.Emit(OpCodes.Ret); + il.MarkLabel(data); il.Emit(OpCodes.Ldloc, descriptor); + il.Emit(OpCodes.Callvirt, runtime.DescriptorStorage.DescriptorValue.GetGetMethod()!); il.Emit(OpCodes.Ret); + il.MarkLabel(next); il.Emit(OpCodes.Ldloc, prototype); il.Emit(OpCodes.Call, runtime.ObjectPrototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, prototype); il.Emit(OpCodes.Br, loop); + il.MarkLabel(absent); il.Emit(OpCodes.Ldsfld, runtime.Sentinels.UndefinedInstance); il.Emit(OpCodes.Ret); + + il = definitions.ReadSuper.GetILGenerator(); + var instanceSuper = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_3); il.Emit(OpCodes.Brfalse, instanceSuper); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, definitions.GetParent); il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, runtime.ObjectRead.Property); il.Emit(OpCodes.Ret); + il.MarkLabel(instanceSuper); + il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Call, definitions.GetParent); + il.Emit(OpCodes.Ldstr, "prototype"); il.Emit(OpCodes.Call, runtime.ObjectRead.Property); + il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Call, definitions.ReadPrototypeProperty); il.Emit(OpCodes.Ret); + } +} diff --git a/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs index ff35c0348..3937ef84b 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs @@ -55,11 +55,24 @@ private void DefineClassDefinitionTypes(ModuleBuilder module, EmittedClassDefini _types.Void, [type, _types.Int32, _types.Object]); var findEnvironment = type.DefineMethod("FindEnvironment", MethodAttributes.Public | MethodAttributes.Static, _types.Object, [type, _types.Type]); + var validateParent = type.DefineMethod("ValidateParent", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object]); + var initializeReceiver = type.DefineMethod("InitializeReceiver", MethodAttributes.Public | MethodAttributes.Static, + _types.Void, [_types.Object, _types.Object, _types.ObjectArray]); + var getParent = type.DefineMethod("GetParent", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object]); + var readArgument = type.DefineMethod("ReadArgument", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.ObjectArray, _types.Int32]); + var readPrototypeProperty = type.DefineMethod("ReadPrototypeProperty", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object, _types.Object, _types.String]); + var readSuper = type.DefineMethod("ReadSuper", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object, _types.Object, _types.String, _types.Boolean]); EmitClassCaptureBodies(captureType, captureOwner, captureField, captures, readCapture, writeCapture, findEnvironment); captureType.CreateType(); definitions.Declare(new(type, instanceInterface, instanceInterface.GetMethod(getter.Name)!, constructor, template, factory, prototype, keys, parent, captures, privateMembers, create, construct, read, invoke, - captureType, captureConstructor, captureOwner, captureField, readCapture, writeCapture, findEnvironment)); + captureType, captureConstructor, captureOwner, captureField, readCapture, writeCapture, findEnvironment, + validateParent, initializeReceiver, getParent, readArgument, readPrototypeProperty, readSuper)); } private void EmitClassDefinitionBodies(EmittedRuntime runtime) @@ -254,6 +267,16 @@ void EmitDataDescriptor(LocalBuilder owner, string name, Action value, bool writ il.Emit(OpCodes.Callvirt, storage.DescriptorValue.GetGetMethod()!); il.Emit(OpCodes.Ret); il.MarkLabel(inheritedProperty); + var noParentProperty = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, definitions.Parent); + il.Emit(OpCodes.Brfalse, noParentProperty); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, definitions.Parent); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, runtime.ObjectRead.Property); + il.Emit(OpCodes.Ret); + il.MarkLabel(noParentProperty); var ordinary = il.DefineLabel(); foreach (var name in new[] { "bind", "call", "apply", "name", "length", "prototype" }) { @@ -276,6 +299,7 @@ void EmitDataDescriptor(LocalBuilder owner, string name, Action value, bool writ il = definitions.Invoke.GetILGenerator(); GuestErrorEmitter.ThrowError(il, runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor, "Class constructor cannot be invoked without 'new'"); + EmitRuntimeParentBodies(runtime); definitions.Type.CreateType(); definitions.CompleteEmission(); } diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Operators.cs b/src/SharpTS/Compilation/RuntimeEmitter.Operators.cs index 39e50f7c7..0e07c610a 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Operators.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Operators.cs @@ -28,6 +28,7 @@ Type UndefinedType private readonly record struct InstanceOfInputs( EmittedClassDefinitionRuntime ClassDefinitions, + EmittedClassPrototypeRuntime ClassPrototypes, EmittedAbortRuntime? Abort, EmittedBoxedPrimitiveRuntime BoxedPrimitives, MethodBuilder GetFunctionMethod, @@ -791,6 +792,26 @@ private void EmitInstanceOf(TypeBuilder typeBuilder, EmittedOperatorRuntime oper il.Emit(OpCodes.Castclass, _types.Type); il.Emit(OpCodes.Stloc, classTypeLocal); + // A guest class-definition receiver can inherit a selected user template + // without CLR assignability. Consult its ordinary prototype chain first. + var staticReceiver = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.ClassDefinitions.InstanceInterface); + il.Emit(OpCodes.Brfalse, staticReceiver); + var selectedPrototype = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldloc, classTypeLocal); + il.Emit(OpCodes.Call, inputs.ClassPrototypes.Get); il.Emit(OpCodes.Stloc, selectedPrototype); + var selectedCandidate = il.DeclareLocal(_types.Object); + il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Stloc, selectedCandidate); + var selectedWalk = il.DefineLabel(); + il.MarkLabel(selectedWalk); + il.Emit(OpCodes.Ldloc, selectedCandidate); il.Emit(OpCodes.Call, inputs.ObjectPrototypes.GetPrototypeOf); + il.Emit(OpCodes.Stloc, selectedCandidate); + il.Emit(OpCodes.Ldloc, selectedCandidate); il.Emit(OpCodes.Brfalse, staticReceiver); + il.Emit(OpCodes.Ldloc, selectedCandidate); il.Emit(OpCodes.Ldloc, selectedPrototype); il.Emit(OpCodes.Beq, trueLabel); + il.Emit(OpCodes.Br, selectedWalk); + il.MarkLabel(staticReceiver); + // `x instanceof Object` — the bare `Object` identifier resolves to the // System.Object Type token (see RuntimeEmitter.GlobalThis.cs), so apply // JS Object semantics rather than the IsAssignableFrom fallback below: diff --git a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs index 1045d231c..38080c760 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.RuntimeClass.cs @@ -921,6 +921,7 @@ void EmitLinkProto(FieldBuilder child) runtime.Operators, new InstanceOfInputs( runtime.ClassDefinitions, + runtime.ClassPrototypes, runtime.Abort, runtime.BoxedPrimitives, runtime.FunctionIntrospection.GetProperty, diff --git a/src/SharpTS/Compilation/StatementEmitterBase.cs b/src/SharpTS/Compilation/StatementEmitterBase.cs index c00020c91..2e77f31ba 100644 --- a/src/SharpTS/Compilation/StatementEmitterBase.cs +++ b/src/SharpTS/Compilation/StatementEmitterBase.cs @@ -1826,6 +1826,23 @@ protected virtual void EmitStoreLoopVariable(LocalBuilder? local, string name, A private void EmitStateMachineClassDeclaration(Stmt.Class classStmt) { + if (Ctx.RuntimeClassDeclarations?.TryGetValue(classStmt, out var runtimeExpression) == true) + { + EmitGuestClassDefinition(runtimeExpression); + var definition = IL.DeclareLocal(Types.Object); + IL.Emit(OpCodes.Stloc, definition); + string name = GetClassStorageName(classStmt); + if (GetHoistedVariableField(name) is { } binding) + { + IL.Emit(OpCodes.Ldarg_0); IL.Emit(OpCodes.Ldloc, definition); IL.Emit(OpCodes.Stfld, binding); + } + else + { + var definitionLocal = Ctx.Locals.GetLocal(name) ?? Ctx.Locals.DeclareLocal(name, Types.Object, classStmt); + IL.Emit(OpCodes.Ldloc, definition); IL.Emit(OpCodes.Stloc, definitionLocal); + } + return; + } TypeBuilder? builder = null; if (Ctx.BlockScopedClassBuilders?.TryGetValue(classStmt, out var scopedBuilder) == true) builder = scopedBuilder; diff --git a/src/SharpTS/Execution/Interpreter.Expressions.cs b/src/SharpTS/Execution/Interpreter.Expressions.cs index 4f2816374..a9d751956 100644 --- a/src/SharpTS/Execution/Interpreter.Expressions.cs +++ b/src/SharpTS/Execution/Interpreter.Expressions.cs @@ -2073,7 +2073,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon throw new InterpreterException( $"Class expression cannot extend built-in '{builtInCtor.Name}': subclassing this built-in is not supported yet."); } - throw new InterpreterException("Superclass must be a class."); + throw new ThrowException(new SharpTSTypeError("Superclass must be a class constructor.")); } } diff --git a/src/SharpTS/Execution/Interpreter.Statements.cs b/src/SharpTS/Execution/Interpreter.Statements.cs index 257e8ee84..cbbccaabd 100644 --- a/src/SharpTS/Execution/Interpreter.Statements.cs +++ b/src/SharpTS/Execution/Interpreter.Statements.cs @@ -2030,7 +2030,7 @@ private async ValueTask VisitClassCore(IEvaluationContext ctx, throw new InterpreterException( $"Class '{classStmt.Name.Lexeme}' cannot extend built-in '{builtInCtor.Name}': subclassing this built-in is not supported yet."); } - throw new InterpreterException("Superclass must be a class."); + throw new ThrowException(new SharpTSTypeError("Superclass must be a class constructor.")); } } diff --git a/tests/SharpTS.Tests/SharedTests/RuntimeSuperclassTests.cs b/tests/SharpTS.Tests/SharedTests/RuntimeSuperclassTests.cs new file mode 100644 index 000000000..741f25f72 --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/RuntimeSuperclassTests.cs @@ -0,0 +1,159 @@ +using SharpTS.Tests.CompilerTests; +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class RuntimeSuperclassTests +{ + [Theory] + [InlineData("awaited-parent.ts", "right\n2 right\ntrue\ntrue false\n")] + [InlineData("observed-parent.ts", "right\nfunction right\ntrue\ntrue false\n")] + [InlineData("abrupt-parent.ts", "parent \n")] + public void InvestigationFixturesMatchInBothEnginesAndVerifiedOutput(string name, string expected) + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null && !Directory.Exists(Path.Combine(directory.FullName, "tests", "fixtures", "DynamicSuperclass"))) + directory = directory.Parent; + Assert.NotNull(directory); + var source = File.ReadAllText(Path.Combine(directory.FullName, "tests", "fixtures", "DynamicSuperclass", name)); + Verify(source, expected); + // Hosted modules invoke a reserved main entry point after initialization. + // These scripts already call their runner, so give that runner a neutral name. + HistoricalRuntimeDeploymentTests.AssertDeployment(name, + source.Replace("main(", "runtimeParentProbe(", StringComparison.Ordinal), expected); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void PendingParentSelectionUsesTheOriginalReceiverAndEachDefinition(bool declaration) + { + var body = """ + { + static initialized = (order += "static "); + own = 10; + constructor(value: number) { super(value); this.value += this.own; } + child() { return super.read() + this.own; } + } + """; + var definition = declaration ? $"class Child extends selected {body}; return Child;" + : $"return class extends selected {body};"; + var source = """ + let order = ""; + class Root { + value = 0; + read() { return this.value; } + } + class Left extends Root { + static side = "left"; + receiver: any; + constructor(value: number) { super(); this.value = value; this.receiver = this; } + read() { return super.read() + 1; } + } + class Right extends Root { + static side = "right"; + receiver: any; + constructor(value: number) { super(); this.value = value * 2; this.receiver = this; } + read() { return super.read() + 2; } + } + function select(parent: any) { + order += "select "; + return new Promise(resolve => setTimeout(() => { order += "settle "; resolve(parent); }, 1)); + } + async function make(parent: any) { + const selected = await select(parent); + order += "ready "; + """ + definition + """ + } + async function main() { + const First: any = await make(Left), Second: any = await make(Right); + const first: any = new First(3), second: any = new Second(4); + console.log(first.read(), first.child(), second.read(), second.child()); + console.log(first.receiver === first, second.receiver === second, first.own, second.own); + console.log(First.side, Second.side, Object.getPrototypeOf(First) === Left, Object.getPrototypeOf(Second) === Right); + console.log(Object.getPrototypeOf(First.prototype) === Left.prototype, Object.getPrototypeOf(Second.prototype) === Right.prototype); + console.log(first instanceof Root, first instanceof Left, first instanceof Right, second instanceof Root, second instanceof Left, second instanceof Right); + console.log(order); + } + main().then(() => {}, error => console.log("rejected", error.message)); + """; + Verify(source, "14 24 20 30\ntrue true 10 10\nleft right true true\ntrue true\ntrue true false true false true\nselect settle ready static select settle ready static \n"); + } + + [Theory, ModeData] + public void ImplicitDerivedConstructorForwardsArguments(ExecutionMode mode) + { + var source = """ + class Base { value = 0; constructor(value: number) { this.value = value; } read() { return this.value; } } + function make(parent: any) { return class extends parent { own = 7; }; } + const Child: any = make(Base), instance: any = new Child(9); + console.log(instance.read(), instance.own, instance instanceof Base); + """; + Assert.Equal("9 7 true\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void ParentAdaptersPreserveDefaultsArgumentsAndStaticReceiver(ExecutionMode mode) + { + var source = """ + class Root { static read() { return this.side; } static side = "root"; } + class Parent extends Root { + static side = "parent"; + static read() { return this.side; } + value = 0; + count = 0; + total = 0; + constructor(value: number = 7, ...rest: number[]) { + super(); this.value = value; this.count = arguments.length; this.total = rest.length; + } + read(...values: number[]) { console.log(arguments.length, values[0]); return this.value; } + } + function make(parent: any) { return class extends parent { static side = "child"; }; } + const Child: any = make(Parent), first: any = new Child(), second: any = new Child(9, 10, 11); + console.log(first.read(3), first.count, first.total, second.read(4), second.count, second.total); + console.log(Child.read(), Parent.read(), Root.read()); + """; + Assert.Equal("1 3\n1 4\n7 0 0 9 3 2\nchild parent root\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void SelectedExpressionParentAndSameNameParameterUseRuntimeIdentity(ExecutionMode mode) + { + var source = """ + class Parent { value = 1; read() { return this.value; } } + const Selected = class { value = 2; read() { return this.value; } }; + function make(Parent: any) { return class extends Parent { own = 3; }; } + const Child: any = make(Selected), instance: any = new Child(); + console.log(instance.read(), instance.own, instance instanceof Selected, instance instanceof Parent); + console.log(Object.getPrototypeOf(Child) === Selected, Object.getPrototypeOf(Child.prototype) === Selected.prototype); + """; + Assert.Equal("2 3 true false\ntrue true\n", TestHarness.Run(source, mode)); + } + + [Theory, ModeData] + public void InvalidParentAndRejectedAwaitPrecedeStaticInitialization(ExecutionMode mode) + { + var source = """ + let initialized = ""; + const reason = new Error("original"); + function make(parent: any) { return class extends parent { static value = (initialized += "bad"); }; } + try { make(7); } catch (error: any) { console.log(error instanceof TypeError, initialized); } + async function run() { + try { class Child extends (await Promise.reject(reason)) { static value = (initialized += "bad"); } } + catch (error: any) { console.log(error === reason, error.message, initialized); } + } + run(); + """; + Assert.Equal("true \ntrue original \n", TestHarness.Run(source, mode)); + } + + private static void Verify(string source, string expected) + { + Assert.Equal(expected, TestHarness.RunInterpreted(source, TimeSpan.FromSeconds(30))); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + } +} From ebceda3d921c6f60145c78c404de4c775623ba5e Mon Sep 17 00:00:00 2001 From: nickna Date: Tue, 6 Oct 2026 03:53:31 -0700 Subject: [PATCH 08/16] Restore lexical captures in async generator class methods (#1965) --- ...AsyncGeneratorMoveNextEmitter.Variables.cs | 3 + .../Compilation/ILCompiler.ArrowFunctions.cs | 3 + ...RepeatedClassAsyncGeneratorCaptureTests.cs | 78 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 tests/SharpTS.Tests/SharedTests/RepeatedClassAsyncGeneratorCaptureTests.cs diff --git a/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs b/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs index 9cef3ebee..a26b7358b 100644 --- a/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs +++ b/src/SharpTS/Compilation/AsyncGeneratorMoveNextEmitter.Variables.cs @@ -14,4 +14,7 @@ public partial class AsyncGeneratorMoveNextEmitter // The block-scope-rename + function-DC variable overrides that consume this live in the shared // IteratorMoveNextEmitter base (#1124). protected override IReadOnlyDictionary BlockScopeRenames => _analysis.BlockScopeRenames ?? NoRenames; + + protected override string ResolveCaptureSourceName(Expr.ArrowFunction af, string capturedVar) => + PivotCaptureSource(_analysis.BlockScopeCaptureRenames, af, capturedVar); } diff --git a/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs b/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs index 6e6533230..7567b7bc7 100644 --- a/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs +++ b/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs @@ -541,6 +541,9 @@ private void CollectArrowsFromStmt(Stmt stmt, bool isClassMember = false) _currentEnclosingFunctionName = GetDefinitionContext().GetQualifiedFunctionName(f.Name.Lexeme); _currentEnclosingFunctionStmt = f; } + // Generator methods own invocation storage even inside a class factory. + if (isClassMember && f.IsGenerator) + _currentEnclosingFunctionStmt = f; _currentEnclosingCallable = f; _currentCollectStrict = previousStrict || _currentCollectClassName != null diff --git a/tests/SharpTS.Tests/SharedTests/RepeatedClassAsyncGeneratorCaptureTests.cs b/tests/SharpTS.Tests/SharedTests/RepeatedClassAsyncGeneratorCaptureTests.cs new file mode 100644 index 000000000..c8090e577 --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/RepeatedClassAsyncGeneratorCaptureTests.cs @@ -0,0 +1,78 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class RepeatedClassAsyncGeneratorCaptureTests +{ + public static IEnumerable Cases() + { + yield return ["instance-arrow-before-await", """ + function make(seed: number) { + return class { + async *values(): AsyncGenerator { + const value = 100; + { + const value = seed; + const read = () => value; + await new Promise(resolve => setTimeout(() => resolve(0), 1)); + yield read; + } + yield value; + yield () => seed; + } + }; + } + async function run() { + const First = make(7), Second = make(9); + const first = new First().values(), second = new Second().values(); + const one = await first.next(), two = await second.next(); + console.log(one.value(), two.value()); + console.log((await first.next()).value, (await second.next()).value); + const outerOne = await first.next(), outerTwo = await second.next(); + console.log(outerOne.value(), outerTwo.value(), one.value(), two.value()); + } + run(); + """, "7 9\n100 100\n7 9 7 9\n"]; + yield return ["static-arrow-after-await", """ + function make(seed: number) { + return class { + static async *values(): AsyncGenerator { + const value = 100; + { + const value = seed; + await new Promise(resolve => setTimeout(() => resolve(0), 1)); + yield () => value; + } + yield value; + yield () => seed; + yield this; + } + }; + } + async function run() { + const First = make(7), Second = make(9); + const first = First.values(), second = Second.values(); + const one = await first.next(), two = await second.next(); + console.log(one.value(), two.value()); + console.log((await first.next()).value, (await second.next()).value); + const outerOne = await first.next(), outerTwo = await second.next(); + console.log(outerOne.value(), outerTwo.value(), one.value(), two.value()); + console.log((await first.next()).value === First, (await second.next()).value === Second); + } + run(); + """, "7 9\n100 100\n7 9 7 9\ntrue true\n"]; + } + + [Theory, MemberData(nameof(Cases))] + public void DefinitionAndShadowCapturesSurviveSuspension(string name, string source, string expected) + { + Assert.NotEmpty(name); + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + Assert.Equal(expected, TestHarness.RunCompiledStandalone(source)); + } +} From 8fba710f0a727a043764baae7cd75be7169e13d3 Mon Sep 17 00:00:00 2001 From: nickna Date: Tue, 6 Oct 2026 05:45:12 -0700 Subject: [PATCH 09/16] fix: preserve checked constructor identity in prototype queries (#1966) --- src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs index 738c0f436..84ac07081 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs @@ -2176,7 +2176,8 @@ protected string ResolveInstanceClassName(Expr receiver, string simpleClassName) { var (parts, name) = ExtractQualifiedNameFromCallee(construction.Callee); if (name.Length > 0) - return ResolveClassNameForNew(parts, name); + return Ctx.ResolveClassName(Ctx.TypeMap?.Get(construction.Callee), + ResolveClassNameForNew(parts, name)); } return Ctx.ResolveClassName(Ctx.TypeMap?.Get(receiver), simpleClassName); } From 33af88e802010dc46183c8a37444b379d2aac3e5 Mon Sep 17 00:00:00 2001 From: nickna Date: Tue, 6 Oct 2026 16:09:51 -0700 Subject: [PATCH 10/16] Justify shared async capture emitter overrides (#1965) --- tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs b/tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs index 11e2d343a..3c66bbd30 100644 --- a/tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs +++ b/tests/SharpTS.Tests/Compilation/EmitterSyncTests.cs @@ -91,6 +91,7 @@ public class EmitterSyncTests "get_Resolver", // Abstract: provides IVariableResolver "GetThisField", // Abstract: provides hoisted 'this' field "GetHoistedVariableField", // Hoisted variable fields on state machine + "GetFunctionDCField", // #1965: shared arrow emission loads captured-and-mutated locals from the async state machine's function display "ResolveCaptureSourceName", // #1517: typed Promise delegates honor block-shadow capture pivots "SharpTS.Compilation.Emitters.IEmitterContext.get_IL", // IEmitterContext impl "SharpTS.Compilation.Emitters.IEmitterContext.SetStackUnknown", // IEmitterContext impl @@ -194,6 +195,7 @@ public class EmitterSyncTests "get_Resolver", "GetThisField", "GetHoistedVariableField", + "ResolveCaptureSourceName", // #1965: source captured block shadows from their renamed async-generator storage // --- Genuinely different behavior --- // (break/continue/loop-nav/EmitBranchToLabel are inherited from StateMachineExitRoutingEmitter.) "EmitReturn", // Async generator return: store in CurrentField + state -2 From afcff3f8f218e29b93fac709822c850bc31a3ab0 Mon Sep 17 00:00:00 2001 From: nickna Date: Tue, 6 Oct 2026 16:13:52 -0700 Subject: [PATCH 11/16] Compare emitted module ownership by module identity (#1964) --- src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs index e42cb99eb..a750e4bad 100644 --- a/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs +++ b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs @@ -57,7 +57,10 @@ internal void Declare(Declarations declarations) declarations.GetParent, declarations.ReadArgument, declarations.ReadPrototypeProperty, declarations.ReadSuper } .Any(member => member.DeclaringType != declarations.Type) || declarations.GetDefinition.DeclaringType != declarations.InstanceInterface - || declarations.InstanceInterface.Module != declarations.Type.Module + // A completed interface exposes RuntimeModule, while its in-memory + // owner still exposes RuntimeModuleBuilder. Their MVID is the same + // module identity even though the reflection wrappers differ. + || declarations.InstanceInterface.Module.ModuleVersionId != declarations.Type.Module.ModuleVersionId || declarations.CaptureType.Module != declarations.Type.Module || new MemberInfo[] { declarations.CaptureConstructor, declarations.CaptureOwner, declarations.CaptureField } .Any(member => member.DeclaringType != declarations.CaptureType)) From 4f11137b7f3e240989b52b462b5f58da5456ecd2 Mon Sep 17 00:00:00 2001 From: nickna Date: Tue, 6 Oct 2026 16:14:06 -0700 Subject: [PATCH 12/16] Preserve nullish internal symbol probes (#1965) --- src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs index 6de6d3af9..24602ef4b 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.Objects.Index.cs @@ -136,6 +136,11 @@ private void EmitGetIndex(TypeBuilder typeBuilder, EmittedObjectReadRuntime obje // null check on obj il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Brfalse, nullLabel); + // Internal protocol probes also pass the emitted undefined sentinel. + // It has no symbol properties or prototype to traverse. + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Isinst, inputs.UndefinedType); + il.Emit(OpCodes.Brtrue, nullLabel); // Proxy check: uses obj.GetType().FullName comparison (no SharpTS.dll dependency) var notProxyLabel = il.DefineLabel(); From 78612a5dc4fa830d9313fd768f22ab66120b9d46 Mon Sep 17 00:00:00 2001 From: nickna Date: Tue, 6 Oct 2026 16:14:26 -0700 Subject: [PATCH 13/16] Omit unused user-class construction helpers (#1967) --- .../EmittedClassDefinitionRuntime.cs | 49 +++++---- .../RuntimeEmitter.ClassDefinition.cs | 102 +++++++++++------- .../Compilation/RuntimeFeatureDetector.cs | 3 + src/SharpTS/Compilation/RuntimeFeatureSet.cs | 1 + .../ClassRuntimeFeatureGatingTests.cs | 28 +++++ 5 files changed, 121 insertions(+), 62 deletions(-) create mode 100644 tests/SharpTS.Tests/CompilerTests/ClassRuntimeFeatureGatingTests.cs diff --git a/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs index a750e4bad..b7fcdad19 100644 --- a/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs +++ b/src/SharpTS/Compilation/EmittedClassDefinitionRuntime.cs @@ -9,11 +9,11 @@ public sealed class EmittedClassDefinitionRuntime internal EmittedClassDefinitionRuntime() { } internal sealed record Declarations(TypeBuilder Type, Type InstanceInterface, MethodInfo GetDefinition, ConstructorBuilder Constructor, FieldBuilder Template, - FieldBuilder Factory, FieldBuilder Prototype, FieldBuilder Keys, FieldBuilder Parent, FieldBuilder Captures, FieldBuilder PrivateMembers, MethodBuilder Create, MethodBuilder Construct, - MethodBuilder ReadProperty, MethodBuilder Invoke, TypeBuilder CaptureType, ConstructorBuilder CaptureConstructor, - FieldBuilder CaptureOwner, FieldBuilder CaptureField, MethodBuilder ReadCapture, MethodBuilder WriteCapture, - MethodBuilder FindEnvironment, MethodBuilder ValidateParent, MethodBuilder InitializeReceiver, - MethodBuilder GetParent, MethodBuilder ReadArgument, MethodBuilder ReadPrototypeProperty, MethodBuilder ReadSuper); + FieldBuilder Factory, FieldBuilder Prototype, FieldBuilder Keys, FieldBuilder Parent, FieldBuilder Captures, FieldBuilder PrivateMembers, MethodBuilder? Create, MethodBuilder Construct, + MethodBuilder ReadProperty, MethodBuilder Invoke, TypeBuilder? CaptureType, ConstructorBuilder? CaptureConstructor, + FieldBuilder? CaptureOwner, FieldBuilder? CaptureField, MethodBuilder? ReadCapture, MethodBuilder? WriteCapture, + MethodBuilder? FindEnvironment, MethodBuilder? ValidateParent, MethodBuilder? InitializeReceiver, + MethodBuilder? GetParent, MethodBuilder? ReadArgument, MethodBuilder? ReadPrototypeProperty, MethodBuilder? ReadSuper); private Declarations? _declarations; private Declarations Required => _declarations @@ -30,40 +30,45 @@ internal sealed record Declarations(TypeBuilder Type, Type InstanceInterface, public FieldBuilder Parent => Required.Parent; public FieldBuilder Captures => Required.Captures; public FieldBuilder PrivateMembers => Required.PrivateMembers; - public MethodBuilder Create => Required.Create; + public MethodBuilder Create => RequireRuntimeParentHelper(Required.Create); public MethodBuilder Construct => Required.Construct; public MethodBuilder ReadProperty => Required.ReadProperty; public MethodBuilder Invoke => Required.Invoke; - public ConstructorBuilder CaptureConstructor => Required.CaptureConstructor; - public MethodBuilder ReadCapture => Required.ReadCapture; - public MethodBuilder WriteCapture => Required.WriteCapture; - public MethodBuilder FindEnvironment => Required.FindEnvironment; - public MethodBuilder ValidateParent => Required.ValidateParent; - public MethodBuilder InitializeReceiver => Required.InitializeReceiver; - public MethodBuilder GetParent => Required.GetParent; - public MethodBuilder ReadArgument => Required.ReadArgument; - public MethodBuilder ReadPrototypeProperty => Required.ReadPrototypeProperty; - public MethodBuilder ReadSuper => Required.ReadSuper; + public ConstructorBuilder CaptureConstructor => Required.CaptureConstructor + ?? throw new InvalidOperationException("User-class runtime helpers were not selected."); + public MethodBuilder ReadCapture => RequireRuntimeParentHelper(Required.ReadCapture); + public MethodBuilder WriteCapture => RequireRuntimeParentHelper(Required.WriteCapture); + public MethodBuilder FindEnvironment => RequireRuntimeParentHelper(Required.FindEnvironment); + internal bool HasRuntimeParentHelpers => Required.ValidateParent is not null; + public MethodBuilder ValidateParent => RequireRuntimeParentHelper(Required.ValidateParent); + public MethodBuilder InitializeReceiver => RequireRuntimeParentHelper(Required.InitializeReceiver); + public MethodBuilder GetParent => RequireRuntimeParentHelper(Required.GetParent); + public MethodBuilder ReadArgument => RequireRuntimeParentHelper(Required.ReadArgument); + public MethodBuilder ReadPrototypeProperty => RequireRuntimeParentHelper(Required.ReadPrototypeProperty); + public MethodBuilder ReadSuper => RequireRuntimeParentHelper(Required.ReadSuper); + + private static MethodBuilder RequireRuntimeParentHelper(MethodBuilder? method) => method + ?? throw new InvalidOperationException("User-class runtime helpers were not selected."); internal void Declare(Declarations declarations) { ArgumentNullException.ThrowIfNull(declarations); if (_declarations is not null || IsComplete) throw new InvalidOperationException("Class-definition metadata has already been declared."); - if (new MemberInfo[] { declarations.Constructor, declarations.Template, declarations.Factory, + if (new MemberInfo?[] { declarations.Constructor, declarations.Template, declarations.Factory, declarations.Prototype, declarations.Keys, declarations.Parent, declarations.Captures, declarations.PrivateMembers, declarations.Create, declarations.Construct, declarations.ReadProperty, declarations.Invoke, declarations.ReadCapture, declarations.WriteCapture, declarations.FindEnvironment, declarations.ValidateParent, declarations.InitializeReceiver, declarations.GetParent, declarations.ReadArgument, declarations.ReadPrototypeProperty, declarations.ReadSuper } - .Any(member => member.DeclaringType != declarations.Type) + .Any(member => member is not null && member.DeclaringType != declarations.Type) || declarations.GetDefinition.DeclaringType != declarations.InstanceInterface // A completed interface exposes RuntimeModule, while its in-memory // owner still exposes RuntimeModuleBuilder. Their MVID is the same // module identity even though the reflection wrappers differ. || declarations.InstanceInterface.Module.ModuleVersionId != declarations.Type.Module.ModuleVersionId - || declarations.CaptureType.Module != declarations.Type.Module - || new MemberInfo[] { declarations.CaptureConstructor, declarations.CaptureOwner, declarations.CaptureField } - .Any(member => member.DeclaringType != declarations.CaptureType)) + || (declarations.CaptureType is not null && declarations.CaptureType.Module != declarations.Type.Module) + || new MemberInfo?[] { declarations.CaptureConstructor, declarations.CaptureOwner, declarations.CaptureField } + .Any(member => member is not null && member.DeclaringType != declarations.CaptureType)) throw new InvalidOperationException("Class-definition handles must belong to their declared owner."); _declarations = declarations; } @@ -76,7 +81,7 @@ internal void CompleteEmission() declarations.ReadCapture, declarations.WriteCapture, declarations.FindEnvironment, declarations.ValidateParent, declarations.InitializeReceiver, declarations.GetParent, declarations.ReadArgument, declarations.ReadPrototypeProperty, declarations.ReadSuper } - .Any(method => method.GetILGenerator().ILOffset == 0)) + .Any(method => method is not null && method.GetILGenerator().ILOffset == 0)) throw new InvalidOperationException("Class-definition method bodies have not all been emitted."); IsComplete = true; } diff --git a/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs index 3937ef84b..76973f494 100644 --- a/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs +++ b/src/SharpTS/Compilation/RuntimeEmitter.ClassDefinition.cs @@ -27,55 +27,69 @@ private void DefineClassDefinitionTypes(ModuleBuilder module, EmittedClassDefini var parent = type.DefineField("Parent", _types.Object, FieldAttributes.Assembly); var captures = type.DefineField("Captures", _types.ObjectArray, FieldAttributes.Assembly); var privateMembers = type.DefineField("PrivateMembers", _types.DictionaryStringObject, FieldAttributes.Assembly); - var create = type.DefineMethod("Create", MethodAttributes.Public | MethodAttributes.Static, - type, [_types.Type, factoryType, _types.String, _types.Double, _types.Object, _types.ObjectArray, _types.ObjectArray]); + var create = _features.UsesUserClasses + ? type.DefineMethod("Create", MethodAttributes.Public | MethodAttributes.Static, + type, [_types.Type, factoryType, _types.String, _types.Double, _types.Object, _types.ObjectArray, _types.ObjectArray]) : null; var construct = type.DefineMethod("Construct", MethodAttributes.Public | MethodAttributes.Static, _types.Object, [type, _types.ObjectArray]); var read = type.DefineMethod("ReadProperty", MethodAttributes.Public | MethodAttributes.Static, _types.Object, [type, _types.String]); var invoke = type.DefineMethod("Invoke", MethodAttributes.Public, _types.Object, [_types.ObjectArray]); - var captureType = module.DefineType("$ClassCapture", TypeAttributes.Public | TypeAttributes.Sealed); - var captureOwner = captureType.DefineField("Owner", _types.Object, FieldAttributes.Assembly); - var captureField = captureType.DefineField("Field", _types.FieldInfo, FieldAttributes.Assembly); - var captureConstructor = captureType.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, - [_types.Object, _types.FieldInfo]); - il = captureConstructor.GetILGenerator(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, _types.GetDefaultConstructor(_types.Object)); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Stfld, captureOwner); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Stfld, captureField); - il.Emit(OpCodes.Ret); - var readCapture = type.DefineMethod("ReadCapture", MethodAttributes.Public | MethodAttributes.Static, - _types.Object, [_types.Object]); - var writeCapture = type.DefineMethod("WriteCapture", MethodAttributes.Public | MethodAttributes.Static, - _types.Void, [type, _types.Int32, _types.Object]); - var findEnvironment = type.DefineMethod("FindEnvironment", MethodAttributes.Public | MethodAttributes.Static, - _types.Object, [type, _types.Type]); - var validateParent = type.DefineMethod("ValidateParent", MethodAttributes.Public | MethodAttributes.Static, - _types.Object, [_types.Object]); - var initializeReceiver = type.DefineMethod("InitializeReceiver", MethodAttributes.Public | MethodAttributes.Static, - _types.Void, [_types.Object, _types.Object, _types.ObjectArray]); - var getParent = type.DefineMethod("GetParent", MethodAttributes.Public | MethodAttributes.Static, - _types.Object, [_types.Object]); - var readArgument = type.DefineMethod("ReadArgument", MethodAttributes.Public | MethodAttributes.Static, - _types.Object, [_types.ObjectArray, _types.Int32]); - var readPrototypeProperty = type.DefineMethod("ReadPrototypeProperty", MethodAttributes.Public | MethodAttributes.Static, - _types.Object, [_types.Object, _types.Object, _types.String]); - var readSuper = type.DefineMethod("ReadSuper", MethodAttributes.Public | MethodAttributes.Static, - _types.Object, [_types.Object, _types.Object, _types.String, _types.Boolean]); - EmitClassCaptureBodies(captureType, captureOwner, captureField, captures, readCapture, writeCapture, findEnvironment); - captureType.CreateType(); + TypeBuilder? captureType = null; + FieldBuilder? captureOwner = null, captureField = null; + ConstructorBuilder? captureConstructor = null; + MethodBuilder? readCapture = null, writeCapture = null, findEnvironment = null; + if (_features.UsesUserClasses) + { + captureType = module.DefineType("$ClassCapture", TypeAttributes.Public | TypeAttributes.Sealed); + captureOwner = captureType.DefineField("Owner", _types.Object, FieldAttributes.Assembly); + captureField = captureType.DefineField("Field", _types.FieldInfo, FieldAttributes.Assembly); + captureConstructor = captureType.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, + [_types.Object, _types.FieldInfo]); + il = captureConstructor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, _types.GetDefaultConstructor(_types.Object)); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Stfld, captureOwner); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Stfld, captureField); + il.Emit(OpCodes.Ret); + readCapture = type.DefineMethod("ReadCapture", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object]); + writeCapture = type.DefineMethod("WriteCapture", MethodAttributes.Public | MethodAttributes.Static, + _types.Void, [type, _types.Int32, _types.Object]); + findEnvironment = type.DefineMethod("FindEnvironment", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [type, _types.Type]); + EmitClassCaptureBodies(captureType, captureOwner, captureField, captures, readCapture, writeCapture, findEnvironment); + captureType.CreateType(); + } + var validateParent = _features.UsesUserClasses + ? type.DefineMethod("ValidateParent", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object]) : null; + var initializeReceiver = _features.UsesUserClasses + ? type.DefineMethod("InitializeReceiver", MethodAttributes.Public | MethodAttributes.Static, + _types.Void, [_types.Object, _types.Object, _types.ObjectArray]) : null; + var getParent = _features.UsesUserClasses + ? type.DefineMethod("GetParent", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object]) : null; + var readArgument = _features.UsesUserClasses + ? type.DefineMethod("ReadArgument", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.ObjectArray, _types.Int32]) : null; + var readPrototypeProperty = _features.UsesUserClasses + ? type.DefineMethod("ReadPrototypeProperty", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object, _types.Object, _types.String]) : null; + var readSuper = _features.UsesUserClasses + ? type.DefineMethod("ReadSuper", MethodAttributes.Public | MethodAttributes.Static, + _types.Object, [_types.Object, _types.Object, _types.String, _types.Boolean]) : null; definitions.Declare(new(type, instanceInterface, instanceInterface.GetMethod(getter.Name)!, constructor, template, factory, prototype, keys, parent, captures, privateMembers, create, construct, read, invoke, captureType, captureConstructor, captureOwner, captureField, readCapture, writeCapture, findEnvironment, validateParent, initializeReceiver, getParent, readArgument, readPrototypeProperty, readSuper)); } - private void EmitClassDefinitionBodies(EmittedRuntime runtime) + private void EmitClassDefinitionCreation(EmittedRuntime runtime) { var definitions = runtime.ClassDefinitions; var storage = runtime.DescriptorStorage; @@ -233,8 +247,15 @@ void EmitDataDescriptor(LocalBuilder owner, string name, Action value, bool writ il.Emit(OpCodes.Call, storage.DefineProperty); il.Emit(OpCodes.Pop); } + } - il = definitions.Construct.GetILGenerator(); + private void EmitClassDefinitionBodies(EmittedRuntime runtime) + { + var definitions = runtime.ClassDefinitions; + var storage = runtime.DescriptorStorage; + if (definitions.HasRuntimeParentHelpers) + EmitClassDefinitionCreation(runtime); + var il = definitions.Construct.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, definitions.Factory); il.Emit(OpCodes.Ldarg_1); @@ -299,7 +320,8 @@ void EmitDataDescriptor(LocalBuilder owner, string name, Action value, bool writ il = definitions.Invoke.GetILGenerator(); GuestErrorEmitter.ThrowError(il, runtime.Errors.CreateException, runtime.Errors.TypeErrorConstructor, "Class constructor cannot be invoked without 'new'"); - EmitRuntimeParentBodies(runtime); + if (definitions.HasRuntimeParentHelpers) + EmitRuntimeParentBodies(runtime); definitions.Type.CreateType(); definitions.CompleteEmission(); } diff --git a/src/SharpTS/Compilation/RuntimeFeatureDetector.cs b/src/SharpTS/Compilation/RuntimeFeatureDetector.cs index 9095b8b8b..f50b060d0 100644 --- a/src/SharpTS/Compilation/RuntimeFeatureDetector.cs +++ b/src/SharpTS/Compilation/RuntimeFeatureDetector.cs @@ -91,6 +91,7 @@ public RuntimeFeatureDetector() UsesReflect = false, UsesIteratorHelpers = false, UsesPromise = false, + UsesUserClasses = false, UsesDate = false, UsesRegExp = false, UsesBuffer = false, @@ -766,6 +767,7 @@ private void VisitStmt(Stmt? stmt) break; case Stmt.Class cls: + _set.UsesUserClasses = true; foreach (var expression in ClassDefinitionExpressions.Enumerate(cls)) VisitExpr(expression); foreach (var m in cls.Methods) @@ -1697,6 +1699,7 @@ prop.Key is not Expr.ComputedKey && VisitExpr(nn.Expression); break; case Expr.ClassExpr ce: + _set.UsesUserClasses = true; foreach (var expression in ClassDefinitionExpressions.Enumerate(ce)) VisitExpr(expression); foreach (var m in ce.Methods) diff --git a/src/SharpTS/Compilation/RuntimeFeatureSet.cs b/src/SharpTS/Compilation/RuntimeFeatureSet.cs index 1b0e0bd5e..8fbc7d4e9 100644 --- a/src/SharpTS/Compilation/RuntimeFeatureSet.cs +++ b/src/SharpTS/Compilation/RuntimeFeatureSet.cs @@ -126,6 +126,7 @@ internal bool CanAssumeCompactObjectRecordIsUnmaterialized(string fingerprint) = public bool UsesReflect { get; set; } = true; // Reflect.set/get/deleteProperty/has/etc. public bool UsesIteratorHelpers { get; set; } = true; // Iterator.prototype.map/filter/flatMap/take/drop public bool UsesPromise { get; set; } = true; // Promise references, async/await, and Promise-returning host/module surfaces + public bool UsesUserClasses { get; set; } = true; // user-class receiver initialization and guest prototype lookup public bool UsesDate { get; set; } = true; // new Date(), Date.now(), Date.X public bool UsesRegExp { get; set; } = true; // /pattern/ or new RegExp() public bool UsesBuffer { get; set; } = true; // Buffer.from(), new Buffer() — also implied by crypto/fs/zlib/http/fetch/dgram diff --git a/tests/SharpTS.Tests/CompilerTests/ClassRuntimeFeatureGatingTests.cs b/tests/SharpTS.Tests/CompilerTests/ClassRuntimeFeatureGatingTests.cs new file mode 100644 index 000000000..38ed4e192 --- /dev/null +++ b/tests/SharpTS.Tests/CompilerTests/ClassRuntimeFeatureGatingTests.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using SharpTS.Compilation; +using SharpTS.Parsing; +using SharpTS.TypeSystem; +using Xunit; + +namespace SharpTS.Tests.CompilerTests; + +public sealed class ClassRuntimeFeatureGatingTests +{ + [Theory] + [InlineData("console.log(1);", false)] + [InlineData("class Item {}", true)] + [InlineData("const Item = class {};", true)] + public void ReceiverAndPrototypeHelpersFollowUserClassSelection(string source, bool selected) + { + var statements = new Parser(new Lexer(source).ScanTokens()).ParseOrThrow(); + var typeMap = new TypeChecker().Check(statements); + Assert.Equal(selected, new RuntimeFeatureDetector().Detect(statements, typeMap).UsesUserClasses); + var compiler = new ILCompiler($"class_gate_{Guid.NewGuid():N}"); + compiler.Compile(statements, typeMap, new DeadCodeAnalyzer(typeMap).Analyze(statements)); + var assembly = Assembly.Load(compiler.SaveToBytes()); + var definition = assembly.GetType("$ClassDefinition", throwOnError: true)!; + Assert.Equal(selected, assembly.GetType("$ClassCapture") is not null); + foreach (string name in new[] { "Create", "ReadCapture", "WriteCapture", "FindEnvironment", "ValidateParent", "InitializeReceiver", "GetParent", "ReadArgument", "ReadPrototypeProperty", "ReadSuper" }) + Assert.Equal(selected, definition.GetMethod(name) is not null); + } +} From ab2adafd1c08dc5f22a1fad00590f4c61db812f5 Mon Sep 17 00:00:00 2001 From: nickna Date: Tue, 6 Oct 2026 16:14:40 -0700 Subject: [PATCH 14/16] Read imported EventEmitter statics through their live owner (#1964) --- .../ILEmitter.EventEmitterFacade.cs | 48 +++++++++++++++++++ .../Compilation/ILEmitter.Properties.cs | 1 + .../EventsClassStaticIdentityTests.cs | 24 ++++++++++ 3 files changed, 73 insertions(+) create mode 100644 src/SharpTS/Compilation/ILEmitter.EventEmitterFacade.cs create mode 100644 tests/SharpTS.Tests/SharedTests/EventsClassStaticIdentityTests.cs diff --git a/src/SharpTS/Compilation/ILEmitter.EventEmitterFacade.cs b/src/SharpTS/Compilation/ILEmitter.EventEmitterFacade.cs new file mode 100644 index 000000000..60f62c2c7 --- /dev/null +++ b/src/SharpTS/Compilation/ILEmitter.EventEmitterFacade.cs @@ -0,0 +1,48 @@ +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public partial class ILEmitter +{ + // The embedded events module owns the constructor statics used by its + // helpers. A named import also has a built-in constructor binding for + // instance construction, so that binding must not redirect these statics + // to the separate internal $EventEmitter type. + private bool TryGetEventEmitterFacade(Expr receiver, string property, + out string className, out TypeBuilder builder) + { + className = string.Empty; + builder = null!; + if (property != "defaultMaxListeners" || receiver is not Expr.Variable variable) + return false; + if (_ctx.ImportedClassAliases?.TryGetValue(variable.Name.Lexeme, out var importedClassName) == true) + { + className = importedClassName; + return _ctx.Classes.TryGetValue(className, out builder!); + } + bool imported = _ctx.CurrentModulePath is { } module + && _ctx.ModuleImportFields?.TryGetValue(module, out var imports) == true + && imports.ContainsKey(variable.Name.Lexeme); + bool builtIn = _ctx.BuiltInModuleMethodBindings?.TryGetValue(variable.Name.Lexeme, out var binding) == true + && binding.ModuleName == "events" && binding.MethodName == "EventEmitter"; + if (!imported && !builtIn) return false; + className = _ctx.ResolveClassName(_ctx.TypeMap?.Get(variable), variable.Name.Lexeme); + return _ctx.Classes.TryGetValue(className, out builder!); + } + + private bool TryEmitEventEmitterFacadeGet(Expr.Get get) + { + if (!TryGetEventEmitterFacade(get.Object, get.Name.Lexeme, out var className, out var builder) + || !_ctx.ClassRegistry!.TryGetCallableStaticField(className, get.Name.Lexeme, builder, out var field)) + return false; + // Module helper writes use the constructor value's descriptor store. + // Read that same live value instead of its CLR template field. + EmitExpression(get.Object); + EmitBoxIfNeeded(get.Object); + IL.Emit(OpCodes.Ldstr, get.Name.Lexeme); + IL.Emit(OpCodes.Call, _ctx.Runtime!.ObjectRead.Property); + SetStackUnknown(); + return true; + } +} diff --git a/src/SharpTS/Compilation/ILEmitter.Properties.cs b/src/SharpTS/Compilation/ILEmitter.Properties.cs index 837e6983a..ed40ea6b8 100644 --- a/src/SharpTS/Compilation/ILEmitter.Properties.cs +++ b/src/SharpTS/Compilation/ILEmitter.Properties.cs @@ -19,6 +19,7 @@ public partial class ILEmitter protected override void EmitGet(Expr.Get g) { if (TryEmitGuestThisGet(g)) return; + if (TryEmitEventEmitterFacadeGet(g)) return; if (TryEmitStableRecordDestructureGet(g)) return; diff --git a/tests/SharpTS.Tests/SharedTests/EventsClassStaticIdentityTests.cs b/tests/SharpTS.Tests/SharedTests/EventsClassStaticIdentityTests.cs new file mode 100644 index 000000000..b6274c266 --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/EventsClassStaticIdentityTests.cs @@ -0,0 +1,24 @@ +using SharpTS.Tests.Infrastructure; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class EventsClassStaticIdentityTests +{ + [Theory, ModeData] + public void ImportedConstructorAndHelpersShareDefaultListenerState(ExecutionMode mode) + { + var files = new Dictionary + { + ["main.ts"] = """ + import { EventEmitter as Emitter, setMaxListeners, getMaxListeners } from 'events'; + setMaxListeners(7); + console.log(Emitter.defaultMaxListeners, getMaxListeners(null)); + Emitter.defaultMaxListeners = 12; + console.log(Emitter.defaultMaxListeners, getMaxListeners(null)); + setMaxListeners(10); + """ + }; + Assert.Equal("7 7\n12 12\n", TestHarness.RunModules(files, "main.ts", mode)); + } +} From 1e8bd980d1889d6f52ced6a962d7764395e0c809 Mon Sep 17 00:00:00 2001 From: nickna Date: Tue, 6 Oct 2026 16:21:10 -0700 Subject: [PATCH 15/16] Verify class runtime reuse across feature selections (#1967) --- .../EmittedClassDefinitionRuntimeTests.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/SharpTS.Tests/CompilerTests/EmittedClassDefinitionRuntimeTests.cs b/tests/SharpTS.Tests/CompilerTests/EmittedClassDefinitionRuntimeTests.cs index 061149014..68fa6c098 100644 --- a/tests/SharpTS.Tests/CompilerTests/EmittedClassDefinitionRuntimeTests.cs +++ b/tests/SharpTS.Tests/CompilerTests/EmittedClassDefinitionRuntimeTests.cs @@ -26,7 +26,7 @@ public void ReusedEmitterFinalizesFreshDefinitionOwnersAndVerifiableBodies(bool { var emitter = new RuntimeEmitter(TypeProvider.Runtime, emitHosted: hosted); var owners = new HashSet(); - foreach (string source in new[] { "const n = 1;", "async function f() { await Promise.resolve(1); } f();" }) + foreach (string source in new[] { "const n = 1;", "class Item {}", "async function f() { await Promise.resolve(1); } f();", "const Item = class {};" }) { var assembly = new PersistedAssemblyBuilder(new AssemblyName($"definitions_{Guid.NewGuid():N}"), typeof(object).Assembly); var module = assembly.DefineDynamicModule("main"); @@ -35,8 +35,15 @@ public void ReusedEmitterFinalizesFreshDefinitionOwnersAndVerifiableBodies(bool var owner = runtime.ClassDefinitions; Assert.True(owner.IsComplete); Assert.True(owners.Add(owner)); - Assert.Same(module, owner.Create.Module); - Assert.Same(owner.Type, owner.Create.DeclaringType); + Assert.Same(module, owner.Construct.Module); + Assert.Same(owner.Type, owner.Construct.DeclaringType); + Assert.Equal(features.UsesUserClasses, owner.HasRuntimeParentHelpers); + if (features.UsesUserClasses) + { + Assert.Same(module, owner.Create.Module); + Assert.Same(owner.Type, owner.Create.DeclaringType); + } + else Assert.Throws(() => owner.Create); Assert.Throws(owner.CompleteEmission); using var bytes = new MemoryStream(); assembly.Save(bytes); From 3b005cacb58b2fbb9b22196e4bc547c3a7f6bb7c Mon Sep 17 00:00:00 2001 From: nickna Date: Tue, 6 Oct 2026 16:28:18 -0700 Subject: [PATCH 16/16] Implement lexical private-in brand checks (#1962) --- docs/plans/private-in-brand-checks.md | 65 ++++ .../CompilationContext.ClassExpressions.cs | 2 + .../ExpressionEmitterBase.CallHelpers.cs | 1 + .../ExpressionEmitterBase.ClassDefinitions.cs | 7 + .../ExpressionEmitterBase.PrivateIn.cs | 151 ++++++++++ ...pressionEmitterBase.PrivateMethodValues.cs | 36 ++- .../Compilation/ExpressionEmitterBase.cs | 3 + .../Compilation/ILCompiler.ArrowFunctions.cs | 3 + src/SharpTS/Compilation/ILCompiler.Async.cs | 3 + .../ILCompiler.Classes.ClassExpressions.cs | 28 +- .../ILCompiler.Classes.Constructors.cs | 277 ++++++++---------- .../Compilation/ILCompiler.Classes.Static.cs | 16 + src/SharpTS/Compilation/ILCompiler.Classes.cs | 3 + .../ILCompiler.ContextFactories.cs | 3 + .../Compilation/NestedFunctionLifter.cs | 5 + .../PrivateClassElementRegistry.cs | 1 + .../Compilation/RuntimeFeatureDetector.cs | 3 + src/SharpTS/Execution/Interpreter.Dispatch.cs | 2 + .../Execution/Interpreter.Expressions.cs | 8 +- .../Execution/Interpreter.PrivateIn.cs | 30 ++ .../Execution/Interpreter.Statements.cs | 9 +- src/SharpTS/Parsing/AST.cs | 2 + src/SharpTS/Parsing/GeneratorArrowLifter.cs | 5 + src/SharpTS/Parsing/Parser.Expressions.cs | 9 +- .../Parser.ForInitializerValidation.cs | 64 ++++ src/SharpTS/Parsing/Parser.Statements.cs | 3 + .../Parsing/Visitors/AstNodeCatalog.cs | 1 + .../Parsing/Visitors/AstVisitorBase.cs | 3 + src/SharpTS/Runtime/RuntimeEnvironment.cs | 8 + src/SharpTS/Runtime/Types/SharpTSClass.cs | 50 +++- .../ControlFlow/LoopAssignmentAnalyzer.cs | 3 + .../TypeSystem/TypeChecker.Dispatch.cs | 1 + .../TypeSystem/TypeChecker.Expressions.cs | 2 + .../TypeSystem/TypeChecker.PrivateIn.cs | 39 +++ .../TypeChecker.Statements.Classes.Stages.cs | 54 ++-- .../TypeChecker.Statements.Classes.cs | 4 + ...ypeChecker.StrictPropertyInitialization.cs | 3 + src/SharpTS/TypeSystem/TypeChecker.cs | 1 + src/SharpTS/TypeSystem/TypeMap.cs | 4 + .../ParserTests/PrivateInParserTests.cs | 252 ++++++++++++++++ .../SharedTests/PrivateInTests.cs | 230 +++++++++++++++ tests/fixtures/PrivateIn/README.md | 6 +- 42 files changed, 1196 insertions(+), 204 deletions(-) create mode 100644 src/SharpTS/Compilation/ExpressionEmitterBase.PrivateIn.cs create mode 100644 src/SharpTS/Execution/Interpreter.PrivateIn.cs create mode 100644 src/SharpTS/Parsing/Parser.ForInitializerValidation.cs create mode 100644 src/SharpTS/TypeSystem/TypeChecker.PrivateIn.cs create mode 100644 tests/SharpTS.Tests/ParserTests/PrivateInParserTests.cs create mode 100644 tests/SharpTS.Tests/SharedTests/PrivateInTests.cs diff --git a/docs/plans/private-in-brand-checks.md b/docs/plans/private-in-brand-checks.md index 5feed60d5..00f756abd 100644 --- a/docs/plans/private-in-brand-checks.md +++ b/docs/plans/private-in-brand-checks.md @@ -94,3 +94,68 @@ Stop after the specified grammar, declared-name lookup and brand cases pass. Private-method values remain #1961; repeated class-evaluation identity and computed-key behavior remain #1906. General parser compatibility, broader class features and unrelated conformance changes do not enter this successor. + +## Implementation verification (#1962) + +The implementation adds the relational AST node, lexical checked-owner lookup, +right-operand traversal and presence queries in both engines. Nested class +expressions retain their enclosing evaluation owner; derived instances use the +declaring template's brand, and generic static probes use the exact guest owner. +Instance fields install in source order after their initializer completes, while +the method brand is available before those initializers run. + +Validation on Windows ARM64, 2026-10-06: + +| Check | Recorded result | +| --- | --- | +| Release build of `SharpTS.Tests` | Succeeded, zero errors; existing NU1902 package advisory | +| `PrivateInTests`, `PrivateInParserTests`, `AstDispatchTests` | 95 passed, zero failed | +| Private/generic/class-expression/arrow/repeated-class selection | 2,043 passed, zero failed, after the #1965 capture follow-up | +| Runtime reference fixtures | All 14 matched Node v25.5.0 stdout, exit 0 and empty stderr | +| Code-quality gate | Passed: 27 duplicate groups, zero errors | +| Actual AOT/trim/single-file analyzer restore and rebuild | Passed: unchanged zero-warning baseline | +| Pinned TypeScript checker smoke gate | Passed: all 32 corpus cases, clean TypeScript 6.0.3 checkout | + +The shared runtime tests execute both engines, serialize and verify emitted IL, +and execute the resulting assembly. Thirteen fixtures also execute without a +deployed `SharpTS.dll`; the proxy fixture uses the deployed runtime. Module-owner +controls run in both engines and verify the compiled multi-file assembly. The +default execution deadline is 30 seconds. Runtime reference sources were emitted +with TypeScript 7.0.2 (`--target ES2022 --skipLibCheck --noImplicitAny false`); +the final flag permits the timing fixtures' unannotated private fields without +changing their runtime source. Node's native type stripper was not used as the +reference parser for the generator fixtures. + +Two added static-write controls reject assignment before a private field's +initializer has completed, leave its brand absent, and evaluate the RHS once. +They also cover an abrupt RHS and successful assignment after installation for +a generic named owner and repeated class expressions. The preserved pre-fix +compiler (`28DF7ED461F26E6AEE0B34B29FA287925CC2C4D1FC9DB30298A13D5768E621E3`) +failed these semantic controls in both engines even though its emitted IL +verified. The candidate matches Node; the two reference emissions additionally +set `--useUnknownInCatchVariables false` for the caught-error message assertion. + +The pinned Test262 comparison selected all 19 `class-fields-private-in` files at +revision `d5e73fc8d2c663554fb72e2380a8c2bc1a318a33`. In each engine the unchanged, +preserved `c782c870` binary reported 11 parse errors and eight deferred negative +cases; the candidate reported eight passes, three parse errors and the same +eight deferred negatives. All worker processes exited 0 with empty stderr and +no runtime failures or timeouts. Two remaining errors require private accessor +declaration parsing; the third uses `function await()` as an ordinary identifier. +The deferred negative cases are not credited as validated semantics; local +parser/checker negative tests provide the separate negative coverage. + +Evidence is retained under `artifacts/issue1962-lexical-*`, +`artifacts/issue1962-static-write/` (focused TRX, reference and pre-fix outputs), +`artifacts/issue1965-shadow-capture/broader-tests.log`, +`artifacts/validation/code-quality-final.log` and `artifacts/validation/issue1962/`. +The latter includes exact commands, binary hashes and every Test262 classification. +The final candidate rerun preserves the same eight passes, three parse errors and +eight deferred negatives in each engine. A separate 28-process CLI matrix checks +the three retained positives against Node, interpretation, verified standalone +emission, and hosted initialization with default declarations and `--noLib`. +Hosted execution uses the public factory and `InitializeAsync`, with a 30-second +process deadline; it is separate from ordinary entry-point execution. These +checks do not claim native AOT publication. The consolidated PR records the +full-suite results separately. The checker gate uses the clean pinned TypeScript +6.0.3 corpus and its committed diagnostics without live `tsc` or npm. diff --git a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs index 8ef3b6cef..f42a4514f 100644 --- a/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs +++ b/src/SharpTS/Compilation/CompilationContext.ClassExpressions.cs @@ -30,6 +30,8 @@ public partial class CompilationContext public ClassGenericParameterRegistry? ClassGenericParameters { get; set; } public Dictionary? ClassExprSuperclass { get; set; } public Dictionary>? ClassExprCaptureSlots { get; set; } + internal const string PrivateOwnerCaptureName = "'privateOwner"; + internal IReadOnlyDictionary? ClassExprEnclosingClasses { get; set; } // Current class expression being compiled public Expr.ClassExpr? CurrentClassExpr { get; set; } diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs index 84ac07081..d951c1281 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.CallHelpers.cs @@ -1314,6 +1314,7 @@ protected static bool AnyContainsSuspension(IEnumerable exprs) Expr.CallPrivate cp => ExprContainsSuspension(cp.Object) || AnyContainsSuspension(cp.Arguments), Expr.Get get => ExprContainsSuspension(get.Object), Expr.GetPrivate gp => ExprContainsSuspension(gp.Object), + Expr.PrivateIn presence => ExprContainsSuspension(presence.Object), Expr.Set s => ExprContainsSuspension(s.Object) || ExprContainsSuspension(s.Value), Expr.SetPrivate sp => ExprContainsSuspension(sp.Object) || ExprContainsSuspension(sp.Value), Expr.GetIndex gi => ExprContainsSuspension(gi.Object) || ExprContainsSuspension(gi.Index), diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs index 55cd6f8fd..cf58ed930 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.ClassDefinitions.cs @@ -229,6 +229,13 @@ private void EmitDefinitionCaptureArray(IReadOnlyList names) var values = new List(names.Count); foreach (var name in names) { + if (name == CompilationContext.PrivateOwnerCaptureName) + { + if (!TryEmitOwnedClassDefinition()) + throw new InvalidOperationException("Nested private names require their enclosing class evaluation."); + values.Add(_helpers.SpillStoreObject()); + continue; + } FieldInfo? field = null; if (Ctx.CellBindingLocals.TryGetValue(name, out var cell)) { diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateIn.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateIn.cs new file mode 100644 index 000000000..495e871b6 --- /dev/null +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateIn.cs @@ -0,0 +1,151 @@ +using System.Reflection.Emit; +using SharpTS.Parsing; + +namespace SharpTS.Compilation; + +public abstract partial class ExpressionEmitterBase +{ + protected virtual void EmitPrivateIn(Expr.PrivateIn expression) + { + string name = expression.Name.Lexeme.TrimStart('#'); + string current = Ctx.CurrentClassName ?? throw new InvalidOperationException("Private name has no lexical owner."); + string owner = Ctx.TypeMap?.GetPrivateInOwner(expression.Name) is { } checkedOwner + ? Ctx.ResolveClassName(checkedOwner, checkedOwner.Name) + : new[] { current }.Concat(Ctx.EnclosingClassNames ?? []) + .First(className => Ctx.ClassRegistry?.GetPrivateElements(className) is { } elements + && (elements.FieldNames.Contains(name) || elements.StaticFields.ContainsKey(name) + || elements.Methods.ContainsKey(name) || elements.StaticMethods.ContainsKey(name))); + var declaration = Ctx.ClassRegistry!.GetPrivateElements(owner)!; + var receiver = SpillGenericPrivateOperand(expression.Object); + var invalid = IL.DefineLabel(); + var absent = IL.DefineLabel(); + var done = IL.DefineLabel(); + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Brfalse, invalid); + foreach (var primitive in new Type[] { Types.String, Types.Double, Types.Boolean, Types.BigInteger, + Ctx.Runtime!.Sentinels.UndefinedType, Ctx.Runtime.Symbols.Type }) + { + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Isinst, primitive); + IL.Emit(OpCodes.Brtrue, invalid); + } + + bool isStatic = declaration.StaticFields.ContainsKey(name) || declaration.StaticMethods.ContainsKey(name); + if (TryEmitPrivateInOwnerDefinition(owner)) + { + var definition = IL.DeclareLocal(Ctx.Runtime!.ClassDefinitions.Type); + IL.Emit(OpCodes.Stloc, definition); + if (isStatic) IL.Emit(OpCodes.Ldloc, receiver); + else + { + var ownerExpression = Ctx.ClassExprBuilders!.First(entry => entry.Value.FullName == owner).Key; + var template = Ctx.ClassExprFactories![ownerExpression].Template; + var field = Ctx.ClassExprDefinitionFields![ownerExpression]; + var reference = template.IsGenericType ? EmitterTypeHelpers.ResolveField(template, field) : field; + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Isinst, reference.DeclaringType!); + IL.Emit(OpCodes.Brfalse, absent); + IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Castclass, reference.DeclaringType!); + IL.Emit(OpCodes.Ldfld, reference); + } + IL.Emit(OpCodes.Ldloc, definition); + IL.Emit(OpCodes.Ceq); + IL.Emit(OpCodes.Brfalse, absent); + if (isStatic) + { + if (declaration.StaticMethods.ContainsKey(name)) IL.Emit(OpCodes.Ldc_I4_1); + else + { + IL.Emit(OpCodes.Ldloc, definition); + IL.Emit(OpCodes.Ldfld, Ctx.Runtime.ClassDefinitions.PrivateMembers); + IL.Emit(OpCodes.Ldstr, $"field:{name}"); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.DictionaryStringObject, "ContainsKey", Types.String)); + } + IL.Emit(OpCodes.Br, done); + } + else EmitInstancePresence(); + } + else if (isStatic) + { + Ctx.ClassRegistry.TryGetClass(owner, out var builder); + if (builder!.IsGenericTypeDefinition) + { + var type = IL.DeclareLocal(Types.Type); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Isinst, Types.Type); IL.Emit(OpCodes.Stloc, type); + IL.Emit(OpCodes.Ldloc, type); IL.Emit(OpCodes.Brfalse, absent); + IL.Emit(OpCodes.Ldloc, type); IL.Emit(OpCodes.Callvirt, Types.GetProperty(Types.Type, "IsGenericType").GetGetMethod()!); + IL.Emit(OpCodes.Brfalse, absent); + IL.Emit(OpCodes.Ldloc, type); IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.Type, "GetGenericTypeDefinition")); + } + else IL.Emit(OpCodes.Ldloc, receiver); + IL.Emit(OpCodes.Ldtoken, builder); IL.Emit(OpCodes.Call, Types.TypeGetTypeFromHandle); + IL.Emit(OpCodes.Ceq); IL.Emit(OpCodes.Brfalse, absent); + if (declaration.StaticMethods.ContainsKey(name)) IL.Emit(OpCodes.Ldc_I4_1); + else + { + Ctx.ClassRegistry.TryGetCallableStaticPrivateField(owner, name, out var ownerField); + var presence = ownerField!.DeclaringType!.IsConstructedGenericType + ? EmitterTypeHelpers.ResolveField(ownerField.DeclaringType, declaration.StaticPresence!) + : declaration.StaticPresence!; + IL.Emit(OpCodes.Ldsfld, presence); + IL.Emit(OpCodes.Ldstr, name); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(typeof(HashSet), "Contains", Types.String)); + } + IL.Emit(OpCodes.Br, done); + } + else EmitInstancePresence(); + + IL.MarkLabel(invalid); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, "Right-hand side of private 'in' is not an object"); + IL.MarkLabel(absent); IL.Emit(OpCodes.Ldc_I4_0); + IL.MarkLabel(done); + SetStackType(StackType.Boolean); + + void EmitInstancePresence() + { + var slots = IL.DeclareLocal(Types.DictionaryStringObject); + if (declaration.InstanceBridge is { } bridge) + { + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Isinst, bridge.InterfaceType); IL.Emit(OpCodes.Brfalse, absent); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Castclass, bridge.InterfaceType); + IL.Emit(OpCodes.Callvirt, bridge.StorageGetter); + } + else IL.Emit(OpCodes.Ldsfld, declaration.Storage!); + IL.Emit(OpCodes.Ldloc, receiver); IL.Emit(OpCodes.Ldloca, slots); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(declaration.Storage!.FieldType, "TryGetValue", Types.Object, Types.DictionaryStringObject.MakeByRefType())); + IL.Emit(OpCodes.Brfalse, absent); + if (declaration.Methods.ContainsKey(name)) IL.Emit(OpCodes.Ldc_I4_1); + else + { + IL.Emit(OpCodes.Ldloc, slots); IL.Emit(OpCodes.Ldstr, name); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.DictionaryStringObject, "ContainsKey", Types.String)); + } + IL.Emit(OpCodes.Br, done); + } + + } + + private bool TryEmitPrivateInOwnerDefinition(string owner) + { + var ownerExpression = Ctx.ClassExprBuilders?.FirstOrDefault(entry => entry.Value.FullName == owner).Key; + if (ownerExpression is null) + return false; + var ownerBuilder = Ctx.ClassExprBuilders![ownerExpression]; + if (!TryEmitOwnedClassDefinition()) + throw new InvalidOperationException("Private name has no class evaluation owner."); + var expression = Ctx.CurrentClassExpr ?? Ctx.ClassExprBuilders! + .First(entry => ReferenceEquals(entry.Value, Ctx.CurrentClassBuilder)).Key; + while (!ReferenceEquals(Ctx.ClassExprBuilders![expression], ownerBuilder)) + { + int slot = Ctx.ClassExprCaptureSlots![expression][CompilationContext.PrivateOwnerCaptureName]; + IL.Emit(OpCodes.Ldfld, Ctx.Runtime!.ClassDefinitions.Captures); + IL.Emit(OpCodes.Ldc_I4, slot); + IL.Emit(OpCodes.Ldelem_Ref); + IL.Emit(OpCodes.Castclass, Ctx.Runtime.ClassDefinitions.Type); + string parent = Ctx.ClassExprEnclosingClasses![expression]; + expression = Ctx.ClassExprBuilders.First(entry => entry.Value.FullName == parent).Key; + } + return true; + } +} diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateMethodValues.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateMethodValues.cs index 65c7af2d6..c233a70b1 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateMethodValues.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.PrivateMethodValues.cs @@ -45,17 +45,32 @@ private LocalBuilder EmitOrdinaryPrivateBrandCheck(string owner, LocalBuilder re protected bool TryEmitGuestStaticPrivateField(Expr receiverExpression, Token token, Expr? assignedValue = null) { - if (Ctx.GuestThisVariableName == null || Ctx.CurrentClassName is not { } current) return false; + if ((Ctx.GuestThisVariableName == null && assignedValue == null) || Ctx.CurrentClassName is not { } current) return false; string name = token.Lexeme.TrimStart('#'); string owner = Ctx.ResolvePrivateFieldOwner(current, name); - if (Ctx.ClassRegistry?.GetPrivateElements(owner)?.StaticFields.ContainsKey(name) != true) return false; + var declaration = Ctx.ClassRegistry?.GetPrivateElements(owner); + if (declaration?.StaticFields.ContainsKey(name) != true) return false; var receiver = SpillGenericPrivateOperand(receiverExpression); var value = assignedValue == null ? null : SpillGenericPrivateOperand(assignedValue); EmitOrdinaryStaticPrivateBrandCheck(owner, receiver, name); - if (!Ctx.ClassRegistry.TryGetCallableStaticPrivateField(owner, name, out var field)) + if (!Ctx.ClassRegistry!.TryGetCallableStaticPrivateField(owner, name, out var field)) throw new InvalidOperationException("Static private field has no callable owner."); if (value == null) IL.Emit(OpCodes.Ldsfld, field!); - else { IL.Emit(OpCodes.Ldloc, value); IL.Emit(OpCodes.Stsfld, field!); IL.Emit(OpCodes.Ldloc, value); } + else + { + // A successful receiver brand check does not install a later field. + var presence = field!.DeclaringType!.IsConstructedGenericType + ? EmitterTypeHelpers.ResolveField(field.DeclaringType, declaration.StaticPresence!) + : declaration.StaticPresence!; + var installed = IL.DefineLabel(); + IL.Emit(OpCodes.Ldsfld, presence); + IL.Emit(OpCodes.Ldstr, name); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(typeof(HashSet), "Contains", Types.String)); + IL.Emit(OpCodes.Brtrue, installed); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, $"Cannot access private member #{name} before initialization"); + IL.MarkLabel(installed); + IL.Emit(OpCodes.Ldloc, value); IL.Emit(OpCodes.Stsfld, field); IL.Emit(OpCodes.Ldloc, value); + } SetStackUnknown(); return true; } @@ -101,6 +116,19 @@ protected bool TryEmitDefinitionPrivateField(Expr receiverExpression, Token toke { IL.Emit(OpCodes.Ldloc, definition); IL.Emit(OpCodes.Ldfld, Ctx.Runtime!.ClassDefinitions.PrivateMembers); + if (value != null) + { + var slots = IL.DeclareLocal(Types.DictionaryStringObject); + var installed = IL.DefineLabel(); + IL.Emit(OpCodes.Stloc, slots); + IL.Emit(OpCodes.Ldloc, slots); + IL.Emit(OpCodes.Ldstr, $"field:{name}"); + IL.Emit(OpCodes.Callvirt, Types.GetMethod(Types.DictionaryStringObject, "ContainsKey", Types.String)); + IL.Emit(OpCodes.Brtrue, installed); + GuestErrorEmitter.ThrowTypeError(IL, Ctx.Runtime!, $"Cannot access private member #{name} before initialization"); + IL.MarkLabel(installed); + IL.Emit(OpCodes.Ldloc, slots); + } } else if (declaration.InstanceBridge is { } bridge) IL.Emit(OpCodes.Ldloc, EmitGenericPrivateBrandCheck(bridge, receiver, name)); diff --git a/src/SharpTS/Compilation/ExpressionEmitterBase.cs b/src/SharpTS/Compilation/ExpressionEmitterBase.cs index 7f26fd78d..ec74f1463 100644 --- a/src/SharpTS/Compilation/ExpressionEmitterBase.cs +++ b/src/SharpTS/Compilation/ExpressionEmitterBase.cs @@ -257,6 +257,9 @@ public virtual void EmitExpression(Expr expr) case Expr.GetPrivate gp: EmitGetPrivate(gp); break; + case Expr.PrivateIn presence: + EmitPrivateIn(presence); + break; case Expr.SetPrivate sp: EmitSetPrivate(sp); break; diff --git a/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs b/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs index 7567b7bc7..4274e05a9 100644 --- a/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs +++ b/src/SharpTS/Compilation/ILCompiler.ArrowFunctions.cs @@ -1056,6 +1056,9 @@ or Stmt.Class case Expr.GetPrivate gp: CollectArrowsFromExpr(gp.Object); break; + case Expr.PrivateIn presence: + CollectArrowsFromExpr(presence.Object); + break; case Expr.SetPrivate sp2: CollectArrowsFromExpr(sp2.Object); CollectArrowsFromExpr(sp2.Value); diff --git a/src/SharpTS/Compilation/ILCompiler.Async.cs b/src/SharpTS/Compilation/ILCompiler.Async.cs index 1a14718ba..d0644f2f4 100644 --- a/src/SharpTS/Compilation/ILCompiler.Async.cs +++ b/src/SharpTS/Compilation/ILCompiler.Async.cs @@ -1035,6 +1035,9 @@ private void AnalyzeArrowExprForAwaits(Expr expr, ref int awaitCount, ref bool s case Expr.Get g: AnalyzeArrowExprForAwaits(g.Object, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); break; + case Expr.PrivateIn presence: + AnalyzeArrowExprForAwaits(presence.Object, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); + break; case Expr.Set s: AnalyzeArrowExprForAwaits(s.Object, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); AnalyzeArrowExprForAwaits(s.Value, ref awaitCount, ref seenAwait, declaredVariables, usedAfterAwait, declaredBeforeAwait); diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs index 6c403a59c..94a48ef93 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.ClassExpressions.cs @@ -52,6 +52,9 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) lexicalCaptures.UnionWith(_closures.Analyzer.GetCaptures(accessor)); if (classExpr.Name != null) lexicalCaptures.Remove(classExpr.Name.Lexeme); + if (_classExprs.EnclosingClass.TryGetValue(classExpr, out var lexicalClass) + && _classExprs.Names.ContainsValue(lexicalClass)) + lexicalCaptures.Add(CompilationContext.PrivateOwnerCaptureName); _classExprs.CaptureSlots[classExpr] = lexicalCaptures.OrderBy(name => name, StringComparer.Ordinal) .Select((name, index) => (name, index)).ToDictionary(entry => entry.name, entry => entry.index, StringComparer.Ordinal); @@ -224,6 +227,8 @@ private void DefineClassExpression(Expr.ClassExpr classExpr) // Store the type builder _classExprs.Builders[classExpr] = typeBuilder; + if (_typeMap.GetClassExprType(classExpr) is { } checkedClass) + _classes.CheckedDeclarationNames[checkedClass.Core.DeclarationId] = className; } /// @@ -584,8 +589,15 @@ private CompilationContext CreateClassExpressionContext( ctx.CurrentClassBuilder = typeBuilder; ctx.EmittingTypeBuilder = typeBuilder; ctx.CurrentClassName = className; - if (_classExprs.EnclosingClass.TryGetValue(classExpr, out var enclosingClassName)) - ctx.EnclosingClassNames = [enclosingClassName]; + List enclosingClasses = []; + var lexicalExpression = classExpr; + while (_classExprs.EnclosingClass.TryGetValue(lexicalExpression, out var enclosingClassName)) + { + enclosingClasses.Add(enclosingClassName); + lexicalExpression = _classExprs.Names.FirstOrDefault(entry => entry.Value == enclosingClassName).Key; + if (lexicalExpression is null) break; + } + ctx.EnclosingClassNames = enclosingClasses; ctx.CurrentSuperclassName = _classExprs.Superclass.GetValueOrDefault(classExpr); ctx.PropertyBackingFields = _typedInterop.PropertyBackingFields; ctx.ClassProperties = _typedInterop.ClassProperties; @@ -822,11 +834,14 @@ private void EmitClassExpressionConstructor(Expr.ClassExpr classExpr, TypeBuilde void EmitInstanceFieldInitializers() { - var savedFields = ctx.FieldsField; - EmitPrivateFieldInitialization(il, className, ClassExpressionDeclaration(classExpr), ctx); - ctx.FieldsField = savedFields; - foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsPrivate && !f.IsDeclare && (f.Initializer != null || f.ComputedKey != null))) + var privateStorage = EmitPrivateFieldStorage(il, className); + foreach (var field in classExpr.Fields.Where(f => !f.IsStatic && !f.IsDeclare && (f.IsPrivate || f.Initializer != null || f.ComputedKey != null))) { + if (field.IsPrivate) + { + EmitPrivateFieldInitializer(il, field, privateStorage!, emitter); + continue; + } if (field.ComputedKey != null) { il.Emit(OpCodes.Ldarg_0); @@ -971,6 +986,7 @@ private static bool ContainsSuperCallInExpr(Expr expr) { Expr.Call call => call.Callee is Expr.Super, Expr.Binary bin => ContainsSuperCallInExpr(bin.Left) || ContainsSuperCallInExpr(bin.Right), + Expr.PrivateIn presence => ContainsSuperCallInExpr(presence.Object), Expr.Logical log => ContainsSuperCallInExpr(log.Left) || ContainsSuperCallInExpr(log.Right), Expr.Grouping grp => ContainsSuperCallInExpr(grp.Expression), _ => false diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs index 27e0d7383..2501e4cf8 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Constructors.cs @@ -258,109 +258,11 @@ private void EmitConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt, Fiel } // Emit instance field initializers to backing fields (before constructor body) - // Note: Declare fields are excluded - they have no initialization - var instanceFieldsWithInit = classStmt.Fields.Where(f => - !f.IsStatic && !f.IsPrivate && !f.IsDeclare && - (f.Initializer != null || f.ComputedKey != null)).ToList(); - if (instanceFieldsWithInit.Count > 0) - { - ctx.FieldsField = fieldsField; - ctx.IsInstanceMethod = true; - var initEmitter = new ILEmitter(ctx); - - foreach (var field in instanceFieldsWithInit) - { - // Handle computed property names (e.g., [mySymbol]: string = "value") - if (field.ComputedKey != null) - { - // Computed keys use dynamic SetIndex to support Symbol keys - // Stack: this - il.Emit(OpCodes.Ldarg_0); - // Load the key captured when the class definition was evaluated. - 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) - { - initEmitter.EmitExpression(field.Initializer); - initEmitter.EmitBoxIfNeeded(field.Initializer); - } - else - { - il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); - } - // Call Runtime.SetIndex(object, key, value) - il.Emit(OpCodes.Call, _runtime.ObjectWrite.Index); - continue; - } - - string fieldName = field.Name.Lexeme; - string pascalName = NamingConventions.ToPascalCase(fieldName); - - // Check if this is a declared property with a backing field (using PascalCase key) - if (_typedInterop.PropertyBackingFields.TryGetValue(className, out var backingFields) && - backingFields.TryGetValue(pascalName, out var backingField)) - { - // Store directly in backing field - il.Emit(OpCodes.Ldarg_0); // this - - // Emit initializer expression - initEmitter.EmitExpression(field.Initializer!); - - // Convert to proper type if needed - Type targetType = _typedInterop.PropertyTypes[className][pascalName]; - EmitTypeConversion(il, initEmitter, field.Initializer!, targetType); - - il.Emit(OpCodes.Stfld, backingField); - } - else - { - // Fallback: store in _extras dictionary (for fields without backing fields) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, EmitterTypeHelpers.SelfMethodReference(ensureFields)); - il.Emit(OpCodes.Ldstr, fieldName); - // number[] unboxing: an `arr: number[] = []` field is created as a numeric $Array - // (escaping by nature — a field is aliasable), so `this.arr[i]=v` writes unboxed. - if (!initEmitter.TryEmitNumericEmptyArrayInit(field.TypeAnnotation, field.Initializer)) - { - initEmitter.EmitExpression(field.Initializer!); - initEmitter.EmitBoxIfNeeded(field.Initializer!); - } - il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); - } - } - } - - // Initialize instance declare fields (without initializers) to null in _extras dictionary - // TypeScript semantics: uninitialized fields return null/undefined, not CLR defaults - var instanceDeclareFields = classStmt.Fields.Where(f => - !f.IsStatic && !f.IsPrivate && f.IsDeclare && f.Initializer == null && f.ComputedKey == null).ToList(); - foreach (var field in instanceDeclareFields) - { - string fieldName = field.Name.Lexeme; - // Store null in _extras dictionary - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, EmitterTypeHelpers.SelfMethodReference(ensureFields)); - il.Emit(OpCodes.Ldstr, fieldName); - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); - } - - // A derived class does not install its private brand until super() returns. Deferring - // this prevents a base field initializer that dispatches into the derived instance from - // observing derived private elements prematurely. bool deferPrivateInitialization = constructor?.Body != null && classStmt.SuperclassExpr != null && constructor.Body.Any(ContainsSuperCall); - bool privateInitializationEmitted = false; - if (!deferPrivateInitialization) - { - // ES2022: Initialize instance private fields. Private fields use a - // ConditionalWeakTable for GC-friendly per-instance storage. - EmitPrivateFieldInitialization(il, className, classStmt, ctx); - privateInitializationEmitted = true; - } - + bool privateInitializationEmitted = !deferPrivateInitialization; + if (!deferPrivateInitialization) EmitOrderedFieldInitializers(); // TypeScript 4.9+: Initialize instance auto-accessor backing fields if (classStmt.AutoAccessors != null) { @@ -421,7 +323,7 @@ private void EmitConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt, Fiel emitter.EmitStatement(stmt); if (!privateInitializationEmitted && ContainsSuperCall(stmt)) { - EmitPrivateFieldInitialization(il, className, classStmt, ctx); + EmitOrderedFieldInitializers(); privateInitializationEmitted = true; } } @@ -431,77 +333,134 @@ private void EmitConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt, Fiel } il.Emit(OpCodes.Ret); - } - - /// - /// Emits IL to initialize ES2022 private fields for a new instance. - /// Creates a Dictionary with initial values and adds it to the ConditionalWeakTable. - /// - private void EmitPrivateFieldInitialization( - ILGenerator il, - string className, - Stmt.Class classStmt, - CompilationContext ctx) - { - // Check if this class has instance private fields - var privateElements = _classes.PrivateElements.Require(className); - if (privateElements.Storage is not { } storageField) - return; - var fieldNames = privateElements.FieldNames; - - var instancePrivateFields = classStmt.Fields - .Where(f => f.IsPrivate && !f.IsStatic) - .ToList(); + void EmitOrderedFieldInitializers() + { + // Note: Declare fields are excluded - they have no initialization + var instanceFieldsWithInit = classStmt.Fields.Where(f => + !f.IsStatic && !f.IsDeclare && + (f.IsPrivate || f.Initializer != null || f.ComputedKey != null)).ToList(); + if (instanceFieldsWithInit.Count > 0 || _classes.PrivateElements.Require(className).Storage != null) + { + ctx.FieldsField = fieldsField; + ctx.IsInstanceMethod = true; + var initEmitter = new ILEmitter(ctx); - ctx.FieldsField = null; // Not using _fields for private field init - ctx.IsInstanceMethod = true; - var initEmitter = new ILEmitter(ctx); + var privateStorage = EmitPrivateFieldStorage(il, className); + foreach (var field in instanceFieldsWithInit) + { + if (field.IsPrivate) + { + EmitPrivateFieldInitializer(il, field, privateStorage!, initEmitter); + continue; + } + // Handle computed property names (e.g., [mySymbol]: string = "value") + if (field.ComputedKey != null) + { + // Computed keys use dynamic SetIndex to support Symbol keys + // Stack: this + il.Emit(OpCodes.Ldarg_0); + // Load the key captured when the class definition was evaluated. + 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) + { + initEmitter.EmitExpression(field.Initializer); + initEmitter.EmitBoxIfNeeded(field.Initializer); + } + else + { + il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + } + // Call Runtime.SetIndex(object, key, value) + il.Emit(OpCodes.Call, _runtime.ObjectWrite.Index); + continue; + } - // Create local for the fields dictionary - var dictType = typeof(Dictionary); - var dictLocal = il.DeclareLocal(dictType); + string fieldName = field.Name.Lexeme; + string pascalName = NamingConventions.ToPascalCase(fieldName); - // Dictionary __fields = new Dictionary(capacity) - il.Emit(OpCodes.Ldc_I4, fieldNames.Count); - il.Emit(OpCodes.Newobj, dictType.GetConstructor([typeof(int)])!); - il.Emit(OpCodes.Stloc, dictLocal); + // Check if this is a declared property with a backing field (using PascalCase key) + if (_typedInterop.PropertyBackingFields.TryGetValue(className, out var backingFields) && + backingFields.TryGetValue(pascalName, out var backingField)) + { + // Store directly in backing field + il.Emit(OpCodes.Ldarg_0); // this - // Add each private field with its initializer value (or null) - foreach (var field in instancePrivateFields) - { - string fieldName = field.Name.Lexeme; - if (fieldName.StartsWith('#')) - fieldName = fieldName[1..]; + // Emit initializer expression + initEmitter.EmitExpression(field.Initializer!); - // __fields[fieldName] = initializer ?? null - il.Emit(OpCodes.Ldloc, dictLocal); - il.Emit(OpCodes.Ldstr, fieldName); + // Convert to proper type if needed + Type targetType = _typedInterop.PropertyTypes[className][pascalName]; + EmitTypeConversion(il, initEmitter, field.Initializer!, targetType); - if (field.Initializer != null) - { - initEmitter.EmitExpression(field.Initializer); - initEmitter.EmitBoxIfNeeded(field.Initializer); + il.Emit(OpCodes.Stfld, backingField); + } + else + { + // Fallback: store in _extras dictionary (for fields without backing fields) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, EmitterTypeHelpers.SelfMethodReference(ensureFields)); + il.Emit(OpCodes.Ldstr, fieldName); + // number[] unboxing: an `arr: number[] = []` field is created as a numeric $Array + // (escaping by nature — a field is aliasable), so `this.arr[i]=v` writes unboxed. + if (!initEmitter.TryEmitNumericEmptyArrayInit(field.TypeAnnotation, field.Initializer)) + { + initEmitter.EmitExpression(field.Initializer!); + initEmitter.EmitBoxIfNeeded(field.Initializer!); + } + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); + } + } } - else + + // Initialize instance declare fields (without initializers) to null in _extras dictionary + // TypeScript semantics: uninitialized fields return null/undefined, not CLR defaults + var instanceDeclareFields = classStmt.Fields.Where(f => + !f.IsStatic && !f.IsPrivate && f.IsDeclare && f.Initializer == null && f.ComputedKey == null).ToList(); + foreach (var field in instanceDeclareFields) { + string fieldName = field.Name.Lexeme; + // Store null in _extras dictionary + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, EmitterTypeHelpers.SelfMethodReference(ensureFields)); + il.Emit(OpCodes.Ldstr, fieldName); il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); } - il.Emit(OpCodes.Callvirt, dictType.GetMethod("set_Item", [typeof(string), typeof(object)])!); + } + } + + // Install the method brand before evaluating fields; each field becomes present + // only after its initializer completes. + private LocalBuilder? EmitPrivateFieldStorage(ILGenerator il, string className) + { + var elements = _classes.PrivateElements.Require(className); + if (elements.Storage is not { } storage) return null; + var slots = il.DeclareLocal(_types.DictionaryStringObject); + il.Emit(OpCodes.Ldc_I4, elements.FieldNames.Count); + il.Emit(OpCodes.Newobj, _types.GetConstructor(_types.DictionaryStringObject, _types.Int32)); + il.Emit(OpCodes.Stloc, slots); + il.Emit(OpCodes.Ldsfld, EmitterTypeHelpers.SelfFieldReference(storage)); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, slots); + il.Emit(OpCodes.Callvirt, _types.GetMethod(storage.FieldType, "Add", typeof(object), _types.DictionaryStringObject)); + return slots; + } - // __privateFields.Add(this, __fields) - var cwtType = EmitGenerics.MakeGenericType(typeof(System.Runtime.CompilerServices.ConditionalWeakTable<,>), typeof(object), typeof(Dictionary)); - var addMethod = _types.GetMethod( - cwtType, - "Add", - typeof(object), - typeof(Dictionary)); - - il.Emit(OpCodes.Ldsfld, storageField); // __privateFields - il.Emit(OpCodes.Ldarg_0); // this - il.Emit(OpCodes.Ldloc, dictLocal); // __fields - il.Emit(OpCodes.Callvirt, addMethod); + private void EmitPrivateFieldInitializer(ILGenerator il, Stmt.Field field, LocalBuilder slots, ILEmitter emitter) + { + il.Emit(OpCodes.Ldloc, slots); + il.Emit(OpCodes.Ldstr, field.Name.Lexeme.TrimStart('#')); + if (field.Initializer is { } initializer) + { + emitter.EmitExpression(initializer); + emitter.EmitBoxIfNeeded(initializer); + } + else il.Emit(OpCodes.Ldsfld, _runtime.Sentinels.UndefinedInstance); + il.Emit(OpCodes.Callvirt, _types.DictionaryStringObjectSetItem); } } diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs b/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs index 1a0029162..6c3c7f478 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.Static.cs @@ -71,6 +71,11 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt } var emitter = new ILEmitter(ctx); + if (privateElements.StaticPresence is { } presence) + { + il.Emit(OpCodes.Newobj, typeof(HashSet).GetConstructor(Type.EmptyTypes)!); + il.Emit(OpCodes.Stsfld, presence); + } // A compact class uses weak side storage; initialize its table before // user-observable static initialization can construct or mutate an instance. @@ -169,6 +174,8 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt il.Emit(OpCodes.Stsfld, staticField); } } + if (field.IsPrivate) + EmitStaticPrivatePresence(il, privateElements, field.Name.Lexeme.TrimStart('#')); break; case Stmt.StaticBlock block: @@ -197,6 +204,7 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt emitter.EmitExpression(field.Initializer!); emitter.EmitBoxIfNeeded(field.Initializer!); il.Emit(OpCodes.Stsfld, staticPrivateField); + EmitStaticPrivatePresence(il, privateElements, fieldName); } } @@ -232,6 +240,14 @@ private void EmitStaticConstructor(TypeBuilder typeBuilder, Stmt.Class classStmt _classes.DeferredDefinitions.MarkInitializerEmitted(deferredDefinition); } + private void EmitStaticPrivatePresence(ILGenerator il, PrivateClassElements elements, string name) + { + il.Emit(OpCodes.Ldsfld, EmitterTypeHelpers.SelfFieldReference(elements.StaticPresence!)); + il.Emit(OpCodes.Ldstr, name); + il.Emit(OpCodes.Callvirt, _types.GetMethod(typeof(HashSet), "Add", typeof(string))); + il.Emit(OpCodes.Pop); + } + /// /// Emits $Runtime.RegisterSymbolAccessor(typeof(ThisClass), symbol, getter, setter, isStatic) /// for each symbol-keyed accessor recorded for this class (#266). diff --git a/src/SharpTS/Compilation/ILCompiler.Classes.cs b/src/SharpTS/Compilation/ILCompiler.Classes.cs index bd003340e..87039bb66 100644 --- a/src/SharpTS/Compilation/ILCompiler.Classes.cs +++ b/src/SharpTS/Compilation/ILCompiler.Classes.cs @@ -558,6 +558,9 @@ private void DefinePrivateClassElements( ? DefinePrivateInstanceBridge(typeBuilder, privateStorage, privateMethods) : null; _classes.PrivateElements.Declare(className, typeBuilder, privateStorage, privateFieldNames, privateStaticFields, privateMethods, privateStaticMethods, instanceBridge); + if (privateStaticFields.Count > 0) + _classes.PrivateElements.Require(className).StaticPresence = typeBuilder.DefineField( + "__installedStaticPrivateFields", typeof(HashSet), FieldAttributes.Assembly | FieldAttributes.Static); DefinePrivateMethodValues(className, typeBuilder, classStmt, expression); } diff --git a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs index 11e8fdd0c..1e27a4f79 100644 --- a/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs +++ b/src/SharpTS/Compilation/ILCompiler.ContextFactories.cs @@ -97,6 +97,7 @@ private CompilationContext CreateBaseCompilationContext(ILGenerator il, MethodBa RuntimeClassDeclarations = _runtimeClassDeclarations, ClassExprStaticMethods = _classExprs.StaticMethods, ClassExprCaptureSlots = _classExprs.CaptureSlots, + ClassExprEnclosingClasses = _classExprs.EnclosingClass, DeferredClassDefinitions = _classes.DeferredDefinitions, BlockScopedClassBuilders = _classes.BlockScopedBuilders, ClassRegistry = GetClassRegistry(), @@ -303,11 +304,13 @@ private CompilationContext CreateNestedAsyncArrowContext(ILGenerator il, Compila ClassExprDefinitionFields = parentCtx.ClassExprDefinitionFields, RuntimeClassDeclarations = parentCtx.RuntimeClassDeclarations, ClassExprCaptureSlots = parentCtx.ClassExprCaptureSlots, + ClassExprEnclosingClasses = parentCtx.ClassExprEnclosingClasses, BlockScopedClassBuilders = parentCtx.BlockScopedClassBuilders, DeferredClassDefinitions = parentCtx.DeferredClassDefinitions, IsStrictMode = parentCtx.IsStrictMode, // ES2022 Private Class Elements support - inherit from parent context CurrentClassName = parentCtx.CurrentClassName, + EnclosingClassNames = parentCtx.EnclosingClassNames, CurrentClassBuilder = parentCtx.CurrentClassBuilder, HasGuestReceiver = parentCtx.HasGuestReceiver, // Registry services diff --git a/src/SharpTS/Compilation/NestedFunctionLifter.cs b/src/SharpTS/Compilation/NestedFunctionLifter.cs index 6fa5c8582..9582164f2 100644 --- a/src/SharpTS/Compilation/NestedFunctionLifter.cs +++ b/src/SharpTS/Compilation/NestedFunctionLifter.cs @@ -922,6 +922,11 @@ private Expr ProcessExpr(Expr expr) var obj = ProcessExpr(e.Object); var value = ProcessExpr(e.Value); return ReferenceEquals(obj, e.Object) && ReferenceEquals(value, e.Value) ? e : e with { Object = obj, Value = value }; } + case Expr.PrivateIn e: + { + var obj = ProcessExpr(e.Object); + return ReferenceEquals(obj, e.Object) ? e : e with { Object = obj }; + } case Expr.GetPrivate e: { var obj = ProcessExpr(e.Object); diff --git a/src/SharpTS/Compilation/PrivateClassElementRegistry.cs b/src/SharpTS/Compilation/PrivateClassElementRegistry.cs index eccf18c44..5d0b0192e 100644 --- a/src/SharpTS/Compilation/PrivateClassElementRegistry.cs +++ b/src/SharpTS/Compilation/PrivateClassElementRegistry.cs @@ -103,6 +103,7 @@ public sealed class PrivateClassElements public IReadOnlyDictionary MethodValues { get; } public PrivateInstanceBridge? InstanceBridge { get; } public FieldBuilder? Storage { get; } + public FieldBuilder? StaticPresence { get; internal set; } public IReadOnlyList FieldNames { get; } public IReadOnlyDictionary StaticFields { get; } public IReadOnlyDictionary Methods { get; } diff --git a/src/SharpTS/Compilation/RuntimeFeatureDetector.cs b/src/SharpTS/Compilation/RuntimeFeatureDetector.cs index f50b060d0..1e0b0612b 100644 --- a/src/SharpTS/Compilation/RuntimeFeatureDetector.cs +++ b/src/SharpTS/Compilation/RuntimeFeatureDetector.cs @@ -1591,6 +1591,9 @@ private void VisitExpr(Expr? expr) case Expr.GetPrivate gp: VisitExpr(gp.Object); break; + case Expr.PrivateIn presence: + VisitExpr(presence.Object); + break; case Expr.SetPrivate sp: VisitExpr(sp.Object); VisitExpr(sp.Value); diff --git a/src/SharpTS/Execution/Interpreter.Dispatch.cs b/src/SharpTS/Execution/Interpreter.Dispatch.cs index 802750f9e..b725d09ae 100644 --- a/src/SharpTS/Execution/Interpreter.Dispatch.cs +++ b/src/SharpTS/Execution/Interpreter.Dispatch.cs @@ -52,6 +52,7 @@ public partial class Interpreter Expr.DestructuringAssign e => VisitDestructuringAssign(e), Expr.Delete e => VisitDelete(e), Expr.GetPrivate e => VisitGetPrivate(e), + Expr.PrivateIn e => VisitPrivateIn(e), Expr.SetPrivate e => VisitSetPrivate(e), Expr.CallPrivate e => VisitCallPrivate(e), Expr.Super e => VisitSuper(e), @@ -158,6 +159,7 @@ public partial class Interpreter Expr.DestructuringAssign e => VisitDestructuringAssignAsync(e), Expr.Delete e => VisitDeleteAsync(e), Expr.GetPrivate e => VisitGetPrivateAsync(e), + Expr.PrivateIn e => VisitPrivateInAsync(e), Expr.SetPrivate e => VisitSetPrivateAsync(e), Expr.CallPrivate e => VisitCallPrivateAsync(e), Expr.Super e => VisitSuperAsync(e), diff --git a/src/SharpTS/Execution/Interpreter.Expressions.cs b/src/SharpTS/Execution/Interpreter.Expressions.cs index a9d751956..406c02bb2 100644 --- a/src/SharpTS/Execution/Interpreter.Expressions.cs +++ b/src/SharpTS/Execution/Interpreter.Expressions.cs @@ -2122,7 +2122,8 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon if (field.IsPrivate) { if (!field.IsStatic) instancePrivateFields.Add(field); - else if (!hasStaticInitializers) staticPrivateFields[field.Name.Lexeme] = field.Initializer == null ? SharpTSUndefined.Instance : Evaluate(field.Initializer); + else staticPrivateFields[field.Name.Lexeme] = !hasStaticInitializers && field.Initializer != null + ? Evaluate(field.Initializer) : SharpTSUndefined.Instance; } else if (field.IsStatic) { @@ -2290,6 +2291,9 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon klass.BindPrivateMemberOwners(); klass.InitializerEnvironment = classEnv; + klass.OrderedInstanceFields = classExpr.Fields.Where(field => !field.IsStatic) + .SelectMany(field => (field.IsPrivate ? instancePrivateFields : instanceFields) + .Where(captured => captured.Name == field.Name)).ToList(); if (symbolAccessors != null) { @@ -2329,7 +2333,7 @@ private async ValueTask EvaluateClassExpressionCore(IEvaluationCon object? fieldValue = field.Initializer != null ? Evaluate(field.Initializer) : field.ComputedKey != null ? SharpTSUndefined.Instance : null; - if (field.IsPrivate) klass.SetStaticPrivateField(field.Name.Lexeme, fieldValue); + if (field.IsPrivate) klass.InstallStaticPrivateField(field.Name.Lexeme, fieldValue); else if (field.ComputedKey != null && computedMemberKeys[field.ComputedKey] is SharpTSSymbol symbol) klass.SetStaticBySymbol(symbol, fieldValue); else diff --git a/src/SharpTS/Execution/Interpreter.PrivateIn.cs b/src/SharpTS/Execution/Interpreter.PrivateIn.cs new file mode 100644 index 000000000..af92c5989 --- /dev/null +++ b/src/SharpTS/Execution/Interpreter.PrivateIn.cs @@ -0,0 +1,30 @@ +using SharpTS.Parsing; +using SharpTS.Runtime; +using SharpTS.Runtime.Exceptions; +using SharpTS.Runtime.Types; + +namespace SharpTS.Execution; + +public partial class Interpreter +{ + internal RuntimeValue VisitPrivateIn(Expr.PrivateIn expr) + => PrivateInCore(Evaluate(expr.Object), expr.Name.Lexeme); + + internal async ValueTask VisitPrivateInAsync(Expr.PrivateIn expr) + => PrivateInCore((await EvaluateAsync(expr.Object)).ToObject(), expr.Name.Lexeme); + + private RuntimeValue PrivateInCore(object? receiver, string name) + { + if (receiver is null or SharpTSUndefined or string or double or bool or SharpTSBigInt or SharpTSSymbol) + throw new ThrowException(new SharpTSTypeError("Right-hand side of private 'in' is not an object")); + var owner = _environment.FindPrivateClass(name) + ?? throw new InterpreterException("Private name has no lexical class owner"); + return RuntimeValue.FromBoolean(owner.HasStaticPrivateField(name) + ? ReferenceEquals(receiver, owner) && owner.HasInstalledStaticPrivateField(name) + : owner.GetStaticPrivateMethod(name) != null + ? ReferenceEquals(receiver, owner) + : owner.GetPrivateMethod(name) != null + ? owner.HasPrivateBrand(receiver) + : owner.HasInstalledPrivateField(receiver, name)); + } +} diff --git a/src/SharpTS/Execution/Interpreter.Statements.cs b/src/SharpTS/Execution/Interpreter.Statements.cs index cbbccaabd..fcb52de84 100644 --- a/src/SharpTS/Execution/Interpreter.Statements.cs +++ b/src/SharpTS/Execution/Interpreter.Statements.cs @@ -2081,7 +2081,7 @@ private async ValueTask VisitClassCore(IEvaluationContext ctx, : null; staticPrivateFields[field.Name.Lexeme] = fieldValue; } - // else: will be evaluated via StaticInitializers with proper 'this' binding + else staticPrivateFields[field.Name.Lexeme] = SharpTSUndefined.Instance; } else { @@ -2329,6 +2329,9 @@ private async ValueTask VisitClassCore(IEvaluationContext ctx, klass.BindPrivateMemberOwners(); klass.InitializerEnvironment = _environment; + klass.OrderedInstanceFields = classStmt.Fields.Where(field => !field.IsStatic) + .SelectMany(field => (field.IsPrivate ? instancePrivateFields : instanceFields) + .Where(captured => captured.Name == field.Name)).ToList(); if (symbolAccessors != null) { @@ -2351,7 +2354,7 @@ private async ValueTask VisitClassCore(IEvaluationContext ctx, { // Create temporary environment with 'this' bound to the class // Also make the class name available so code like Foo.x works - var staticEnv = new RuntimeEnvironment(_environment); + var staticEnv = new RuntimeEnvironment(_environment, strictMode: true) { PrivateClass = klass }; staticEnv.Define("this", klass); staticEnv.Define(classStmt.Name.Lexeme, klass); @@ -2369,7 +2372,7 @@ private async ValueTask VisitClassCore(IEvaluationContext ctx, ? Evaluate(field.Initializer) : field.ComputedKey != null ? SharpTSUndefined.Instance : null; if (field.IsPrivate) - klass.SetStaticPrivateField(field.Name.Lexeme, fieldValue); + klass.InstallStaticPrivateField(field.Name.Lexeme, fieldValue); else if (field.ComputedKey != null && computedMemberKeys[field.ComputedKey] is SharpTSSymbol symbol) klass.SetStaticBySymbol(symbol, fieldValue); else diff --git a/src/SharpTS/Parsing/AST.cs b/src/SharpTS/Parsing/AST.cs index a7ba991e4..3dbc6903a 100644 --- a/src/SharpTS/Parsing/AST.cs +++ b/src/SharpTS/Parsing/AST.cs @@ -115,6 +115,8 @@ public record Get(Expr Object, Token Name, bool Optional = false, bool Defaulted public record Set(Expr Object, Token Name, Expr Value) : Expr; /// Private field access: obj.#field public record GetPrivate(Expr Object, Token Name) : Expr; + /// Lexical private-name presence check: #name in object. + public record PrivateIn(Token Name, Expr Object) : Expr; /// Private field assignment: obj.#field = value public record SetPrivate(Expr Object, Token Name, Expr Value) : Expr; /// Private method call: obj.#method(args) diff --git a/src/SharpTS/Parsing/GeneratorArrowLifter.cs b/src/SharpTS/Parsing/GeneratorArrowLifter.cs index 06a876ba8..7d1b27fd7 100644 --- a/src/SharpTS/Parsing/GeneratorArrowLifter.cs +++ b/src/SharpTS/Parsing/GeneratorArrowLifter.cs @@ -706,6 +706,11 @@ private Expr RewriteExpr(Expr expr) var o = RewriteExpr(g.Object); return ReferenceEquals(o, g.Object) ? g : new Expr.Get(o, g.Name, g.Optional); } + case Expr.PrivateIn probe: + { + var o = RewriteExpr(probe.Object); + return ReferenceEquals(o, probe.Object) ? probe : probe with { Object = o }; + } case Expr.GetPrivate gp: { var o = RewriteExpr(gp.Object); diff --git a/src/SharpTS/Parsing/Parser.Expressions.cs b/src/SharpTS/Parsing/Parser.Expressions.cs index f177849ec..a385b2163 100644 --- a/src/SharpTS/Parsing/Parser.Expressions.cs +++ b/src/SharpTS/Parsing/Parser.Expressions.cs @@ -287,7 +287,14 @@ private Expr Equality() private Expr Comparison() { - Expr expr = Shift(); + Expr expr; + if (Match(TokenType.PRIVATE_IDENTIFIER)) + { + Token name = Previous(); + Consume(TokenType.IN, "Private identifier must be followed by 'in'."); + expr = new Expr.PrivateIn(name, Shift()); + } + else expr = Shift(); // In TSX, adjacent JSX roots are not a less-than comparison. tsc commits to both // elements, reports TS2657 on the first root, and retains both ASTs so their normal diff --git a/src/SharpTS/Parsing/Parser.ForInitializerValidation.cs b/src/SharpTS/Parsing/Parser.ForInitializerValidation.cs new file mode 100644 index 000000000..c277a7bf8 --- /dev/null +++ b/src/SharpTS/Parsing/Parser.ForInitializerValidation.cs @@ -0,0 +1,64 @@ +using SharpTS.Parsing.Visitors; + +namespace SharpTS.Parsing; + +public partial class Parser +{ + // Traditional for initializers use the NoIn grammar. Preserve the parser's + // existing ordinary `in` behavior while enforcing it for the new private-name + // production. These overrides stop at subexpressions whose grammar enables In. + private sealed class PrivateInForInitializerValidator : AstVisitorBase + { + protected override void VisitPrivateIn(Expr.PrivateIn expr) + => throw new Exception("Private identifier 'in' expressions in for-loop initializers must be parenthesized."); + + protected override void VisitGrouping(Expr.Grouping expr) { } + protected override void VisitArrayLiteral(Expr.ArrayLiteral expr) { } + protected override void VisitObjectLiteral(Expr.ObjectLiteral expr) { } + protected override void VisitTemplateLiteral(Expr.TemplateLiteral expr) { } + protected override void VisitClassExpr(Expr.ClassExpr expr) { } + protected override void VisitDynamicImport(Expr.DynamicImport expr) { } + + protected override void VisitCall(Expr.Call expr) => Visit(expr.Callee); + protected override void VisitCallPrivate(Expr.CallPrivate expr) => Visit(expr.Object); + protected override void VisitNew(Expr.New expr) => Visit(expr.Callee); + protected override void VisitGetIndex(Expr.GetIndex expr) => Visit(expr.Object); + protected override void VisitTaggedTemplateLiteral(Expr.TaggedTemplateLiteral expr) => Visit(expr.Tag); + + protected override void VisitTernary(Expr.Ternary expr) + { + Visit(expr.Condition); + Visit(expr.ElseBranch); + } + + protected override void VisitArrowFunction(Expr.ArrowFunction expr) + { + if (expr.ExpressionBody is not null) + Visit(expr.ExpressionBody); + } + + protected override void VisitDestructuringAssign(Expr.DestructuringAssign expr) + { + if (expr.RawDefault is not null) + Visit(expr.RawDefault); + } + + protected override void VisitSetIndex(Expr.SetIndex expr) + { + Visit(expr.Object); + Visit(expr.Value); + } + + protected override void VisitCompoundSetIndex(Expr.CompoundSetIndex expr) + { + Visit(expr.Object); + Visit(expr.Value); + } + + protected override void VisitLogicalSetIndex(Expr.LogicalSetIndex expr) + { + Visit(expr.Object); + Visit(expr.Value); + } + } +} diff --git a/src/SharpTS/Parsing/Parser.Statements.cs b/src/SharpTS/Parsing/Parser.Statements.cs index aee4f139c..b63f42c6a 100644 --- a/src/SharpTS/Parsing/Parser.Statements.cs +++ b/src/SharpTS/Parsing/Parser.Statements.cs @@ -199,6 +199,9 @@ private Stmt ForStatement() private Stmt FinishTraditionalFor(Stmt? initializer) { + if (initializer is not null) + new PrivateInForInitializerValidator().Visit(initializer); + Expr? condition = null; if (!Check(TokenType.SEMICOLON)) { diff --git a/src/SharpTS/Parsing/Visitors/AstNodeCatalog.cs b/src/SharpTS/Parsing/Visitors/AstNodeCatalog.cs index d03d71e62..81a179a3a 100644 --- a/src/SharpTS/Parsing/Visitors/AstNodeCatalog.cs +++ b/src/SharpTS/Parsing/Visitors/AstNodeCatalog.cs @@ -41,6 +41,7 @@ public static class AstNodeCatalog typeof(Expr.Get), typeof(Expr.Set), typeof(Expr.GetPrivate), + typeof(Expr.PrivateIn), typeof(Expr.SetPrivate), typeof(Expr.CallPrivate), typeof(Expr.This), diff --git a/src/SharpTS/Parsing/Visitors/AstVisitorBase.cs b/src/SharpTS/Parsing/Visitors/AstVisitorBase.cs index 024d5211e..946e979cc 100644 --- a/src/SharpTS/Parsing/Visitors/AstVisitorBase.cs +++ b/src/SharpTS/Parsing/Visitors/AstVisitorBase.cs @@ -41,6 +41,7 @@ public virtual void Visit(Expr expr) case Expr.Get e: VisitGet(e); break; case Expr.Set e: VisitSet(e); break; case Expr.GetPrivate e: VisitGetPrivate(e); break; + case Expr.PrivateIn e: VisitPrivateIn(e); break; case Expr.SetPrivate e: VisitSetPrivate(e); break; case Expr.CallPrivate e: VisitCallPrivate(e); break; case Expr.This e: VisitThis(e); break; @@ -233,6 +234,8 @@ protected virtual void VisitGetPrivate(Expr.GetPrivate expr) Visit(expr.Object); } + protected virtual void VisitPrivateIn(Expr.PrivateIn expr) => Visit(expr.Object); + protected virtual void VisitSetPrivate(Expr.SetPrivate expr) { Visit(expr.Object); diff --git a/src/SharpTS/Runtime/RuntimeEnvironment.cs b/src/SharpTS/Runtime/RuntimeEnvironment.cs index 726574261..46a031583 100644 --- a/src/SharpTS/Runtime/RuntimeEnvironment.cs +++ b/src/SharpTS/Runtime/RuntimeEnvironment.cs @@ -31,6 +31,14 @@ internal SharpTSClass? PrivateClass set => _privateClass = value; } + internal SharpTSClass? FindPrivateClass(string name) + { + for (RuntimeEnvironment? scope = this; scope is not null; scope = scope.Enclosing) + if (scope._privateClass is { } owner && owner.DeclaresPrivateName(name)) + return owner; + return null; + } + public RuntimeEnvironment(RuntimeEnvironment? enclosing = null, bool? strictMode = null) : base(enclosing, strictMode) { diff --git a/src/SharpTS/Runtime/Types/SharpTSClass.cs b/src/SharpTS/Runtime/Types/SharpTSClass.cs index aa916512b..7c7ea36f6 100644 --- a/src/SharpTS/Runtime/Types/SharpTSClass.cs +++ b/src/SharpTS/Runtime/Types/SharpTSClass.cs @@ -1,6 +1,7 @@ using SharpTS.Execution; using SharpTS.Parsing; using SharpTS.Runtime; +using SharpTS.Runtime.Exceptions; using SharpTS.TypeSystem; using System.Collections.Frozen; using System.Runtime.CompilerServices; @@ -44,6 +45,7 @@ public class SharpTSClass( public SharpTSClass? Superclass { get; } = superclass; public bool IsAbstract { get; } = isAbstract; internal RuntimeEnvironment? InitializerEnvironment { get; set; } + internal IReadOnlyList? OrderedInstanceFields { get; set; } private readonly FrozenDictionary _methods = methods.ToFrozenDictionary(); private readonly FrozenDictionary _staticMethods = staticMethods.ToFrozenDictionary(); private readonly Dictionary _staticProperties = staticProperties; @@ -83,6 +85,7 @@ public class SharpTSClass( private readonly List _instancePrivateFields = instancePrivateFields ?? []; private readonly FrozenDictionary _privateMethods = privateMethods?.ToFrozenDictionary() ?? FrozenDictionary.Empty; private readonly Dictionary _staticPrivateFields = staticPrivateFields ?? []; + private readonly HashSet _installedStaticPrivateFields = new(StringComparer.Ordinal); private readonly FrozenDictionary _staticPrivateMethods = staticPrivateMethods?.ToFrozenDictionary() ?? FrozenDictionary.Empty; // Auto-accessor backing storage (TypeScript 4.9+) @@ -105,10 +108,7 @@ public int Arity() // Initialize instance fields with their initializers (before constructor runs) // Superclass fields are initialized first via inheritance chain - InitializeInstanceFields(interpreter, instance); - - // Initialize ES2022 private fields (not inherited, each class has its own storage) - InitializePrivateFields(interpreter, instance); + InitializeOrderedFields(interpreter, instance); // Initialize auto-accessor backing storage (before constructor runs) InitializeAutoAccessors(interpreter, instance); @@ -211,6 +211,27 @@ protected void InitializeInstanceFields(Interpreter interpreter, SharpTSInstance } } + private void InitializeOrderedFields(Interpreter interpreter, SharpTSInstance instance) + { + Superclass?.InitializeOrderedFields(interpreter, instance); + var privateFields = new Dictionary(); + _privateFieldStorage.Add(instance, privateFields); + foreach (var field in OrderedInstanceFields ?? _instanceFields.Concat(_instancePrivateFields).ToList()) + { + object? value = field.Initializer != null + ? interpreter.EvaluateClassInitializer(this, instance, field.Initializer) + : field.ComputedKey != null || field.IsPrivate ? SharpTSUndefined.Instance : null; + if (field.IsPrivate) privateFields[field.Name.Lexeme] = value; + else if (field.ComputedKey != null) + { + var key = interpreter.EvaluateClassInitializer(this, instance, field.ComputedKey); + if (key is SharpTSSymbol symbol) instance.SetBySymbol(symbol, value); + else instance.SetRawField(key?.ToString() ?? "undefined", value); + } + else instance.SetRawField(field.Name.Lexeme, value); + } + } + public ISharpTSCallable? FindMethod(string name) { // Check cache first @@ -502,6 +523,16 @@ private void BindPrivateMemberOwner(ISharpTSCallable method) internal bool HasPrivateBrand(object receiver) => _privateFieldStorage.TryGetValue(receiver, out _); + internal bool DeclaresPrivateName(string name) + => _instancePrivateFields.Any(field => field.Name.Lexeme == name) + || _privateMethods.ContainsKey(name) || _staticPrivateFields.ContainsKey(name) + || _staticPrivateMethods.ContainsKey(name); + + internal bool HasInstalledPrivateField(object receiver, string name) + => _privateFieldStorage.TryGetValue(receiver, out var fields) && fields.ContainsKey(name); + + internal bool HasInstalledStaticPrivateField(string name) => _installedStaticPrivateFields.Contains(name); + /// /// Initializes private fields for an instance. Each class in the hierarchy /// has its own private field storage - private fields are NOT inherited. @@ -513,6 +544,7 @@ protected void InitializePrivateFields(Interpreter interpreter, SharpTSInstance Superclass?.InitializePrivateFields(interpreter, instance); var fields = new Dictionary(); + _privateFieldStorage.Add(instance, fields); foreach (var field in _instancePrivateFields) { object? value = field.Initializer != null @@ -521,8 +553,6 @@ protected void InitializePrivateFields(Interpreter interpreter, SharpTSInstance fields[field.Name.Lexeme] = value; } - // Add to ConditionalWeakTable - GC-friendly storage - _privateFieldStorage.Add(instance, fields); } /// @@ -582,8 +612,16 @@ public void SetPrivateField(object instance, string name, object? value) /// Sets a static private field value. /// public void SetStaticPrivateField(string name, object? value) + { + if (!_installedStaticPrivateFields.Contains(name)) + throw new ThrowException(new SharpTSTypeError($"Cannot access private member {name} before initialization")); + _staticPrivateFields[name] = value; + } + + internal void InstallStaticPrivateField(string name, object? value) { _staticPrivateFields[name] = value; + _installedStaticPrivateFields.Add(name); } /// diff --git a/src/SharpTS/TypeSystem/ControlFlow/LoopAssignmentAnalyzer.cs b/src/SharpTS/TypeSystem/ControlFlow/LoopAssignmentAnalyzer.cs index 959b1bbe0..0534fc9ca 100644 --- a/src/SharpTS/TypeSystem/ControlFlow/LoopAssignmentAnalyzer.cs +++ b/src/SharpTS/TypeSystem/ControlFlow/LoopAssignmentAnalyzer.cs @@ -274,6 +274,9 @@ private static void CollectAssignedPathsFromExpr(Expr expr, HashSet VisitGet(e), Expr.Set e => VisitSet(e), Expr.GetPrivate e => VisitGetPrivate(e), + Expr.PrivateIn e => VisitPrivateIn(e), Expr.SetPrivate e => VisitSetPrivate(e), Expr.CallPrivate e => VisitCallPrivate(e), Expr.This e => VisitThis(e), diff --git a/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs b/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs index a68d5ea05..a0276dc78 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Expressions.cs @@ -2727,6 +2727,7 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) TypeEnvironment prevEnv = _environment; TypeInfo.Class? prevClass = _currentClass; _environment = classEnv; + _privateInEnclosingClasses.Push(prevClass); _currentClass = classTypeForBody is TypeInfo.Class c ? c : mutableClass.Freeze(); // Set when a method's inferred (un-annotated) return type is resolved during the body @@ -2994,6 +2995,7 @@ TypeInfo.Function BuildMethodFuncType(Stmt.Function method) finally { _environment = prevEnv; + _privateInEnclosingClasses.Pop(); _currentClass = prevClass; } diff --git a/src/SharpTS/TypeSystem/TypeChecker.PrivateIn.cs b/src/SharpTS/TypeSystem/TypeChecker.PrivateIn.cs new file mode 100644 index 000000000..828b542b4 --- /dev/null +++ b/src/SharpTS/TypeSystem/TypeChecker.PrivateIn.cs @@ -0,0 +1,39 @@ +using SharpTS.Parsing; +using SharpTS.TypeSystem.Exceptions; + +namespace SharpTS.TypeSystem; + +public partial class TypeChecker +{ + private readonly Stack _privateInEnclosingClasses = []; + + internal TypeInfo VisitPrivateIn(Expr.PrivateIn expr) + { + if (_currentClass == null) + throw new TypeCheckException("Private identifiers are not allowed outside class bodies.", tsCode: "TS18016"); + string name = expr.Name.Lexeme; + var owner = new[] { _currentClass }.Concat(_privateInEnclosingClasses).FirstOrDefault(candidate => candidate is not null + && (candidate.PrivateFieldTypes.ContainsKey(name) || candidate.StaticPrivateFieldTypes.ContainsKey(name) + || candidate.PrivateMethodTypes.ContainsKey(name) || candidate.StaticPrivateMethodTypes.ContainsKey(name))); + if (owner is null) + throw new TypeCheckException($"Private name '{name}' does not exist on class '{_currentClass.Name}'.", tsCode: "TS2339"); + _typeMap.SetPrivateInOwner(expr.Name, owner); + var type = CheckExpr(expr.Object); + if (type is TypeInfo.Unknown) + throw new TypeCheckException("Object is of type 'unknown'.", tsCode: "TS18046"); + if (!IsAnyPermissive(type) && IsInvalidPrivateInReceiver(type)) + throw new TypeCheckException($"Type '{type}' is not assignable to type 'object'.", tsCode: "TS2322"); + return TypeInfo.Primitive.Boolean; + } + + private bool IsInvalidPrivateInReceiver(TypeInfo type) => type switch + { + TypeInfo.Primitive or TypeInfo.String or TypeInfo.NumberLiteral or TypeInfo.StringLiteral + or TypeInfo.BooleanLiteral or TypeInfo.Null or TypeInfo.Undefined + or TypeInfo.BigInt or TypeInfo.Symbol or TypeInfo.UniqueSymbol or TypeInfo.Void or TypeInfo.Unknown => true, + TypeInfo.TypeParameter parameter => ApparentTypeOf(parameter) is not { } constraint + || constraint is TypeInfo.Any || IsInvalidPrivateInReceiver(constraint), + TypeInfo.Union union => union.Types.Any(IsInvalidPrivateInReceiver), + _ => false, + }; +} diff --git a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.Stages.cs b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.Stages.cs index 0ae9f2e61..55944125a 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.Stages.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.Stages.cs @@ -282,34 +282,44 @@ private bool CheckClassStaticFieldInitializers( TypeInfo.MutableClass mutableClass) { bool anyInferredFieldTypeResolved = false; - - // Second pass: check static property initializers at class scope - foreach (var field in classStmt.Fields) + var previousClass = _currentClass; + _privateInEnclosingClasses.Push(previousClass); + _currentClass = classTypeForBody; + try { - if (field.IsStatic && field.Initializer != null) + + // Static private names are visible before their fields are installed. + foreach (var field in classStmt.Fields) { - TypeInfo initType = CheckExpr(field.Initializer); - // For ES2022 private static fields, look in StaticPrivateFieldTypes - TypeInfo staticFieldDeclaredType = field.IsPrivate - ? classTypeForBody.StaticPrivateFieldTypes[GetFieldMemberName(field)] - : classTypeForBody.StaticProperties[GetFieldMemberName(field)]; - if (field.TypeAnnotation is null) - { - TypeInfo inferredFieldType = WidenLiteralType(initType); - if (field.IsPrivate) - mutableClass.StaticPrivateFields[GetFieldMemberName(field)] = inferredFieldType; - else - mutableClass.StaticProperties[GetFieldMemberName(field)] = inferredFieldType; - anyInferredFieldTypeResolved = true; - continue; - } - if (!IsCompatible(staticFieldDeclaredType, initType)) + if (field.IsStatic && field.Initializer != null) { - throw new TypeCheckException($" Cannot assign type '{initType}' to static property '{field.Name.Lexeme}' of type '{staticFieldDeclaredType}'.", tsCode: "TS2322"); + TypeInfo initType = CheckExpr(field.Initializer); + TypeInfo staticFieldDeclaredType = field.IsPrivate + ? classTypeForBody.StaticPrivateFieldTypes[GetFieldMemberName(field)] + : classTypeForBody.StaticProperties[GetFieldMemberName(field)]; + if (field.TypeAnnotation is null) + { + TypeInfo inferredFieldType = WidenLiteralType(initType); + if (field.IsPrivate) + mutableClass.StaticPrivateFields[GetFieldMemberName(field)] = inferredFieldType; + else + mutableClass.StaticProperties[GetFieldMemberName(field)] = inferredFieldType; + anyInferredFieldTypeResolved = true; + continue; + } + if (!IsCompatible(staticFieldDeclaredType, initType)) + { + throw new TypeCheckException($" Cannot assign type '{initType}' to static property '{field.Name.Lexeme}' of type '{staticFieldDeclaredType}'.", tsCode: "TS2322"); + } } } + return anyInferredFieldTypeResolved; + } + finally + { + _privateInEnclosingClasses.Pop(); + _currentClass = previousClass; } - return anyInferredFieldTypeResolved; } // Checks initializers at declaration scope before static blocks, preserving diagnostic order. diff --git a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs index e3f542dad..5f4af520e 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.Statements.Classes.cs @@ -537,6 +537,7 @@ private void CheckClassStaticBlocks(Stmt.Class classStmt, TypeInfo.Class classTy var previousClass = _currentClass; _inStaticBlock = true; _inStaticMethod = true; + _privateInEnclosingClasses.Push(previousClass); _currentClass = classTypeForBody; try @@ -550,6 +551,7 @@ private void CheckClassStaticBlocks(Stmt.Class classStmt, TypeInfo.Class classTy { _inStaticBlock = previousInStaticBlock; _inStaticMethod = previousInStaticMethod; + _privateInEnclosingClasses.Pop(); _currentClass = previousClass; } } @@ -637,6 +639,7 @@ TypeInfo.MutableClass preregisteredClass TypeInfo.Class? prevClass = _currentClass; _environment = classEnv; + _privateInEnclosingClasses.Push(prevClass); _currentClass = classTypeForBody; // Set when a method's inferred (un-annotated) return type is resolved during the body @@ -655,6 +658,7 @@ TypeInfo.MutableClass preregisteredClass finally { _environment = prevEnv; + _privateInEnclosingClasses.Pop(); _currentClass = prevClass; } diff --git a/src/SharpTS/TypeSystem/TypeChecker.StrictPropertyInitialization.cs b/src/SharpTS/TypeSystem/TypeChecker.StrictPropertyInitialization.cs index 019417887..ae0e16174 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.StrictPropertyInitialization.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.StrictPropertyInitialization.cs @@ -206,6 +206,9 @@ private HashSet AnalyzeInitializationExpression( case Expr.Grouping grouping: assigned = AnalyzeInitializationExpression(grouping.Expression, assigned); break; + case Expr.PrivateIn presence: + assigned = AnalyzeInitializationExpression(presence.Object, assigned); + break; case Expr.Ternary ternary: { var afterCondition = AnalyzeInitializationExpression( diff --git a/src/SharpTS/TypeSystem/TypeChecker.cs b/src/SharpTS/TypeSystem/TypeChecker.cs index 251f7e969..c28d414db 100644 --- a/src/SharpTS/TypeSystem/TypeChecker.cs +++ b/src/SharpTS/TypeSystem/TypeChecker.cs @@ -3055,6 +3055,7 @@ private TypeInfo GetDeclaredType(Stmt decl) Expr.ImportMeta im => im.Keyword.Line, Expr.Set s => s.Name.Line, Expr.Get g => g.Name.Line, + Expr.PrivateIn presence => presence.Name.Line, Expr.Assign a => a.Name.Line, Expr.Binary b => b.Operator.Line, Expr.Logical l => l.Operator.Line, diff --git a/src/SharpTS/TypeSystem/TypeMap.cs b/src/SharpTS/TypeSystem/TypeMap.cs index f2d7cc65c..ff9482fb9 100644 --- a/src/SharpTS/TypeSystem/TypeMap.cs +++ b/src/SharpTS/TypeSystem/TypeMap.cs @@ -14,6 +14,10 @@ namespace SharpTS.TypeSystem; public class TypeMap { private readonly Dictionary _types = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary _privateInOwners = new(ReferenceEqualityComparer.Instance); + + internal void SetPrivateInOwner(Token name, TypeInfo.Class owner) => _privateInOwners[name] = owner; + internal TypeInfo.Class? GetPrivateInOwner(Token name) => _privateInOwners.GetValueOrDefault(name); private readonly Dictionary _classTypes = new(StringComparer.Ordinal); private readonly Dictionary _functionTypes = new(StringComparer.Ordinal); private readonly Dictionary _classExprTypes = new(ReferenceEqualityComparer.Instance); diff --git a/tests/SharpTS.Tests/ParserTests/PrivateInParserTests.cs b/tests/SharpTS.Tests/ParserTests/PrivateInParserTests.cs new file mode 100644 index 000000000..9612b9b85 --- /dev/null +++ b/tests/SharpTS.Tests/ParserTests/PrivateInParserTests.cs @@ -0,0 +1,252 @@ +using SharpTS.Parsing; +using SharpTS.Tests.Infrastructure; +using SharpTS.TypeSystem; +using SharpTS.TypeSystem.Exceptions; +using Xunit; + +namespace SharpTS.Tests.ParserTests; + +public sealed class PrivateInParserTests +{ + private static Expr ParseExpression(string source) + => Assert.IsType(Assert.Single(TestHarness.ParseOrThrow(source))).Expr; + + [Fact] + public void BarePrivateNameFormsAPresenceExpression() + { + var expression = Assert.IsType(ParseExpression("#value in object;")); + Assert.Equal("#value", expression.Name.Lexeme); + Assert.Equal("object", Assert.IsType(expression.Object).Name.Lexeme); + } + + [Fact] + public void PresenceRightOperandIncludesShiftAndArithmeticExpressions() + { + var expression = Assert.IsType(ParseExpression("#value in object + other << 1;")); + var shift = Assert.IsType(expression.Object); + Assert.Equal(TokenType.LESS_LESS, shift.Operator.Type); + Assert.Equal(TokenType.PLUS, Assert.IsType(shift.Left).Operator.Type); + } + + [Fact] + public void PresenceBindsBeforeEqualityAndLogicalOperators() + { + var logical = Assert.IsType(ParseExpression("#value in object === true && enabled;")); + Assert.Equal(TokenType.AND_AND, logical.Operator.Type); + var equality = Assert.IsType(logical.Left); + Assert.Equal(TokenType.EQUAL_EQUAL_EQUAL, equality.Operator.Type); + Assert.IsType(equality.Left); + } + + [Theory] + [InlineData("#value in object in other;", TokenType.IN)] + [InlineData("#value in object < other;", TokenType.LESS)] + [InlineData("#value in object instanceof Other;", TokenType.INSTANCEOF)] + public void FollowingRelationalOperatorsAssociateToTheLeft(string source, TokenType operation) + { + var expression = Assert.IsType(ParseExpression(source)); + Assert.Equal(operation, expression.Operator.Type); + Assert.IsType(expression.Left); + } + + [Fact] + public void PrivateNameAndInMayBeSeparatedByALineTerminator() + => Assert.IsType(ParseExpression("#value\nin object;")); + + [Theory] + [InlineData("#value;")] + [InlineData("(#value) in object;")] + [InlineData("!#value in object;")] + [InlineData("delete #value in object;")] + [InlineData("#value + 1;")] + [InlineData("#value === object;")] + [InlineData("object in #value;")] + [InlineData("object < #value in object;")] + [InlineData("#value in #other in object;")] + [InlineData("#value in;")] + [InlineData("#value in object = true;")] + [InlineData("(#value in object) = true;")] + [InlineData("#value in object += true;")] + public void PrivateNamesAreRejectedOutsideTheBarePresenceProduction(string expression) + { + var source = $$""" + class Owner { + #value: number = 1; + #other: number = 2; + check(object: any): void { {{expression}} } + } + """; + var result = new Parser(new Lexer(source).ScanTokens()).Parse(); + Assert.False(result.IsSuccess); + Assert.NotEmpty(result.Diagnostics); + } + + [Theory] + [InlineData("for (#value in object) {}")] + [InlineData("for (let #value in object) {}")] + public void PrivatePresenceCannotBeAForInBinding(string statement) + { + var source = $$""" + class Owner { + #value: number = 1; + check(object: any): void { {{statement}} } + } + """; + Assert.False(new Parser(new Lexer(source).ScanTokens()).Parse().IsSuccess); + } + + [Fact] + public void ParenthesizedPresenceCanInitializeATraditionalForLoop() + { + var source = """ + class Owner { + #value: number = 1; + check(object: any): void { + for (let present = (#value in object); present; present = false) {} + } + } + """; + Assert.Single(TestHarness.ParseOrThrow(source)); + } + + [Theory] + [InlineData("for (let present = #value in object; ; ) {}")] + [InlineData("for (var present = true, other = #value in object; ; ) {}")] + [InlineData("for (#value in object; ; ) {}")] + [InlineData("for (present = #value in object; ; ) {}")] + [InlineData("for (present = false, #value in object; ; ) {}")] + [InlineData("for (let present = true && #value in object; ; ) {}")] + [InlineData("for (let present = true ? false : #value in object; ; ) {}")] + [InlineData("for (let present = #value in object ? true : false; ; ) {}")] + [InlineData("for (let present = () => #value in object; ; ) {}")] + [InlineData("for ([present] = #value in object; ; ) {}")] + public void UnparenthesizedPresenceIsRejectedInNoInInitializerPositions(string statement) + { + var source = $$""" + class Owner { + #value: number = 1; + check(object: any): void { let present: any; {{statement}} } + } + """; + var result = new Parser(new Lexer(source).ScanTokens()).Parse(); + Assert.False(result.IsSuccess); + Assert.Contains(result.Diagnostics, diagnostic => diagnostic.Message.Contains("must be parenthesized")); + } + + [Theory] + [InlineData("for (let present = (#value in object); ; ) {}")] + [InlineData("for (let present = consume(#value in object); ; ) {}")] + [InlineData("for (let present = [#value in object]; ; ) {}")] + [InlineData("for (let present = { value: #value in object }; ; ) {}")] + [InlineData("for (let present = object[#value in object]; ; ) {}")] + [InlineData("for (let present = new Other(#value in object); ; ) {}")] + [InlineData("for (let present = true ? #value in object : false; ; ) {}")] + [InlineData("for (let present = `${#value in object}`; ; ) {}")] + [InlineData("for (let present = () => (#value in object); ; ) {}")] + [InlineData("for (let present = () => { return #value in object; }; ; ) {}")] + [InlineData("for ([present = #value in object] = array; ; ) {}")] + [InlineData("for (let present = true; #value in object; ) {}")] + [InlineData("for (let present = true; ; present = #value in object) {}")] + [InlineData("for (let key in (#value in object ? object : {})) {}")] + [InlineData("for (let item of (#value in object ? array : [])) {}")] + public void NestedInEnabledExpressionsAndOtherForClausesRemainAllowed(string statement) + { + var source = $$""" + class Owner { + #value: number = 1; + check(object: any): void { let present: any; {{statement}} } + } + """; + Assert.Single(TestHarness.ParseOrThrow(source)); + } + + [Fact] + public void ExistingOrdinaryInInitializerParsingIsPreserved() + => Assert.Single(TestHarness.ParseOrThrow("for (let present = 'value' in object; ; ) {}")); + + [Fact] + public void LexicalPrivateNamesOutsideClassBodiesAreRejected() + { + var exception = Assert.ThrowsAny(() => + new TypeChecker().Check(TestHarness.ParseOrThrow("#value in {};"))); + Assert.Equal("TS18016", exception.Diagnostic.TsCode); + } + + [Fact] + public void NestedClassDeclarationsCanResolveLexicalOuterPrivateNames() + => new TypeChecker().Check(TestHarness.ParseOrThrow(""" + class Outer { + #outer = 1; + make() { class Inner { has(value: any) { return #outer in value; } } return Inner; } + } + """)); + + [Theory] + [InlineData("class Owner { check(object: any): boolean { return #missing in object; } }")] + [InlineData("class Owner { value: number = 1; check(object: any): boolean { return #value in object; } }")] + [InlineData("class Base { #value: number = 1; } class Owner extends Base { check(object: any): boolean { return #value in object; } }")] + public void UndeclaredPrivateNamesCannotBeSynthesizedFromPublicOrInheritedNames(string source) + { + var exception = Assert.ThrowsAny(() => + new TypeChecker().Check(TestHarness.ParseOrThrow(source))); + Assert.Equal("TS2339", exception.Diagnostic.TsCode); + } + + [Theory] + [InlineData("number")] + [InlineData("string")] + [InlineData("boolean")] + [InlineData("bigint")] + [InlineData("symbol")] + [InlineData("null")] + [InlineData("undefined")] + [InlineData("number | object")] + public void PrimitivePresenceReceiversAreRejected(string type) + { + var source = $$""" + class Owner { + #value: number = 1; + check(object: {{type}}): boolean { return #value in object; } + } + """; + var exception = Assert.ThrowsAny(() => + new TypeChecker().Check(TestHarness.ParseOrThrow(source))); + Assert.Equal("TS2322", exception.Diagnostic.TsCode); + } + + [Theory] + [InlineData("", "TS2322")] + [InlineData(" extends any", "TS2322")] + [InlineData(" extends number", "TS2322")] + [InlineData(" extends number | object", "TS2322")] + public void GenericPresenceReceiversMustHaveObjectCompatibleConstraints(string constraint, string expectedCode) + { + var source = $$""" + class Owner { + #value = 1; + has(value: T) { return #value in value; } + } + """; + var exception = Assert.ThrowsAny(() => + new TypeChecker().Check(TestHarness.ParseOrThrow(source))); + Assert.Equal(expectedCode, exception.Diagnostic.TsCode); + } + + [Fact] + public void UnknownPresenceReceiversAreRejected() + { + var exception = Assert.ThrowsAny(() => + new TypeChecker().Check(TestHarness.ParseOrThrow(""" + class Owner { #value = 1; has(value: unknown) { return #value in value; } } + """))); + Assert.Equal("TS18046", exception.Diagnostic.TsCode); + } + + [Theory] + [InlineData("object")] + [InlineData("{}")] + public void ObjectCompatibleGenericConstraintsAreAllowed(string constraint) + => new TypeChecker().Check(TestHarness.ParseOrThrow($$""" + class Owner { #value = 1; has(value: T) { return #value in value; } } + """)); +} diff --git a/tests/SharpTS.Tests/SharedTests/PrivateInTests.cs b/tests/SharpTS.Tests/SharedTests/PrivateInTests.cs new file mode 100644 index 000000000..3987b127c --- /dev/null +++ b/tests/SharpTS.Tests/SharedTests/PrivateInTests.cs @@ -0,0 +1,230 @@ +using SharpTS.Tests.Infrastructure; +using SharpTS.TypeSystem.Exceptions; +using Xunit; + +namespace SharpTS.Tests.SharedTests; + +public sealed class PrivateInTests +{ + public static IEnumerable Cases() + { + yield return ["instance-brand", """ + class Base { #field = 1; #method() {} static has(value: any) { console.log(#field in value, #method in value); } } + class Derived extends Base { #field = 2; } + class Other { #field = 3; } + const value = new Base(); + Base.has(value); Base.has(new Derived()); Base.has(new Other()); + Base.has({ '#field': 1 }); Base.has(new Proxy(value, {})); + """, "true true\ntrue true\nfalse false\nfalse false\nfalse false\n"]; + yield return ["static-brand", """ + class Box { static #field = 1; static #method() {} static has(value: any) { console.log(#field in value, #method in value); } } + class Derived extends Box {} + const Alias = Box; Box.has(Box); Box.has(Alias); Box.has(Derived); Box.has(new Box()); Box.has(() => {}); + """, "true true\ntrue true\nfalse false\nfalse false\nfalse false\n"]; + yield return ["operand-once-and-primitives", """ + class Box { #field = 1; static has(value: any) { return #field in value; } } + let count = 0; + function next() { count++; return new Box(); } + console.log(Box.has(next()), count); + let errors = 0; + for (const value of [null, undefined, 0, false, "", 1n, Symbol("x")]) { + try { Box.has(value); } catch (error) { if (error instanceof TypeError) errors++; } + } + console.log(errors); + """, "true 1\n7\n"]; + yield return ["instance-initialization-order", """ + class Box { + first = console.log(#later in this, #early in this, #method in this); + #early = console.log(#later in this, #early in this, #method in this); + middle = console.log(#later in this, #early in this); + #later; + #method() {} + last = console.log(#later in this, #early in this); + } + new Box(); + """, "false false true\nfalse false true\nfalse true\ntrue true\n"]; + yield return ["static-initialization-order", """ + class Box { + static first = console.log(#later in this, #method in this); + static #early = console.log(#early in this, #later in this); + static middle = console.log(#early in this, #later in this); + static #later; + static #method() {} + static last = console.log(#early in this, #later in this); + } + """, "false true\nfalse false\ntrue false\ntrue true\n"]; + yield return ["premature-static-private-writes", """ + let calls = 0; + function next() { calls++; return 7; } + class Box { + static first = Box.probe(); + static #later = 2; + static probe() { + let threw = false; + try { this.#later = next(); } catch (error) { threw = error instanceof TypeError; } + console.log(threw, #later in this, calls); + try { this.#later = (() => { throw new Error("rhs"); })(); } + catch (error) { console.log(error.message); } + } + static after() { + const value = this.#later = next(); + console.log(value, this.#later, #later in this, calls); + } + } + Box.after(); + """, "true false 1\nrhs\n7 7 true 2\n"]; + yield return ["fresh-premature-static-private-writes", """ + let calls = 0; + function next() { calls++; return 7; } + function make(value: number) { return class Named { + static first = Named.probe(); + static #later = value; + static probe() { + let threw = false; + try { this.#later = next(); } catch (error) { threw = error instanceof TypeError; } + console.log(threw, #later in this, calls); + } + static after() { + console.log(this.#later, #later in this); + this.#later = next(); + console.log(this.#later, #later in this, calls); + } + }; } + const First = make(3), Second = make(5); + First.after(); Second.after(); + """, "true false 1\ntrue false 2\n3 true\n7 true 3\n5 true\n7 true 4\n"]; + yield return ["fresh-definition-brands", """ + function make() { return class { + #field = 1; #method() {} + static #value = 2; static #staticMethod() {} + static has(object: any) { return #value in object && #staticMethod in object; } + has(object: any) { return #field in object && #method in object; } + }; } + const First = make(), Second = make(), one = new First(), two = new Second(); + console.log(one.has(one), one.has(two), two.has(two), two.has(one)); + console.log(First.has(First), First.has(Second), Second.has(Second), Second.has(First)); + """, "true false true false\ntrue false true false\n"]; + yield return ["closures-and-suspension", """ + class Box { + #field = 1; + probe() { return (object: any) => #field in object; } + async later(object: any) { return #field in await new Promise(resolve => setTimeout(() => resolve(object), 1)); } + *values(object: any) { yield #field in object; return #field in (yield object); } + async *delayed(object: any) { yield #field in await Promise.resolve(object); } + } + async function run() { + const box = new Box(), other = new Box(), probe = box.probe(); + console.log(probe(box), probe(other), probe({})); + console.log(await box.later(box), await box.later({})); + const it = box.values(box); console.log(it.next().value); it.next(); console.log(it.next({}).value); + console.log((await box.delayed(box).next()).value); + } + run(); + """, "true true false\ntrue false\ntrue\nfalse\ntrue\n"]; + yield return ["expression-field-order", """ + const Box = class { + first = console.log(#field in this, #method in this); + #field = console.log(#field in this); + #method() {} + last = console.log(#field in this); + }; + new Box(); + """, "false true\nfalse\ntrue\n"]; + yield return ["nested-lexical-names-and-shadowing", """ + class Outer { + #outer = 1; #method() {} #same = 2; static #static = 3; + make() { return class Inner { + #same() {} + has(value: any) { return #outer in value && #method in value; } + shadows(value: any) { return #same in value; } + hasStatic(value: any) { return #static in value; } + }; } + } + const outer = new Outer(), Inner = outer.make(), inner = new Inner(); + console.log(inner.has(outer), inner.has(inner), inner.shadows(outer), inner.shadows(inner)); + console.log(inner.hasStatic(Outer), inner.hasStatic(Inner)); + """, "true false false true\ntrue false\n"]; + yield return ["nested-fresh-lexical-owners", """ + function make() { return class Outer { + #outer = 1; static #static = 2; + make() { return class Middle { + make() { return class Inner { + has(value: any) { return #outer in value; } + hasStatic(value: any) { return #static in value; } + }; } + }; } + }; } + const First = make(), Second = make(), first = new First(), second = new Second(); + const Middle = first.make(), Inner = new Middle().make(), inner = new Inner(); + console.log(inner.has(first), inner.has(second)); + console.log(inner.hasStatic(First), inner.hasStatic(Second)); + """, "true false\ntrue false\n"]; + yield return ["derived-expression-private-brands", """ + function make() { return class Base { + #field = 1; #method() {} + has(value: any) { return #field in value && #method in value; } + }; } + const First = make(), Second = make(); + const Child = class extends First {}; + const child = new Child(); + console.log(child.has(child), new First().has(child), new Second().has(child)); + """, "true true false\n"]; + yield return ["generic-static-closure-and-suspension", """ + class Box { + static #field = 1; static #method() {} + static probe() { return (value: any) => #field in value && #method in value; } + static async later(value: any) { return #field in await Promise.resolve(value); } + static *values(value: any) { yield #field in value; return #method in (yield value); } + static async *delayed(value: any) { yield #field in await Promise.resolve(value); } + } + class Child extends Box {} + async function run() { + const probe = Box.probe(); console.log(probe(Box), probe(Child), probe({})); + console.log(await Box.later(Box), await Box.later(Child)); + const it = Box.values(Box); console.log(it.next().value); it.next(); console.log(it.next(Child).value); + console.log((await Box.delayed(Box).next()).value); + } + run(); + """, "true false false\ntrue false\ntrue\nfalse\ntrue\n"]; + } + + [Theory, MemberData(nameof(Cases))] + public void BrandChecksMatchAcrossEngines(string name, string source, string expected) + { + Assert.NotEmpty(name); + Assert.Equal(expected, TestHarness.RunInterpreted(source)); + Assert.Equal(expected, TestHarness.RunCompiled(source)); + var (errors, output) = TestHarness.CompileVerifyAndRun(source); + Assert.Empty(errors); + Assert.Equal(expected, output); + // Proxy explicitly requires the deployed runtime; verification/run above + // supplies it. The other cases also enforce dependency-free deployment. + if (name != "instance-brand") Assert.Equal(expected, TestHarness.RunCompiledStandalone(source)); + } + + [Theory, ModeData] + public void ModuleNamesKeepTheirLexicalBrands(ExecutionMode mode) + { + var files = new Dictionary + { + ["left.ts"] = "export class Box { #field = 1; static has(x: any) { return #field in x; } }", + ["right.ts"] = "export class Box { #field = 1; static has(x: any) { return #field in x; } }", + ["main.ts"] = """ + import { Box as Left } from './left'; import { Box as Right } from './right'; + console.log(Left.has(new Left()), Left.has(new Right()), Right.has(new Right())); + """ + }; + Assert.Equal("true false true\n", TestHarness.RunModules(files, "main.ts", mode)); + if (mode == ExecutionMode.Compiled) Assert.Empty(TestHarness.CompileModulesAndVerifyOnly(files, "main.ts")); + } + + [Theory] + [InlineData("class Box { #field = 1; has() { return #field in 1; } }")] + [InlineData("class Box { has(x: any) { return #missing in x; } }")] + [InlineData("const x = #field in {};")] + public void InvalidNamesOrPrimitiveTypesAreRejected(string source) + { + Assert.Throws(() => TestHarness.RunInterpreted(source)); + Assert.Throws(() => TestHarness.RunCompiled(source)); + } +} diff --git a/tests/fixtures/PrivateIn/README.md b/tests/fixtures/PrivateIn/README.md index a97e0fd7e..20eb156a4 100644 --- a/tests/fixtures/PrivateIn/README.md +++ b/tests/fixtures/PrivateIn/README.md @@ -1,8 +1,12 @@ -# Private-in investigation controls (#1903) +# Private-in regression controls (#1962) These new sources are not recovered historical reproducers. The implementation design and recorded evidence are in `docs/plans/private-in-brand-checks.md`. Implementation is tracked by [#1962](https://github.com/nickna/SharpTS/issues/1962). +The positive controls are now exercised by `PrivateInTests` in both engines, +including serialized IL verification. CLI verification and execution use a +30-second deadline for each process. The proxy control deploys `SharpTS.dll`; +the other positive controls also run without it. Node v25.5.0 and TypeScript 7.0.2 accept the three positive sources. The two brand files each print `true true` twice followed by `false false` three times.