using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; namespace Lua.SourceGenerator; partial class LuaObjectGenerator { static bool TryEmit(TypeMetadata typeMetadata, CodeBuilder builder, SymbolReferences references, Compilation compilation, in SourceProductionContext context) { try { var error = false; // must be partial if (!typeMetadata.IsPartial()) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.MustBePartial, typeMetadata.Syntax.Identifier.GetLocation(), typeMetadata.Symbol.Name)); error = true; } // nested is not allowed if (typeMetadata.IsNested()) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.NestedNotAllowed, typeMetadata.Syntax.Identifier.GetLocation(), typeMetadata.Symbol.Name)); error = true; } // verify abstract/interface if (typeMetadata.Symbol.IsAbstract) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.AbstractNotAllowed, typeMetadata.Syntax.Identifier.GetLocation(), typeMetadata.TypeName)); error = true; } if (!ValidateMembers(typeMetadata, compilation, references, context)) { error = true; } if (error) { return false; } builder.AppendLine("// "); builder.AppendLine("#nullable enable"); builder.AppendLine("#pragma warning disable CS0162 // Unreachable code"); builder.AppendLine("#pragma warning disable CS0219 // Variable assigned but never used"); builder.AppendLine("#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type."); builder.AppendLine("#pragma warning disable CS8601 // Possible null reference assignment"); builder.AppendLine("#pragma warning disable CS8602 // Possible null return"); builder.AppendLine("#pragma warning disable CS8604 // Possible null reference argument for parameter"); builder.AppendLine("#pragma warning disable CS8631 // The type cannot be used as type parameter in the generic type or method"); builder.AppendLine(); var ns = typeMetadata.Symbol.ContainingNamespace; if (!ns.IsGlobalNamespace) { builder.AppendLine($"namespace {ns}"); builder.BeginBlock(); } var typeDeclarationKeyword = (typeMetadata.Symbol.IsRecord, typeMetadata.Symbol.IsValueType) switch { (true, true) => "record struct", (true, false) => "record", (false, true) => "struct", (false, false) => "class", }; using var _ = builder.BeginBlockScope($"partial {typeDeclarationKeyword} {typeMetadata.TypeName} : global::Lua.ILuaUserData"); // add ILuaUserData impl builder.Append( $$""" global::Lua.LuaTable? global::Lua.ILuaUserData.Metatable { get { if (__metatable != null) return __metatable; __metatable = new(); __metatable[global::Lua.Runtime.Metamethods.Index] = __metamethod_index; __metatable[global::Lua.Runtime.Metamethods.NewIndex] = __metamethod_newindex; return __metatable; } set { __metatable = value; } } static global::Lua.LuaTable? __metatable; public static implicit operator global::Lua.LuaValue({{typeMetadata.FullTypeName}} value) { return new(value); } """, false); if (!TryEmitMethods(typeMetadata, builder, context)) { return false; } if (!TryEmitIndexMetamethod(typeMetadata, builder, context)) { return false; } if (!TryEmitNewIndexMetamethod(typeMetadata, builder, context)) { return false; } if (!ns.IsGlobalNamespace) builder.EndBlock(); builder.AppendLine("#pragma warning restore CS0162 // Unreachable code"); builder.AppendLine("#pragma warning restore CS0219 // Variable assigned but never used"); builder.AppendLine("#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type."); builder.AppendLine("#pragma warning restore CS8601 // Possible null reference assignment"); builder.AppendLine("#pragma warning restore CS8602 // Possible null return"); builder.AppendLine("#pragma warning restore CS8604 // Possible null reference argument for parameter"); builder.AppendLine("#pragma warning restore CS8631 // The type cannot be used as type parameter in the generic type or method"); return true; } catch (Exception) { return false; } } static bool ValidateMembers(TypeMetadata typeMetadata, Compilation compilation, SymbolReferences references, in SourceProductionContext context) { var error = true; foreach (var property in typeMetadata.Properties) { if (SymbolEqualityComparer.Default.Equals(property.Type, references.LuaValue)) continue; if (SymbolEqualityComparer.Default.Equals(property.Type, typeMetadata.Symbol)) continue; var conversion = compilation.ClassifyConversion(property.Type, references.LuaValue); if (!conversion.Exists) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.InvalidPropertyType, property.Symbol.Locations.FirstOrDefault(), property.Type.Name)); error = false; } } foreach (var method in typeMetadata.Methods) { foreach (var typeSymbol in method.Symbol.Parameters .Select(x => x.Type) .Append(method.Symbol.ReturnType)) { if (SymbolEqualityComparer.Default.Equals(typeSymbol, references.LuaValue)) continue; if (SymbolEqualityComparer.Default.Equals(typeSymbol, typeMetadata.Symbol)) continue; var conversion = compilation.ClassifyConversion(typeSymbol, references.LuaValue); if (!conversion.Exists) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.InvalidMethodType, typeSymbol.Locations.FirstOrDefault(), typeSymbol.Name)); error = false; } } } return error; } static bool TryEmitIndexMetamethod(TypeMetadata typeMetadata, CodeBuilder builder, in SourceProductionContext context) { builder.AppendLine("static readonly global::Lua.LuaFunction __metamethod_index = new global::Lua.LuaFunction((context, buffer, ct) =>"); using (builder.BeginBlockScope()) { builder.AppendLine($"var userData = context.GetArgument<{typeMetadata.FullTypeName}>(0);"); builder.AppendLine($"var key = context.GetArgument(1);"); builder.AppendLine("var result = key switch"); using (builder.BeginBlockScope()) { foreach (var propertyMetadata in typeMetadata.Properties) { if (propertyMetadata.IsStatic) { builder.AppendLine(@$"""{propertyMetadata.LuaMemberName}"" => new global::Lua.LuaValue({typeMetadata.FullTypeName}.{propertyMetadata.Symbol.Name}),"); } else { builder.AppendLine(@$"""{propertyMetadata.LuaMemberName}"" => new global::Lua.LuaValue(userData.{propertyMetadata.Symbol.Name}),"); } } foreach (var methodMetadata in typeMetadata.Methods) { builder.AppendLine(@$"""{methodMetadata.LuaMemberName}"" => new global::Lua.LuaValue(__function_{methodMetadata.LuaMemberName}),"); } builder.AppendLine(@$"_ => global::Lua.LuaValue.Nil,"); } builder.AppendLine(";"); builder.AppendLine("buffer.Span[0] = result;"); builder.AppendLine("return new(1);"); } builder.AppendLine(");"); return true; } static bool TryEmitNewIndexMetamethod(TypeMetadata typeMetadata, CodeBuilder builder, in SourceProductionContext context) { builder.AppendLine("static readonly global::Lua.LuaFunction __metamethod_newindex = new global::Lua.LuaFunction((context, buffer, ct) =>"); using (builder.BeginBlockScope()) { builder.AppendLine($"var userData = context.GetArgument<{typeMetadata.FullTypeName}>(0);"); builder.AppendLine($"var key = context.GetArgument(1);"); builder.AppendLine("switch (key)"); using (builder.BeginBlockScope()) { foreach (var propertyMetadata in typeMetadata.Properties) { builder.AppendLine(@$"case ""{propertyMetadata.LuaMemberName}"":"); using (builder.BeginIndentScope()) { if (propertyMetadata.IsReadOnly) { builder.AppendLine($@"throw new global::Lua.LuaRuntimeException(context.State.GetTraceback(), $""'{{key}}' cannot overwrite."");"); } else if (propertyMetadata.IsStatic) { builder.AppendLine(@$"{typeMetadata.FullTypeName}.{propertyMetadata.Symbol.Name} = context.GetArgument<{propertyMetadata.TypeFullName}>(2);"); builder.AppendLine("break;"); } else { builder.AppendLine(@$"userData.{propertyMetadata.Symbol.Name} = context.GetArgument<{propertyMetadata.TypeFullName}>(2);"); builder.AppendLine("break;"); } } } foreach (var methodMetadata in typeMetadata.Methods) { builder.AppendLine(@$"case ""{methodMetadata.LuaMemberName}"":"); using (builder.BeginIndentScope()) { builder.AppendLine($@"throw new global::Lua.LuaRuntimeException(context.State.GetTraceback(), $""'{{key}}' cannot overwrite."");"); } } builder.AppendLine(@$"default:"); using (builder.BeginIndentScope()) { builder.AppendLine(@$"throw new global::Lua.LuaRuntimeException(context.State.GetTraceback(), $""'{{key}}' not found."");"); } } builder.AppendLine("return new(0);"); } builder.AppendLine(");"); return true; } static bool TryEmitMethods(TypeMetadata typeMetadata, CodeBuilder builder, in SourceProductionContext context) { builder.AppendLine(); foreach (var methodMetadata in typeMetadata.Methods) { builder.AppendLine($"static readonly global::Lua.LuaFunction __function_{methodMetadata.LuaMemberName} = new global::Lua.LuaFunction((context, buffer, ct) =>"); using (builder.BeginBlockScope()) { var index = 0; foreach (var parameter in methodMetadata.Symbol.Parameters) { builder.AppendLine($"var arg{index} = context.GetArgument<{parameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>({index});"); index++; } if (methodMetadata.IsStatic) { builder.Append($"var result = {typeMetadata.FullTypeName}.{methodMetadata.Symbol.Name}("); builder.Append(string.Join(",", Enumerable.Range(0, index).Select(x => $"arg{x}"))); builder.AppendLine(");"); } else { builder.Append($"var result = userData.{methodMetadata.Symbol.Name}("); builder.Append(string.Join(",", Enumerable.Range(1, index).Select(x => $"arg{x}"))); builder.AppendLine(");"); } builder.AppendLine("buffer.Span[0] = new global::Lua.LuaValue(result);"); builder.AppendLine("return new(1);"); } builder.AppendLine(");"); } return true; } }