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;
}
}