From fa90e0b2158863ed6ccbe9d32a1f97a1437551af Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 31 May 2026 21:39:04 +0300 Subject: [PATCH 1/5] implement --- .../GenerateCS/CSInteropTest.cs | 26 +++++ .../GenerateJS/DeclarationTest.cs | 22 +++++ .../GenerateJS/JSModuleTest.cs | 96 ++++++++++++++++--- .../Common/Global/GlobalInspection.cs | 11 ++- .../Common/Global/GlobalType.cs | 4 + .../Common/Inspection/GenericDisambiguator.cs | 74 ++++++++++++++ .../Common/Inspection/Meta/MemberMeta.cs | 4 + .../Inspection/OverloadDisambiguator.cs | 18 ++-- .../Common/Inspection/SerializedInspector.cs | 2 +- .../Common/Inspection/TypeInspector.cs | 34 +++++-- .../GenerateCS/CSInstanceGenerator.cs | 6 +- .../GenerateCS/CSInteropGenerator.cs | 10 +- .../GenerateCS/CSModuleGenerator.cs | 4 +- .../Declarations/DeclarationGenerator.cs | 2 +- .../GenerateJS/JSModuleGenerator.cs | 2 +- .../GenerateJS/JSModules/JSModules.cs | 2 +- src/cs/Directory.Build.props | 2 +- src/js/test/cs/Test/Static.cs | 22 +++++ src/js/test/spec/interop.spec.ts | 15 +++ 19 files changed, 310 insertions(+), 46 deletions(-) create mode 100644 src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs index ef5ab048..e85a1532 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs @@ -388,6 +388,32 @@ public class Class DoesNotContain("Foo"); } + [Fact] + public void GeneratesSupportedGenericMethods () + { + AddAssembly(With( + """ + public interface IShape {} + public class Circle : IShape { public double Radius { get; set; } } + public class Square : IShape { public double Side { get; set; } } + + public class Class + { + [Export] public static T Make () where T : IShape => default!; + [Export] public static void Take (T shape) where T : IShape {} + [Export] public static void Mix (T shape, int n) where T : IShape {} + [Export] public static T? EchoNull (T? shape) where T : class, IShape => shape; + } + """)); + Execute(); + Contains("Class_MakeCircle () => Instances.Export(global::Class.Make())"); + Contains("Class_MakeSquare () => Instances.Export(global::Class.Make())"); + Contains("Class_TakeCircle (int shape) => global::Class.Take(Instances.Resolve(shape))"); + Contains("Class_TakeSquare (int shape) => global::Class.Take(Instances.Resolve(shape))"); + Contains("Class_MixCircle (int shape, global::System.Int32 n) => global::Class.Mix(Instances.Resolve(shape), n)"); + Contains("Class_EchoNullCircle (int shape) => Instances.Export(global::Class.EchoNull(Instances.Resolve(shape)))"); + } + [Fact] public void DoesNotSerializeTypesThatShouldNotBeSerialized () { diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs index b03c3aa3..5b7b01e0 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs @@ -542,6 +542,28 @@ export interface GenericClass2 { """); } + [Fact] + public void GeneratedForGenericMethods () + { + AddAssembly(With( + """ + public interface IShape {} + public class Circle : IShape { public double Radius { get; set; } } + public class Square : IShape { public double Side { get; set; } } + + public class Class + { + [Export] public static T Make () where T : IShape => default!; + [Export] public static void Take (T shape) where T : IShape {} + } + """)); + Execute(); + Contains("export function makeWithCircle(): Circle;"); + Contains("export function makeWithSquare(): Square;"); + Contains("export function takeWithCircle(shape: Circle): void;"); + Contains("export function takeWithSquare(shape: Square): void;"); + } + [Fact] public void GeneratesForDelegates () { diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs index a7072f6c..68e743d2 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs @@ -705,31 +705,99 @@ [Export] public static void Start (string title, string info, double progress) { """ export const Class = { foo: (a) => exports.Class_Foo(a), - fooWithA: (a) => exports.Class_Foo(a), - fooWithB: (b, a) => exports.Class_Foo(b, a), + fooWithA: (a) => exports.Class_FooWithA(a), + fooWithB: (b, a) => exports.Class_FooWithB(b, a), bar: (x) => exports.Class_Bar(x), - barWithName: (x, name) => exports.Class_Bar(x, name), + barWithName: (x, name) => exports.Class_BarWithName(x, name), baz: (x, y) => exports.Class_Baz(x, y), - bazWithXAndY: (x, y) => exports.Class_Baz(x, y), + bazWithXAndY: (x, y) => exports.Class_BazWithXAndY(x, y), qux: (a) => exports.Class_Qux(a), - quxWithB: (a, b) => exports.Class_Qux(a, b), - quxWithBAndC: (a, b, c) => exports.Class_Qux(a, b, c), + quxWithB: (a, b) => exports.Class_QuxWithB(a, b), + quxWithBAndC: (a, b, c) => exports.Class_QuxWithBAndC(a, b, c), x: (x, y) => exports.Class_X(x, y), - xWithStringAndInt32: (x, y) => exports.Class_X(x, y), - xWithInt32AndString: (x, y) => exports.Class_X(x, y), + xWithStringAndInt32: (x, y) => exports.Class_XWithStringAndInt32(x, y), + xWithInt32AndString: (x, y) => exports.Class_XWithInt32AndString(x, y), bob: (x, y, z) => exports.Class_Bob(x, y, z), - bobWithQ: (x, y, q) => exports.Class_Bob(x, y, q), + bobWithQ: (x, y, q) => exports.Class_BobWithQ(x, y, q), change: (progress) => exports.Class_Change(progress), - changeWithInfo: (info) => exports.Class_Change(info), - changeWithProgressAndInfo: (progress, info) => exports.Class_Change(progress, info), + changeWithInfo: (info) => exports.Class_ChangeWithInfo(info), + changeWithProgressAndInfo: (progress, info) => exports.Class_ChangeWithProgressAndInfo(progress, info), start: (title) => exports.Class_Start(title), - startWithInfo: (title, info) => exports.Class_Start(title, info), - startWithProgress: (title, progress) => exports.Class_Start(title, progress), - startWithInfoAndProgress: (title, info, progress) => exports.Class_Start(title, info, progress) + startWithInfo: (title, info) => exports.Class_StartWithInfo(title, info), + startWithProgress: (title, progress) => exports.Class_StartWithProgress(title, progress), + startWithInfoAndProgress: (title, info, progress) => exports.Class_StartWithInfoAndProgress(title, info, progress) }; """); } + [Fact] + public void GeneratesSupportedGenericMethods () + { + AddAssembly(With( + """ + public interface IShape {} + public class Circle : IShape { public double Radius { get; set; } } + public class Square : IShape { public double Side { get; set; } } + + public class Class + { + [Export] public static T Make () where T : IShape => default!; + [Export] public static void Take (T shape) where T : IShape {} + } + """)); + Execute(); + Contains("makeWithCircle: () =>"); + Contains("makeWithSquare: () =>"); + Contains("takeWithCircle: (shape) =>"); + Contains("takeWithSquare: (shape) =>"); + Contains("exports.Class_MakeCircle()"); + Contains("exports.Class_MakeSquare()"); + Contains("exports.Class_TakeCircle("); + Contains("exports.Class_TakeSquare("); + } + + [Fact] + public void DiscardsUnsupportedGenericMethods () + { + AddAssembly(With( + """ + using System.Collections.Generic; + public interface IShape {} + public static class Box { [Export] public static void Stored (T item) {} } + public class Class + { + [Export] public static void Pair () where T1 : IShape where T2 : IShape {} + [Export] public static void Many (List items) where T : IShape {} + [Export] public static void Free () {} + [Export] public static void Real () {} + } + """)); + Execute(); + Contains("real:"); + DoesNotContain("stored"); // method declared on a generic type + DoesNotContain("pair"); // multiple type parameters + DoesNotContain("many"); // type parameter used nested + DoesNotContain("free"); // type parameter not constrained to a user type + } + + [Fact] + public void DiscardsGeneratedTypesFromGenericCandidates () + { + AddAssembly(With( + """ + public interface IShape {} + public class Circle : IShape { public double Radius { get; set; } } + namespace Bootsharp.Generated { public class JS_Import_Leaked : global::IShape {} } + public class Class + { + [Export] public static T Make () where T : IShape => default!; + } + """)); + Execute(); + Contains("makeWithCircle:"); + DoesNotContain("makeWithJS_Import_Leaked"); + } + [Fact] public void RespectsPrefsInStatics () { diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs index 1b66c79e..f90df4a7 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs @@ -36,6 +36,7 @@ public static bool IsUserAssembly (string assemblyName) => public static bool IsUserType (Type type) { + if (type.Namespace?.StartsWith("Bootsharp.Generated") == true) return false; if (Preferences.IsSpecialized(type)) return true; if (IsDelegate(type)) return true; if (type.IsArray) return false; @@ -79,10 +80,14 @@ public bool HasBase (Type clr, [NotNullWhen(true)] out TypeMeta? bs) return bs != null && IsUserType(bs.Clr); } - public TypeMeta Get (Type clr) + public TypeMeta First (Type clr) { - var def = clr.IsGenericType ? clr.GetGenericTypeDefinition() : clr; - return types.First(t => (t.Clr.IsGenericType ? t.Clr.GetGenericTypeDefinition() : t.Clr) == def); + return types.First(t => IsSameType(t.Clr, clr)); + } + + public bool Has (Type clr) + { + return types.Any(t => IsSameType(t.Clr, clr)); } } } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index 1bb70f4e..96342fa6 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -77,6 +77,10 @@ static bool IsDictionary (Type type) => type.GetGenericTypeDefinition().FullName == typeof(IReadOnlyDictionary<,>).FullName); } + public static bool IsSameType (Type a, Type b) => + (a.IsGenericType ? a.GetGenericTypeDefinition() : a) == + (b.IsGenericType ? b.GetGenericTypeDefinition() : b); + public static NullabilityInfo GetNullity (PropertyInfo prop) => FixNullity(new NullabilityInfoContext().Create(prop), prop.CustomAttributes, prop.DeclaringType); public static NullabilityInfo GetNullity (ParameterInfo param) => diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs b/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs new file mode 100644 index 00000000..2e50149a --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs @@ -0,0 +1,74 @@ +using System.Reflection; + +namespace Bootsharp.Publish; + +/// +/// Rewrites associated with the generic methods expanding them into a concrete +/// overload for each user type compatible with the method's type parameter constraint. +/// +internal static class GenericDisambiguator +{ + private static IReadOnlyCollection types = null!; + + public static void Disambiguate (IReadOnlyCollection types) + { + GenericDisambiguator.types = types; + foreach (var surface in types.OfType()) + foreach (var method in surface.Members.OfType().ToArray()) + if (method.IK == InteropKind.Export && method.Info.ContainsGenericParameters) + Disambiguate(surface, method); + } + + private static void Disambiguate (SurfaceMeta surf, MethodMeta meth) + { + surf.MemberList.Remove(meth); + foreach (var expanded in Expand(meth)) + surf.MemberList.Add(expanded); + } + + private static IEnumerable Expand (MethodMeta meth) + { + if (!meth.Info.IsGenericMethodDefinition) yield break; // ignore methods declared on a generic type + if (meth.Info.GetGenericArguments() is not [{ } param]) yield break; // only single supported + if (param.GetGenericParameterConstraints().FirstOrDefault(IsUserType) is not { } ct) yield break; + foreach (var compatible in GetCompatible(ct)) + yield return CloseGeneric(meth, compatible); + } + + private static IEnumerable GetCompatible (Type constraint) => types + .Where(t => t is InstanceMeta && !t.Clr.IsAbstract && constraint.IsAssignableFrom(t.Clr)) + .DistinctBy(t => t.Clr); + + private static MethodMeta CloseGeneric (MethodMeta meth, TypeMeta closeType) + { + var closed = meth.Info.MakeGenericMethod(closeType.Clr); + return new MethodMeta(closed) { + Surf = meth.Surf, + IK = meth.IK, + Name = $"{meth.Name}<{closeType.Syntax}>", + Endpoint = $"{meth.Name}{closeType.Id}", + JSName = $"{meth.JSName}With{closeType.Clr.Name}", + Args = meth.Args.Zip(closed.GetParameters(), RewriteArg).ToArray(), + Return = meth.Info.ReturnType.IsGenericMethodParameter ? RewriteValue(meth.Return) : meth.Return, + Void = meth.Void, + Async = meth.Async + }; + + ArgumentMeta RewriteArg (ArgumentMeta arg, ParameterInfo param) + { + if (!arg.Info.ParameterType.IsGenericMethodParameter) return arg; + var value = RewriteValue(arg.Value); + return new ArgumentMeta(param) { Name = arg.Name, JSName = arg.JSName, Value = value }; + } + + ValueMeta RewriteValue (ValueMeta value) => value with { + Type = closeType, + TypeSyntax = value.Nullable ? $"{closeType.Syntax}?" : closeType.Syntax + }; + } + + extension (SurfaceMeta srf) + { + private IList MemberList => (IList)srf.Members; + } +} diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/Meta/MemberMeta.cs b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/MemberMeta.cs index aaf5fbe3..3afa5677 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/Meta/MemberMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/MemberMeta.cs @@ -41,6 +41,10 @@ internal record MethodMeta (MethodInfo Info) : MemberMeta /// public override MethodInfo Info { get; } = Info; /// + /// Identifier of the generated interop endpoint. + /// + public required string Endpoint { get; init; } + /// /// Arguments of the method. /// public required IReadOnlyList Args { get; init; } diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs b/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs index 48f1a9bd..abb7757e 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs @@ -1,10 +1,13 @@ namespace Bootsharp.Publish; +/// +/// Renames associated with the overloaded methods making the names unique. +/// internal static class OverloadDisambiguator { - public static void Disambiguate (IEnumerable surfaces) + public static void Disambiguate (IEnumerable types) { - foreach (var surface in surfaces) + foreach (var surface in types.OfType()) foreach (var overloaded in surface.Members.OfType() .GroupBy(m => m.JSName).Where(g => g.Count() > 1)) Disambiguate(surface, overloaded.ToArray()); @@ -27,7 +30,7 @@ private static Dictionary MapArgs (IReadOnlyList a.JSName).ToHashSet(); return overloaded.Where(m => m != baseline).ToDictionary(m => m, m => m.Args .Where(a => !baselineArgs.Contains(a.JSName)) - .Select(a => ToFirstUpper(a.JSName)) + .Select(a => ToFirstUpper(a.Info.Name!)) // if an overload has extra args — use their names as discriminator .ToArray() is { Length: > 0 } extra ? extra : GetArgNames(m)); } @@ -38,7 +41,7 @@ private static IEnumerable FindAmbiguous (Dictionary g.Select(kv => kv.Key)); private static string[] GetArgNames (MethodMeta method) => method.Args - .Select(a => ToFirstUpper(a.JSName)).ToArray(); + .Select(a => ToFirstUpper(a.Info.Name!)).ToArray(); private static string[] GetArgTypes (MethodMeta method) => method.Args .Select(a => a.Value.Type.Clr.Name).ToArray(); @@ -48,9 +51,12 @@ private static MethodMeta ResolveBaseline (IReadOnlyList overloaded) .ThenBy(m => string.Join("|", m.Args.Select(a => a.Value.Type.Clr.FullName))) .First(); - private static void Rename (SurfaceMeta srf, MethodMeta method, string discriminator) + private static void Rename (SurfaceMeta srf, MethodMeta meth, string discriminator) { var members = (IList)srf.Members; - members[members.IndexOf(method)] = method with { JSName = $"{method.JSName}With{discriminator}" }; + members[members.IndexOf(meth)] = meth with { + JSName = $"{meth.JSName}With{discriminator}", + Endpoint = $"{meth.Endpoint}With{discriminator}" + }; } } diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/SerializedInspector.cs index b2618e6f..3c747f3f 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/SerializedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/SerializedInspector.cs @@ -39,7 +39,7 @@ public IReadOnlyList Collect () private static bool IsSerialized (Type type) { - if (IsVoid(type)) return false; + if (IsVoid(type) || type.ContainsGenericParameters) return false; if (IsNullable(type, out var value)) return IsSerialized(value); if (IsTaskWithResult(type, out var result)) return IsSerialized(result); return !native.Contains(type.FullName!); diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs index df3ece7a..9da6a852 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs @@ -31,15 +31,16 @@ public void Inspect (Assembly assembly) public IReadOnlyCollection Collect () { - OverloadDisambiguator.Disambiguate([..surfaces, ..its.Values]); - TypeMeta[] specialized = [..surfaces, ..its.Values, ..srd.Collect()]; - var clrs = specialized.Select(t => t.Clr).ToHashSet(); - return Preferences.Rename([..specialized, ..crawled.Values.Where(c => !clrs.Contains(c.Clr))]); + TypeMeta[] types = [..surfaces, ..its.Values, ..srd.Collect()]; + GenericDisambiguator.Disambiguate(types); + OverloadDisambiguator.Disambiguate(types); + var clrs = types.Select(t => t.Clr).ToHashSet(); + return Preferences.Rename([..types, ..crawled.Values.Where(c => !clrs.Contains(c.Clr))]); } private StaticMeta? InspectStatic (Type type) { - if (type.Namespace?.StartsWith("Bootsharp.Generated") == true) return null; + if (!IsUserType(type)) return null; var members = new List(); var st = new StaticMeta(type) { Members = members }; var flags = BindingFlags.Public | BindingFlags.Static; @@ -86,7 +87,7 @@ static bool IsInstanced (Type type) { // Instanced types are mutable user types that are passed by reference when crossing the // interop boundary (as opposed to serialized immutable types, which are copied by value). - if (!IsUserType(type)) return false; + if (!IsUserType(type) || type.ContainsGenericParameters) return false; if (type.IsInterface || Preferences.IsSpecialized(type)) return true; return type.IsClass && !IsStatic(type) && !IsRecord(type); // records are immutable by convention } @@ -172,6 +173,7 @@ bool ShouldInspectMethod (MethodInfo method) IK = ik, Surf = srf, Name = BuildCSName(method.Name), + Endpoint = BuildCSName(method.Name), JSName = BuildJSName(method.Name), Args = method.GetParameters().Select(p => InspectArg(p, GetNullity(p), ik.Invert)).ToArray(), Return = InspectValue(method.ReturnParameter.ParameterType, GetNullity(method.ReturnParameter), ik), @@ -193,8 +195,7 @@ bool ShouldInspectMethod (MethodInfo method) private TypeMeta InspectType (Type type, InteropKind ik, NullabilityInfo? nul = null) { - for (var clr = type; clr.IsNested && IsUserType(clr.DeclaringType!); clr = clr.DeclaringType!) - crawled.TryAdd(clr.DeclaringType!, new(clr.DeclaringType!)); + CrawlInspected(type, ik); return InspectInstance(type, ik, nul) ?? srd.Inspect(type, ik) ?? new TypeMeta(type); } @@ -213,6 +214,23 @@ private SurfaceProxy BuildProxy (Type type, string typeId, InteropKind ik, Speci }; } + private void CrawlInspected (Type type, InteropKind ik) + { + for (var clr = type; clr.IsNested && IsUserType(clr.DeclaringType!); clr = clr.DeclaringType!) + crawled.TryAdd(clr.DeclaringType!, new(clr.DeclaringType!)); + if (type.IsGenericMethodParameter) + foreach (var compatible in FindCompatible(type)) + InspectInstance(compatible, ik, null); + + static IEnumerable FindCompatible (Type param) + { + foreach (var ct in param.GetGenericParameterConstraints().Where(IsUserType)) + foreach (var type in ct.Assembly.GetExportedTypes()) + if (!type.IsAbstract && !type.ContainsGenericParameters && ct.IsAssignableFrom(type)) + yield return type; + } + } + private InteropKind? ResolveIK (MemberInfo info) { foreach (var attr in info.CustomAttributes) diff --git a/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs index 7b69208d..b8bd45cb 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs @@ -160,10 +160,10 @@ private string EmitMethodImport (MethodMeta method) { var args = string.Join(", ", method.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); var callArgs = PrependIdArg(string.Join(", ", method.Args.Select(a => a.Name))); - var name = $"{it.Proxy.Id}_{method.Name}"; - var head = it.Proxy is SpecializedProxy + var name = $"{it.Proxy.Id}_{method.Endpoint}"; + var sig = it.Proxy is SpecializedProxy ? $"public override {method.Return.TypeSyntax} {method.Name}" : $"{method.Return.TypeSyntax} {it.Syntax}.{method.Name}"; - return $"{head} ({args}) => global::Bootsharp.Generated.Interop.{name}({callArgs});"; + return $"{sig} ({args}) => global::Bootsharp.Generated.Interop.{name}({callArgs});"; } } diff --git a/src/cs/Bootsharp.Publish/GenerateCS/CSInteropGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSInteropGenerator.cs index cba39481..c6c95b19 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/CSInteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/CSInteropGenerator.cs @@ -43,7 +43,7 @@ private static IEnumerable EmitInitializers (SurfaceMeta srf) yield return $"{stx}.{evt.Name} += Handle_{id}_{evt.Name};"; if (srf is not StaticMeta) yield break; foreach (var mem in srf.Members.OfType().Where(m => m.IK == InteropKind.Import)) - yield return $"{stx}.Bootsharp_{mem.Name} = &{id}_{mem.Name};"; + yield return $"{stx}.Bootsharp_{mem.Name} = &{id}_{mem.Endpoint};"; foreach (var p in srf.Members.OfType().Where(p => p.IK == InteropKind.Import)) { if (p.CanGet) yield return $"{stx}.Bootsharp_Get{p.Name} = &{id}_Get{p.Name};"; @@ -156,14 +156,14 @@ private IEnumerable EmitMethodExport (MethodMeta method) { var wait = ShouldWait(method); var attr = $"[JSExport] {MarshalAmbiguous(method.Return, true)}"; - var name = $"{id}_{method.Name}"; + var name = $"{id}_{method.Endpoint}"; var @return = BuildValueSyntax(method.Return); if (wait) @return = $"async global::System.Threading.Tasks.Task<{@return}>"; var sigArgs = string.Join(", ", method.Args.Select(a => BuildParameter(a.Value, a.Name))); if (isIt) sigArgs = $"int {PrependIdArg(sigArgs)}"; var invArgs = string.Join(", ", method.Args.Select(ImportCS)); - var invName = isIt - ? $"Instances.Exported<{key}>(_id).{method.Name}" + var invName = isIt ? $"Instances.Exported<{key}>(_id).{method.Name}" + : isMd ? $"{stx}.{method.Endpoint}" : $"{stx}.{method.Name}"; var body = ExportCS(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); yield return $"{attr}internal static {@return} {name} ({sigArgs}) => {body};"; @@ -173,7 +173,7 @@ private IEnumerable EmitMethodImport (MethodMeta method) { var attr = $"""[JSImport("{srf.JSNode}.{method.JSName}Serialized", "{srf.JSModule}")]"""; var marshalAs = MarshalAmbiguous(method.Return, true); - var name = $"{id}_{method.Name}"; + var name = $"{id}_{method.Endpoint}"; var @return = BuildValueSyntax(method.Return); if (ShouldWait(method)) @return = $"global::System.Threading.Tasks.Task<{@return}>"; var args = string.Join(", ", method.Args.Select(a => BuildParameter(a.Value, a.Name))); diff --git a/src/cs/Bootsharp.Publish/GenerateCS/CSModuleGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSModuleGenerator.cs index 3ef65f31..08c64fe7 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/CSModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/CSModuleGenerator.cs @@ -124,7 +124,7 @@ private string EmitPropertyImport (PropertyMeta prop) private string EmitMethodExport (MethodMeta method) { var args = string.Join(", ", method.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var sig = $"public static {method.Return.TypeSyntax} {method.Name} ({args})"; + var sig = $"public static {method.Return.TypeSyntax} {method.Endpoint} ({args})"; var callArgs = string.Join(", ", method.Args.Select(a => a.Name)); return $"[Export] {sig} => handler.{method.Name}({callArgs});"; } @@ -133,7 +133,7 @@ private string EmitMethodImport (MethodMeta method) { var args = string.Join(", ", method.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); var callArgs = string.Join(", ", method.Args.Select(a => a.Name)); - var name = $"{md.Proxy.Id}_{method.Name}"; + var name = $"{md.Proxy.Id}_{method.Endpoint}"; return $"{method.Return.TypeSyntax} {md.Syntax}.{method.Name} ({args}) => " + $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; } diff --git a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs index e687e874..126bf5d8 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs @@ -109,7 +109,7 @@ private void DeclareInstance (InstanceMeta it) string BuildExtensions () { if (it.Proxy is SpecializedProxy) return ""; // specialized surfaces are self-contained - var ext = it.Clr.GetInterfaces().Where(IsUserType).ToList(); + var ext = it.Clr.GetInterfaces().Where(i => IsUserType(i) && spec.Types.Has(i)).ToList(); if (spec.Types.HasBase(it.Clr, out var bs)) ext.Insert(0, bs.Clr); return ext.Count == 0 ? "" : $" extends {string.Join(", ", ext.Select(ts.BuildFullName))}"; } diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs index 1d56a49f..77bebfc7 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs @@ -152,7 +152,7 @@ private void EmitPropertyImport (PropertyMeta prop) private void EmitMethodExport (MethodMeta method) { var wait = ShouldWait(method); - var fnName = $"{id}_{method.Name}"; + var fnName = $"{id}_{method.Endpoint}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; var args = string.Join(", ", method.Args.Select(a => a.JSName)); if (isIt) args = PrependIdArg(args); diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs index 53f3e8c7..81717635 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs @@ -38,7 +38,7 @@ public string Ref (TypeMeta type, JSModule? fromMd = null) /// public string Ref (Type clr, JSModule? fromMd = null) { - return Ref(types.Get(clr), fromMd); + return Ref(types.First(clr), fromMd); } /// diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index e9523d02..33aab034 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.440 + 0.8.0-alpha.448 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/test/cs/Test/Static.cs b/src/js/test/cs/Test/Static.cs index 3d869cd7..bf767f95 100644 --- a/src/js/test/cs/Test/Static.cs +++ b/src/js/test/cs/Test/Static.cs @@ -12,6 +12,23 @@ public static partial class Static { public enum Enum { One = 1, Two = 2 } + public interface IShape + { + string Describe (); + } + + public class Circle : IShape + { + public string Describe () => "circle"; + public double Area () => 3.14; + } + + public class Square : IShape + { + public string Describe () => "square"; + public double Area () => 4.0; + } + [Import] public static event Action? ImportedEvent; [Export] public static event Action? ExportedEvent; @@ -28,6 +45,11 @@ public enum Enum { One = 1, Two = 2 } [Export] public static void BroadcastExportedEvent (string? payload) => ExportedEvent?.Invoke(payload); [Export] public static DateTime AddDays (DateTime date, int days) => date.AddDays(days); [Export] public static Enum GetEnum (int idx) => (Enum)idx; + [Export] public static T MakeGeneric () where T : IShape, new() => new T(); + [Export] public static T EchoGeneric (T shape) where T : IShape => shape; + [Export] public static string Combine (int a) => $"int:{a}"; + [Export] public static string Combine (string a) => $"str:{a}"; + [Export] public static string Combine (int a, int b) => $"sum:{a + b}"; [Export] public static async Task CanInteropWithImportedStaticsAsync () diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index 18c0181a..fa971372 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -296,4 +296,19 @@ describe("while bootsharp is booted", () => { const actual = new Date(Static.addDays(date, 7)); expect(actual).toStrictEqual(expected); }); + it("can interop with generic methods", () => { + const circle = Static.makeGenericWithCircle(); + const square = Static.makeGenericWithSquare(); + expect(circle.describe()).toBe("circle"); + expect(circle.area()).toBe(3.14); + expect(square.describe()).toBe("square"); + expect(square.area()).toBe(4.0); + expect(Static.echoGenericWithCircle(circle)).toBe(circle); + expect(Static.echoGenericWithSquare(square)).toBe(square); + }); + it("can interop with overloaded methods", () => { + expect(Static.combine(42)).toBe("int:42"); + expect(Static.combineWithA("foo")).toBe("str:foo"); + expect(Static.combineWithB(1, 2)).toBe("sum:3"); + }); }); From 4076801df09cae58b7b73dbae96f53ba5bdec9c2 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:41:52 +0300 Subject: [PATCH 2/5] discard nested generic --- .../GenerateJS/JSModuleTest.cs | 4 +++- .../Common/Inspection/GenericDisambiguator.cs | 22 ++++++++++--------- .../Inspection/OverloadDisambiguator.cs | 2 +- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs index 68e743d2..2f975812 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs @@ -761,12 +761,13 @@ public void DiscardsUnsupportedGenericMethods () { AddAssembly(With( """ - using System.Collections.Generic; public interface IShape {} + public class Circle : IShape { public double Radius { get; set; } } public static class Box { [Export] public static void Stored (T item) {} } public class Class { [Export] public static void Pair () where T1 : IShape where T2 : IShape {} + [Export] public static void Take (T shape) where T : IShape {} [Export] public static void Many (List items) where T : IShape {} [Export] public static void Free () {} [Export] public static void Real () {} @@ -774,6 +775,7 @@ [Export] public static void Real () {} """)); Execute(); Contains("real:"); + Contains("takeWithCircle:"); DoesNotContain("stored"); // method declared on a generic type DoesNotContain("pair"); // multiple type parameters DoesNotContain("many"); // type parameter used nested diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs b/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs index 2e50149a..31e1f2e9 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs @@ -8,34 +8,36 @@ namespace Bootsharp.Publish; /// internal static class GenericDisambiguator { - private static IReadOnlyCollection types = null!; - - public static void Disambiguate (IReadOnlyCollection types) + public static void Disambiguate (TypeMeta[] types) { - GenericDisambiguator.types = types; foreach (var surface in types.OfType()) foreach (var method in surface.Members.OfType().ToArray()) if (method.IK == InteropKind.Export && method.Info.ContainsGenericParameters) - Disambiguate(surface, method); + Disambiguate(surface, method, types); } - private static void Disambiguate (SurfaceMeta surf, MethodMeta meth) + private static void Disambiguate (SurfaceMeta surf, MethodMeta meth, TypeMeta[] types) { surf.MemberList.Remove(meth); - foreach (var expanded in Expand(meth)) + foreach (var expanded in Expand(meth, types)) surf.MemberList.Add(expanded); } - private static IEnumerable Expand (MethodMeta meth) + private static IEnumerable Expand (MethodMeta meth, TypeMeta[] types) { if (!meth.Info.IsGenericMethodDefinition) yield break; // ignore methods declared on a generic type if (meth.Info.GetGenericArguments() is not [{ } param]) yield break; // only single supported if (param.GetGenericParameterConstraints().FirstOrDefault(IsUserType) is not { } ct) yield break; - foreach (var compatible in GetCompatible(ct)) + if (HasNestedGeneric(meth.Info)) yield break; // type parameter nested in another type can't be rewritten + foreach (var compatible in GetCompatible(ct, types)) yield return CloseGeneric(meth, compatible); } - private static IEnumerable GetCompatible (Type constraint) => types + private static bool HasNestedGeneric (MethodInfo meth) => meth + .GetParameters().Select(p => p.ParameterType).Prepend(meth.ReturnType) + .Any(t => t.ContainsGenericParameters && !t.IsGenericMethodParameter); + + private static IEnumerable GetCompatible (Type constraint, TypeMeta[] types) => types .Where(t => t is InstanceMeta && !t.Clr.IsAbstract && constraint.IsAssignableFrom(t.Clr)) .DistinctBy(t => t.Clr); diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs b/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs index abb7757e..a98c74a1 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs @@ -5,7 +5,7 @@ namespace Bootsharp.Publish; /// internal static class OverloadDisambiguator { - public static void Disambiguate (IEnumerable types) + public static void Disambiguate (TypeMeta[] types) { foreach (var surface in types.OfType()) foreach (var overloaded in surface.Members.OfType() From 8322fb4c1d1e7fac1df39cb2adcb15a1f8843952 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:54:41 +0300 Subject: [PATCH 3/5] discover compatible types in cross asses --- .../GenerateCS/CSInteropTest.cs | 19 +++++++++++++++++++ .../Common/Inspection/TypeInspector.cs | 4 +++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs index e85a1532..e1ef2520 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs @@ -414,6 +414,25 @@ [Export] public static void Mix (T shape, int n) where T : IShape {} Contains("Class_EchoNullCircle (int shape) => Instances.Export(global::Class.EchoNull(Instances.Resolve(shape)))"); } + [Fact] + public void GeneratesGenericMethodsForCompatibleTypesInOtherAssemblies () + { + AddAssembly("Contracts.dll", With("public interface IShape {}")); + AddAssembly("Shapes.dll", With( + """ + public class Circle : IShape { public double Radius { get; set; } } + public class Square : IShape { public double Side { get; set; } } + + public class Class + { + [Export] public static T Make () where T : IShape => default!; + } + """)); + Execute(); + Contains("Class_MakeCircle () => Instances.Export(global::Class.Make())"); + Contains("Class_MakeSquare () => Instances.Export(global::Class.Make())"); + } + [Fact] public void DoesNotSerializeTypesThatShouldNotBeSerialized () { diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs index 9da6a852..cffcf15d 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.Loader; namespace Bootsharp.Publish; @@ -225,7 +226,8 @@ private void CrawlInspected (Type type, InteropKind ik) static IEnumerable FindCompatible (Type param) { foreach (var ct in param.GetGenericParameterConstraints().Where(IsUserType)) - foreach (var type in ct.Assembly.GetExportedTypes()) + foreach (var ass in AssemblyLoadContext.GetLoadContext(ct.Assembly)!.Assemblies) + foreach (var type in ass.GetExportedTypes()) if (!type.IsAbstract && !type.ContainsGenericParameters && ct.IsAssignableFrom(type)) yield return type; } From 49ef3e48bbda4674190b690e3e75b85eeef61875 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:13:16 +0300 Subject: [PATCH 4/5] change generic suffix to "Of", add docs --- docs/guide/declarations.md | 34 +++++++++++++++++++ .../GenerateCS/CSInteropTest.cs | 16 ++++----- .../GenerateJS/DeclarationTest.cs | 8 ++--- .../GenerateJS/JSModuleTest.cs | 22 ++++++------ .../Common/Inspection/GenericDisambiguator.cs | 4 +-- src/cs/Directory.Build.props | 2 +- src/js/test/spec/interop.spec.ts | 8 ++--- 7 files changed, 64 insertions(+), 30 deletions(-) diff --git a/docs/guide/declarations.md b/docs/guide/declarations.md index 543bc1fd..25870879 100644 --- a/docs/guide/declarations.md +++ b/docs/guide/declarations.md @@ -83,6 +83,40 @@ export namespace Class { ::: +## Generic Methods + +JavaScript does not have generic functions, so Bootsharp projects an exported generic method as a concrete overload for each user type that satisfies the method's type parameter constraint, suffixing each with `Of...` derived from the bound type's name. Only a single type parameter, constrained to a user type, is expanded. + +::: code-group + +```csharp [Class.cs] +public interface IShape {} +public class Circle : IShape {} +public class Square : IShape {} + +public class Class +{ + [Export] + public static T CreateShape () where T : IShape + { + if (typeof(T) == typeof(Circle)) return new Circle(); + if (typeof(T) == typeof(Square)) return new Square(); + } +} +``` + +```ts [index.g.d.mts] +export interface Circle {} +export interface Square {} + +export namespace Class { + export function createShapeOfCircle(): Circle; + export function createShapeOfSquare(): Square; +} +``` + +::: + ## Default Arguments C# method parameters with default values are emitted as optional TypeScript parameters using the `?:` syntax, letting callers omit them at the call site: diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs index e1ef2520..8e0ed139 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs @@ -406,12 +406,12 @@ [Export] public static void Mix (T shape, int n) where T : IShape {} } """)); Execute(); - Contains("Class_MakeCircle () => Instances.Export(global::Class.Make())"); - Contains("Class_MakeSquare () => Instances.Export(global::Class.Make())"); - Contains("Class_TakeCircle (int shape) => global::Class.Take(Instances.Resolve(shape))"); - Contains("Class_TakeSquare (int shape) => global::Class.Take(Instances.Resolve(shape))"); - Contains("Class_MixCircle (int shape, global::System.Int32 n) => global::Class.Mix(Instances.Resolve(shape), n)"); - Contains("Class_EchoNullCircle (int shape) => Instances.Export(global::Class.EchoNull(Instances.Resolve(shape)))"); + Contains("Class_MakeOfCircle () => Instances.Export(global::Class.Make())"); + Contains("Class_MakeOfSquare () => Instances.Export(global::Class.Make())"); + Contains("Class_TakeOfCircle (int shape) => global::Class.Take(Instances.Resolve(shape))"); + Contains("Class_TakeOfSquare (int shape) => global::Class.Take(Instances.Resolve(shape))"); + Contains("Class_MixOfCircle (int shape, global::System.Int32 n) => global::Class.Mix(Instances.Resolve(shape), n)"); + Contains("Class_EchoNullOfCircle (int shape) => Instances.Export(global::Class.EchoNull(Instances.Resolve(shape)))"); } [Fact] @@ -429,8 +429,8 @@ public class Class } """)); Execute(); - Contains("Class_MakeCircle () => Instances.Export(global::Class.Make())"); - Contains("Class_MakeSquare () => Instances.Export(global::Class.Make())"); + Contains("Class_MakeOfCircle () => Instances.Export(global::Class.Make())"); + Contains("Class_MakeOfSquare () => Instances.Export(global::Class.Make())"); } [Fact] diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs index 5b7b01e0..5a3e2333 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs @@ -558,10 +558,10 @@ [Export] public static void Take (T shape) where T : IShape {} } """)); Execute(); - Contains("export function makeWithCircle(): Circle;"); - Contains("export function makeWithSquare(): Square;"); - Contains("export function takeWithCircle(shape: Circle): void;"); - Contains("export function takeWithSquare(shape: Square): void;"); + Contains("export function makeOfCircle(): Circle;"); + Contains("export function makeOfSquare(): Square;"); + Contains("export function takeOfCircle(shape: Circle): void;"); + Contains("export function takeOfSquare(shape: Square): void;"); } [Fact] diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs index 2f975812..47b4fbc9 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs @@ -746,14 +746,14 @@ [Export] public static void Take (T shape) where T : IShape {} } """)); Execute(); - Contains("makeWithCircle: () =>"); - Contains("makeWithSquare: () =>"); - Contains("takeWithCircle: (shape) =>"); - Contains("takeWithSquare: (shape) =>"); - Contains("exports.Class_MakeCircle()"); - Contains("exports.Class_MakeSquare()"); - Contains("exports.Class_TakeCircle("); - Contains("exports.Class_TakeSquare("); + Contains("makeOfCircle: () =>"); + Contains("makeOfSquare: () =>"); + Contains("takeOfCircle: (shape) =>"); + Contains("takeOfSquare: (shape) =>"); + Contains("exports.Class_MakeOfCircle()"); + Contains("exports.Class_MakeOfSquare()"); + Contains("exports.Class_TakeOfCircle("); + Contains("exports.Class_TakeOfSquare("); } [Fact] @@ -775,7 +775,7 @@ [Export] public static void Real () {} """)); Execute(); Contains("real:"); - Contains("takeWithCircle:"); + Contains("takeOfCircle:"); DoesNotContain("stored"); // method declared on a generic type DoesNotContain("pair"); // multiple type parameters DoesNotContain("many"); // type parameter used nested @@ -796,8 +796,8 @@ public class Class } """)); Execute(); - Contains("makeWithCircle:"); - DoesNotContain("makeWithJS_Import_Leaked"); + Contains("makeOfCircle:"); + DoesNotContain("makeOfJS_Import_Leaked"); } [Fact] diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs b/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs index 31e1f2e9..51280322 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs @@ -48,8 +48,8 @@ private static MethodMeta CloseGeneric (MethodMeta meth, TypeMeta closeType) Surf = meth.Surf, IK = meth.IK, Name = $"{meth.Name}<{closeType.Syntax}>", - Endpoint = $"{meth.Name}{closeType.Id}", - JSName = $"{meth.JSName}With{closeType.Clr.Name}", + Endpoint = $"{meth.Name}Of{closeType.Id}", + JSName = $"{meth.JSName}Of{closeType.Clr.Name}", Args = meth.Args.Zip(closed.GetParameters(), RewriteArg).ToArray(), Return = meth.Info.ReturnType.IsGenericMethodParameter ? RewriteValue(meth.Return) : meth.Return, Void = meth.Void, diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 33aab034..6e5256bb 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.448 + 0.8.0-alpha.449 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index fa971372..80515691 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -297,14 +297,14 @@ describe("while bootsharp is booted", () => { expect(actual).toStrictEqual(expected); }); it("can interop with generic methods", () => { - const circle = Static.makeGenericWithCircle(); - const square = Static.makeGenericWithSquare(); + const circle = Static.makeGenericOfCircle(); + const square = Static.makeGenericOfSquare(); expect(circle.describe()).toBe("circle"); expect(circle.area()).toBe(3.14); expect(square.describe()).toBe("square"); expect(square.area()).toBe(4.0); - expect(Static.echoGenericWithCircle(circle)).toBe(circle); - expect(Static.echoGenericWithSquare(square)).toBe(square); + expect(Static.echoGenericOfCircle(circle)).toBe(circle); + expect(Static.echoGenericOfSquare(square)).toBe(square); }); it("can interop with overloaded methods", () => { expect(Static.combine(42)).toBe("int:42"); From 89d135e837192114e8f4b8e70173ff8935eb7f25 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:17:33 +0300 Subject: [PATCH 5/5] support serialized in generic types --- .../GenerateJS/JSModuleTest.cs | 2 +- .../Common/Inspection/GenericDisambiguator.cs | 4 ++-- .../Common/Inspection/TypeInspector.cs | 8 ++++---- src/cs/Directory.Build.props | 2 +- src/js/test/cs/Test/Serialization.cs | 4 +--- src/js/test/cs/Test/Static.cs | 14 +++++++------- src/js/test/spec/interop.spec.ts | 16 ++++++++++------ src/js/test/spec/serialization.spec.ts | 9 +++++---- 8 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs index 47b4fbc9..2db7af20 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs @@ -737,7 +737,7 @@ public void GeneratesSupportedGenericMethods () """ public interface IShape {} public class Circle : IShape { public double Radius { get; set; } } - public class Square : IShape { public double Side { get; set; } } + public record Square : IShape { public double Side { get; set; } } public class Class { diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs b/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs index 51280322..a3832c51 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs @@ -37,8 +37,8 @@ private static bool HasNestedGeneric (MethodInfo meth) => meth .GetParameters().Select(p => p.ParameterType).Prepend(meth.ReturnType) .Any(t => t.ContainsGenericParameters && !t.IsGenericMethodParameter); - private static IEnumerable GetCompatible (Type constraint, TypeMeta[] types) => types - .Where(t => t is InstanceMeta && !t.Clr.IsAbstract && constraint.IsAssignableFrom(t.Clr)) + private static IEnumerable GetCompatible (Type ct, TypeMeta[] types) => types + .Where(t => t is InstanceMeta or SerializedObjectMeta && !t.Clr.IsAbstract && ct.IsAssignableFrom(t.Clr)) .DistinctBy(t => t.Clr); private static MethodMeta CloseGeneric (MethodMeta meth, TypeMeta closeType) diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs index cffcf15d..f6288c3f 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs @@ -221,15 +221,15 @@ private void CrawlInspected (Type type, InteropKind ik) crawled.TryAdd(clr.DeclaringType!, new(clr.DeclaringType!)); if (type.IsGenericMethodParameter) foreach (var compatible in FindCompatible(type)) - InspectInstance(compatible, ik, null); + InspectType(compatible, ik); static IEnumerable FindCompatible (Type param) { foreach (var ct in param.GetGenericParameterConstraints().Where(IsUserType)) foreach (var ass in AssemblyLoadContext.GetLoadContext(ct.Assembly)!.Assemblies) - foreach (var type in ass.GetExportedTypes()) - if (!type.IsAbstract && !type.ContainsGenericParameters && ct.IsAssignableFrom(type)) - yield return type; + foreach (var clr in ass.GetExportedTypes()) + if (IsUserType(clr) && !clr.IsAbstract && !clr.ContainsGenericParameters && ct.IsAssignableFrom(clr)) + yield return clr; } } diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 6e5256bb..98f2c319 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.449 + 0.8.0-alpha.450 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/test/cs/Test/Serialization.cs b/src/js/test/cs/Test/Serialization.cs index 40dc05d6..d0ffeadf 100644 --- a/src/js/test/cs/Test/Serialization.cs +++ b/src/js/test/cs/Test/Serialization.cs @@ -62,6 +62,7 @@ public static class Serialization { [Export] public static Primitives?[]? EchoPrimitives (Primitives?[]? value) => value; [Export] public static Union?[]? EchoUnions (Union?[]? value) => value; + [Export] public static Static.Square?[]? EchoSquares (Static.Square?[]? value) => value; [Export] public static byte[]? EchoBytes (byte[]? value) => value; [Export] public static int[]? EchoIntArray (int[]? value) => value; [Export] public static double[]? EchoDoubleArray (double[]? value) => value; @@ -73,11 +74,8 @@ public static class Serialization [Export] public static List?[]? EchoNestedIntList (List?[]? value) => value; [Export] public static Dictionary? EchoDictionary (Dictionary? value) => value; [Export] public static Dictionary?[]? EchoNestedDictionary (Dictionary?[]? value) => value; - [Export] public static IList EchoListInterface (IList value) => value; [Export] public static IReadOnlyList EchoReadOnlyList (IReadOnlyList value) => value; - [Export] public static ICollection EchoCollection (ICollection value) => value; [Export] public static IReadOnlyCollection EchoReadOnlyCollection (IReadOnlyCollection value) => value; - [Export] public static IDictionary EchoDictionaryInterface (IDictionary value) => value; [Export] public static IReadOnlyDictionary EchoReadOnlyDictionary (IReadOnlyDictionary value) => value; [Export] diff --git a/src/js/test/cs/Test/Static.cs b/src/js/test/cs/Test/Static.cs index bf767f95..f1617e96 100644 --- a/src/js/test/cs/Test/Static.cs +++ b/src/js/test/cs/Test/Static.cs @@ -14,19 +14,19 @@ public enum Enum { One = 1, Two = 2 } public interface IShape { - string Describe (); + string Name { get; set; } } public class Circle : IShape { - public string Describe () => "circle"; - public double Area () => 3.14; + public string Name { get; set; } = "circle"; + public double GetRadius () => 3.14; } - public class Square : IShape + public record Square : IShape { - public string Describe () => "square"; - public double Area () => 4.0; + public string Name { get; set; } = "square"; + public double Area { get; set; } } [Import] public static event Action? ImportedEvent; @@ -45,7 +45,7 @@ public class Square : IShape [Export] public static void BroadcastExportedEvent (string? payload) => ExportedEvent?.Invoke(payload); [Export] public static DateTime AddDays (DateTime date, int days) => date.AddDays(days); [Export] public static Enum GetEnum (int idx) => (Enum)idx; - [Export] public static T MakeGeneric () where T : IShape, new() => new T(); + [Export] public static T MakeGeneric () where T : IShape, new() => new(); [Export] public static T EchoGeneric (T shape) where T : IShape => shape; [Export] public static string Combine (int a) => $"int:{a}"; [Export] public static string Combine (string a) => $"str:{a}"; diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index 80515691..a84480cd 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -298,13 +298,17 @@ describe("while bootsharp is booted", () => { }); it("can interop with generic methods", () => { const circle = Static.makeGenericOfCircle(); - const square = Static.makeGenericOfSquare(); - expect(circle.describe()).toBe("circle"); - expect(circle.area()).toBe(3.14); - expect(square.describe()).toBe("square"); - expect(square.area()).toBe(4.0); + expect(circle.name).toBe("circle"); + circle.name = "foo"; + expect(circle.name).toBe("foo"); + expect(circle.getRadius()).toBe(3.14); expect(Static.echoGenericOfCircle(circle)).toBe(circle); - expect(Static.echoGenericOfSquare(square)).toBe(square); + const square = Static.makeGenericOfSquare(); + expect(square.name).toBe("square"); + expect(square.area).toBe(0); + const echoed = Static.echoGenericOfSquare(square); + expect(echoed).toEqual(square); + expect(echoed).not.toBe(square); }); it("can interop with overloaded methods", () => { expect(Static.combine(42)).toBe("int:42"); diff --git a/src/js/test/spec/serialization.spec.ts b/src/js/test/spec/serialization.spec.ts index 3a6448a1..235da536 100644 --- a/src/js/test/spec/serialization.spec.ts +++ b/src/js/test/spec/serialization.spec.ts @@ -52,6 +52,11 @@ describe("serialization", () => { }; expect(Serialization.echoPrimitives([input])).toStrictEqual([input]); }); + it("can echo squares", () => { + expect(Serialization.echoSquares([{ name: "square", area: 4 }, null])) + .toStrictEqual([{ name: "square", area: 4 }, null]); + expect(Serialization.echoSquares(undefined)).toBeNull(); + }); it("can echo unions", () => { const a: Union = { shared: "A", a: { string: "*", map: new Map([["a", null], ["b", 7]]) } }; const b: Union = { shared: "B", b: { ints: [], strings: ["foo", "bar"], times: [new Date()] } }; @@ -96,9 +101,7 @@ describe("serialization", () => { expect(Serialization.echoIntList([1, 2, 3])).toStrictEqual([1, 2, 3]); expect(Serialization.echoStringList(["a", null, "", "b"])).toStrictEqual(["a", null, "", "b"]); expect(Serialization.echoNestedIntList([[1, 2], null, []])).toStrictEqual([[1, 2], null, []]); - expect(Serialization.echoListInterface(["a", "b", "c"])).toStrictEqual(["a", "b", "c"]); expect(Serialization.echoReadOnlyList([1, 2, 3])).toStrictEqual([1, 2, 3]); - expect(Serialization.echoCollection(["a", "b", "c"])).toStrictEqual(["a", "b", "c"]); expect(Serialization.echoReadOnlyCollection([1, 2, 3])).toStrictEqual([1, 2, 3]); expect(Serialization.echoIntList(undefined)).toBeNull(); expect(Serialization.echoStringList(undefined)).toBeNull(); @@ -109,8 +112,6 @@ describe("serialization", () => { .toStrictEqual(new Map([["1", "a"], ["2", null], ["3", ""]])); expect(Serialization.echoNestedDictionary([new Map([["1", "a"]]), null, new Map([["2", null]])])) .toStrictEqual([new Map([["1", "a"]]), null, new Map([["2", null]])]); - expect(Serialization.echoDictionaryInterface(new Map([["a", "b"], ["c", "d"]]))) - .toStrictEqual(new Map([["a", "b"], ["c", "d"]])); expect(Serialization.echoReadOnlyDictionary(new Map([["a", "b"], ["c", "d"]]))) .toStrictEqual(new Map([["a", "b"], ["c", "d"]])); expect(Serialization.echoDictionary(undefined)).toBeNull();