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"); var metamethodSet = new HashSet(); if (!TryEmitMethods(typeMetadata, builder, metamethodSet, context)) { return false; } if (!TryEmitIndexMetamethod(typeMetadata, builder, context)) { return false; } if (!TryEmitNewIndexMetamethod(typeMetadata, builder, context)) { return false; } if (!TryEmitMetatable(builder, metamethodSet, context)) { return false; } // implicit operator builder.AppendLine($"public static implicit operator global::Lua.LuaValue({typeMetadata.FullTypeName} value)"); using (builder.BeginBlockScope()) { builder.AppendLine("return new(value);"); } 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 isValid = 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)); isValid = false; } } foreach (var method in typeMetadata.Methods) { if (!method.Symbol.ReturnsVoid) { var typeSymbol = method.Symbol.ReturnType; if (method.IsAsync) { var namedType = (INamedTypeSymbol)typeSymbol; if (namedType.TypeArguments.Length == 0) goto PARAMETERS; typeSymbol = namedType.TypeArguments[0]; } if (SymbolEqualityComparer.Default.Equals(typeSymbol, references.LuaValue)) goto PARAMETERS; if (SymbolEqualityComparer.Default.Equals(typeSymbol, typeMetadata.Symbol)) goto PARAMETERS; var conversion = compilation.ClassifyConversion(typeSymbol, references.LuaValue); if (!conversion.Exists) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.InvalidReturnType, typeSymbol.Locations.FirstOrDefault(), typeSymbol.Name)); isValid = false; } } PARAMETERS: foreach (var typeSymbol in method.Symbol.Parameters .Select(x => x.Type)) { 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.InvalidParameterType, typeSymbol.Locations.FirstOrDefault(), typeSymbol.Name)); isValid = false; } } } return isValid; } 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 .Where(x => x.HasMemberAttribute)) { 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 .Where(x => x.HasMemberAttribute)) { 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, HashSet metamethodSet, in SourceProductionContext context) { builder.AppendLine(); foreach (var methodMetadata in typeMetadata.Methods) { string? functionName = null; if (methodMetadata.HasMemberAttribute) { functionName = $"__function_{methodMetadata.LuaMemberName}"; EmitMethodFunction(functionName, typeMetadata, methodMetadata, builder); } if (methodMetadata.HasMetamethodAttribute) { if (!metamethodSet.Add(methodMetadata.Metamethod)) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.DuplicateMetamethod, methodMetadata.Symbol.Locations.FirstOrDefault(), typeMetadata.TypeName, methodMetadata.Metamethod )); continue; } if (functionName == null) { EmitMethodFunction($"__metamethod_{methodMetadata.Metamethod}", typeMetadata, methodMetadata, builder); } else { builder.AppendLine($"static global::Lua.LuaFunction __metamethod_{methodMetadata.Metamethod} => {functionName};"); } } } return true; } static void EmitMethodFunction(string functionName, TypeMetadata typeMetadata, MethodMetadata methodMetadata, CodeBuilder builder) { builder.AppendLine($"static readonly global::Lua.LuaFunction {functionName} = new global::Lua.LuaFunction({(methodMetadata.IsAsync ? "async" : "")} (context, buffer, ct) =>"); using (builder.BeginBlockScope()) { var index = 0; if (!methodMetadata.IsStatic) { builder.AppendLine($"var userData = context.GetArgument<{typeMetadata.FullTypeName}>(0);"); index++; } foreach (var parameter in methodMetadata.Symbol.Parameters) { builder.AppendLine($"var arg{index} = context.GetArgument<{parameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>({index});"); index++; } if (methodMetadata.HasReturnValue) { builder.Append("var result = "); } if (methodMetadata.IsAsync) { builder.Append("await ", false); } if (methodMetadata.IsStatic) { builder.Append($"{typeMetadata.FullTypeName}.{methodMetadata.Symbol.Name}(", false); builder.Append(string.Join(",", Enumerable.Range(0, index).Select(x => $"arg{x}")), false); builder.AppendLine(");", false); } else { builder.Append($"userData.{methodMetadata.Symbol.Name}("); builder.Append(string.Join(",", Enumerable.Range(1, index - 1).Select(x => $"arg{x}")), false); builder.AppendLine(");", false); } if (methodMetadata.HasReturnValue) { builder.AppendLine("buffer.Span[0] = new global::Lua.LuaValue(result);"); builder.AppendLine($"return {(methodMetadata.IsAsync ? "1" : "new(1)")};"); } else { builder.AppendLine($"return {(methodMetadata.IsAsync ? "0" : "new(0)")};"); } } builder.AppendLine(");"); builder.AppendLine(); } static bool TryEmitMetatable(CodeBuilder builder, IEnumerable metamethods, in SourceProductionContext context) { builder.AppendLine("global::Lua.LuaTable? global::Lua.ILuaUserData.Metatable"); using (builder.BeginBlockScope()) { builder.AppendLine("get"); using (builder.BeginBlockScope()) { builder.AppendLine("if (__metatable != null) return __metatable;"); builder.AppendLine(); builder.AppendLine("__metatable = new();"); builder.AppendLine("__metatable[global::Lua.Runtime.Metamethods.Index] = __metamethod_index;"); builder.AppendLine("__metatable[global::Lua.Runtime.Metamethods.NewIndex] = __metamethod_newindex;"); foreach (var metamethod in metamethods) { builder.AppendLine($"__metatable[global::Lua.Runtime.Metamethods.{metamethod}] = __metamethod_{metamethod};"); } builder.AppendLine("return __metatable;"); } builder.AppendLine("set"); using (builder.BeginBlockScope()) { builder.AppendLine("__metatable = value;"); } } builder.AppendLine("static global::Lua.LuaTable? __metatable;"); builder.AppendLine(); return true; } }