Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,38 @@ Return types such as `void`, `Task/Task<T>`, `ValueTask/ValueTask<T>`, `UniTask/

If the type is not supported, the Source Generator will output a compile-time error.

### Variable-length arguments

A final `ReadOnlySpan<LuaValue>` or `ReadOnlyMemory<LuaValue>` parameter receives the remaining Lua arguments. An injected `CancellationToken` may follow it. Fixed arguments are read first; an instance method's `self` is excluded, and `out` parameters consume no Lua arguments. No remaining arguments produces an empty view, and `nil` values are preserved.

```cs
[LuaObject]
public partial class Calculator
{
[LuaMember]
public double Sum(double first, ReadOnlySpan<LuaValue> rest)
{
foreach (var value in rest)
first += value.Read<double>();
return first;
}

[LuaMember]
public async Task<int> CountAsync(ReadOnlyMemory<LuaValue> values, CancellationToken ct)
{
await Task.Delay(1, ct);
return values.Length;
}
}
```

```lua
local total = calculator:Sum(1, 2, 3) -- 6
local count = calculator:CountAsync(1, nil, 3) -- 3
```

These are borrowed, read-only views of the Lua argument stack. Use `ReadOnlyMemory<LuaValue>` when an async method needs the values after an `await`. Copy with `ToArray()` if the values must be retained after the method returns. Mutable views, `ref`/`out`/`in` views, other element types and C# `params LuaValue[]` are not supported. Pass the view by value, after any fixed parameters and before an optional injected token.

### LuaMetamethod

By adding the `[LuaMetamethod]` attribute, you can designate a C# method to be used as a Lua metamethod.
Expand Down Expand Up @@ -646,4 +678,4 @@ While `collectgarbage()` is available, it simply calls the corresponding .NET ga

## License

Lua-CSharp is licensed under the [MIT License](LICENSE).
Lua-CSharp is licensed under the [MIT License](LICENSE).
77 changes: 39 additions & 38 deletions src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -441,13 +441,7 @@ typeSymbol is not INamedTypeSymbol namedTypeSymbol
continue;
}

if (
index == method.Symbol.Parameters.Length - 1
&& SymbolEqualityComparer.Default.Equals(
typeSymbol,
references.CancellationToken
)
)
if (index >= method.FixedParameterCount)
{
continue;
}
Expand Down Expand Up @@ -518,15 +512,27 @@ typeSymbol is not INamedTypeSymbol namedTypeSymbol
int contextArgumentIndex
)
{
var parameterIndex = methodMetadata.IsStatic
? contextArgumentIndex
: contextArgumentIndex - 1;
if (parameterIndex < 0 || parameterIndex >= methodMetadata.Symbol.Parameters.Length)
var luaArgumentIndex = methodMetadata.IsStatic ? 0 : 1;
for (
var parameterIndex = 0;
parameterIndex < methodMetadata.FixedParameterCount;
parameterIndex++
)
{
return null;
var parameter = methodMetadata.Symbol.Parameters[parameterIndex];
if (parameter.RefKind == RefKind.Out)
{
continue;
}
if (luaArgumentIndex == contextArgumentIndex)
{
return parameter;
}
luaArgumentIndex++;
}

return methodMetadata.Symbol.Parameters[parameterIndex];
// Variadic arguments are not a single typed key to preload for __index/__newindex.
return null;
}

static bool IsPreloadableArgumentType(
Expand Down Expand Up @@ -1096,9 +1102,7 @@ static void EmitMethodFunctionBody(
luaArgumentIndex++;
}

var hasCancellationToken = false;

for (var i = 0; i < methodMetadata.Symbol.Parameters.Length; i++)
for (var i = 0; i < methodMetadata.FixedParameterCount; i++)
{
var parameter = methodMetadata.Symbol.Parameters[i];
var parameterType = parameter.Type;
Expand All @@ -1111,18 +1115,6 @@ static void EmitMethodFunctionBody(
references.LuaValue
);

if (
i == methodMetadata.Symbol.Parameters.Length - 1
&& SymbolEqualityComparer.Default.Equals(
parameterType,
references.CancellationToken
)
)
{
hasCancellationToken = true;
break;
}

var allowNull = parameter.ContainsAttribute(references.AllowNullAttribute);

if (parameter.RefKind == RefKind.Out)
Expand Down Expand Up @@ -1231,6 +1223,25 @@ static void EmitMethodFunctionBody(
luaArgumentIndex++;
}

if (methodMetadata.HasParamsLikeParameter)
{
var arguments = methodMetadata.ParamsAsMemory
? "context.ArgumentsMemory"
: "context.Arguments";
// Out parameters consume no Lua arguments; an instance receiver consumes one.
// Missing optional fixed arguments still leave a valid, empty variadic view.
callArguments.Add(
luaArgumentIndex == 0
? arguments
: $"{arguments}.Slice(global::System.Math.Min(context.ArgumentCount, {luaArgumentIndex}))"
);
}

if (methodMetadata.HasCancellationTokenParameter)
{
callArguments.Add("ct");
}

if (methodMetadata.HasReturnValue)
{
builder.Append("var result = ");
Expand All @@ -1249,11 +1260,6 @@ static void EmitMethodFunctionBody(
);
builder.Append(string.Join(",", callArguments), false);

if (hasCancellationToken)
{
builder.Append(callArguments.Count > 0 ? ",ct" : "ct", false);
}

builder.AppendLine(");", false);
}
else
Expand All @@ -1264,11 +1270,6 @@ static void EmitMethodFunctionBody(
);
builder.Append(string.Join(",", callArguments), false);

if (hasCancellationToken)
{
builder.Append(callArguments.Count > 0 ? ",ct" : "ct", false);
}

builder.AppendLine(");", false);
}

Expand Down
50 changes: 50 additions & 0 deletions src/Lua.SourceGenerator/MethodMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class MethodMetadata
public bool HasReturnValue { get; }
public bool HasMemberAttribute { get; set; }
public bool HasMetamethodAttribute { get; set; }
public bool HasCancellationTokenParameter { get; }
public bool HasParamsLikeParameter { get; }
public bool ParamsAsMemory { get; }
public int FixedParameterCount { get; }
public string LuaMemberName { get; }
public LuaObjectMetamethod Metamethod { get; }

Expand Down Expand Up @@ -63,5 +67,51 @@ returnType is INamedTypeSymbol namedType && namedType.ContainingNamespace is { }
metamethodAttribute.ConstructorArguments[0].Value!.ToString()
);
}

var parameters = symbol.Parameters;
var parameterCount = parameters.Length;
if (
parameterCount > 0
&& parameters[parameterCount - 1].RefKind == RefKind.None
&& SymbolEqualityComparer.Default.Equals(
parameters[parameterCount - 1].Type,
references.CancellationToken
)
)
{
HasCancellationTokenParameter = true;
parameterCount--;
}

// A borrowed argument view must be last, except for the injected cancellation token.
// In particular, a token-only method has no preceding parameter to inspect.
if (
parameterCount > 0
&& parameters[parameterCount - 1].RefKind == RefKind.None
&& parameters[parameterCount - 1].Type
is INamedTypeSymbol { TypeArguments.Length: 1 } parameterType
&& SymbolEqualityComparer.Default.Equals(
parameterType.TypeArguments[0],
references.LuaValue
)
)
{
ParamsAsMemory = SymbolEqualityComparer.Default.Equals(
parameterType.OriginalDefinition,
references.ReadOnlyMemory
);
HasParamsLikeParameter =
ParamsAsMemory
|| SymbolEqualityComparer.Default.Equals(
parameterType.OriginalDefinition,
references.ReadOnlySpan
);
if (HasParamsLikeParameter)
{
parameterCount--;
}
}

FixedParameterCount = parameterCount;
}
}
4 changes: 4 additions & 0 deletions src/Lua.SourceGenerator/SymbolReferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public sealed class SymbolReferences
LuaThread = compilation.GetTypeByMetadataName("Lua.LuaThread")!,
LuaTable = compilation.GetTypeByMetadataName("Lua.LuaTable")!,
LuaUserData = compilation.GetTypeByMetadataName("Lua.ILuaUserData")!,
ReadOnlyMemory = compilation.GetTypeByMetadataName("System.ReadOnlyMemory`1")!,
ReadOnlySpan = compilation.GetTypeByMetadataName("System.ReadOnlySpan`1")!,
CancellationToken = compilation.GetTypeByMetadataName(
"System.Threading.CancellationToken"
)!,
Expand All @@ -54,5 +56,7 @@ public sealed class SymbolReferences
public INamedTypeSymbol LuaThread { get; private set; } = null!;
public INamedTypeSymbol LuaTable { get; private set; } = null!;
public INamedTypeSymbol LuaUserData { get; private set; } = null!;
public INamedTypeSymbol ReadOnlyMemory { get; private set; } = null!;
public INamedTypeSymbol ReadOnlySpan { get; private set; } = null!;
public INamedTypeSymbol CancellationToken { get; private set; } = null!;
}
127 changes: 127 additions & 0 deletions tests/Lua.SourceGenerator.Tests/LuaObjectGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,133 @@ namespace Lua.SourceGenerator.Tests;

public class LuaObjectGeneratorTests
{
[TestCase("public static int Read(System.ReadOnlySpan<LuaValue> values) => values.Length;")]
[TestCase("public int Read(System.ReadOnlySpan<LuaValue> values) => values.Length;")]
[TestCase(
"public static int Read(int first, System.ReadOnlySpan<LuaValue> values) => first + values.Length;"
)]
[TestCase(
"public int Read(int first, System.ReadOnlySpan<LuaValue> values) => first + values.Length;"
)]
[TestCase(
"public int Read(System.ReadOnlySpan<LuaValue> values, System.Threading.CancellationToken ct) => values.Length;"
)]
[TestCase("public static int Read(System.ReadOnlyMemory<LuaValue> values) => values.Length;")]
[TestCase("public int Read(System.ReadOnlyMemory<LuaValue> values) => values.Length;")]
[TestCase(
"public static int Read(int first, System.ReadOnlyMemory<LuaValue> values, System.Threading.CancellationToken ct) => first + values.Length;"
)]
[TestCase(
"public int Read(int first, System.ReadOnlyMemory<LuaValue> values, System.Threading.CancellationToken ct) => first + values.Length;"
)]
[TestCase(
"public int Read(int first = 1, System.ReadOnlySpan<LuaValue> values = default) => first + values.Length;"
)]
[TestCase(
"public int Read(out int extra, ref int first, System.ReadOnlyMemory<LuaValue> values) { extra = first++; return values.Length; }"
)]
[TestCase(
"public System.Threading.Tasks.Task<int> Read(System.ReadOnlyMemory<LuaValue> values) => System.Threading.Tasks.Task.FromResult(values.Length);"
)]
[TestCase(
"public System.Threading.Tasks.ValueTask<int> Read(System.ReadOnlyMemory<LuaValue> values) => new System.Threading.Tasks.ValueTask<int>(values.Length);"
)]
[TestCase(
"public System.Threading.Tasks.Task<int> Read(System.ReadOnlySpan<LuaValue> values) => System.Threading.Tasks.Task.FromResult(values.Length);"
)]
[TestCase("public void Read(System.Threading.CancellationToken ct) { }")]
public void VariadicParameters_GenerateWithoutDiagnostics(string method)
{
var result = CompileGeneratedSource(
$$"""
using Lua;
[LuaObject]
internal partial class VariadicObject
{
[LuaMember] {{method}}
}
internal static class Consumer
{
public static LuaValue Convert() => new VariadicObject();
}
"""
);
Assert.That(result.Success, Is.True, string.Join("\n", result.Diagnostics));
Assert.That(result.Diagnostics, Is.Empty);
}

[TestCase(
"System.ReadOnlySpan<LuaValue> values, int last",
"System.ReadOnlySpan<Lua.LuaValue>"
)]
[TestCase(
"System.ReadOnlyMemory<LuaValue> values, int last",
"System.ReadOnlyMemory<Lua.LuaValue>"
)]
[TestCase("System.ReadOnlySpan<int> values", "System.ReadOnlySpan<int>")]
[TestCase("System.ReadOnlyMemory<int> values", "System.ReadOnlyMemory<int>")]
[TestCase("System.Span<LuaValue> values", "System.Span<Lua.LuaValue>")]
[TestCase("System.Memory<LuaValue> values", "System.Memory<Lua.LuaValue>")]
[TestCase("ref System.ReadOnlySpan<LuaValue> values", "System.ReadOnlySpan<Lua.LuaValue>")]
[TestCase("out System.ReadOnlyMemory<LuaValue> values", "System.ReadOnlyMemory<Lua.LuaValue>")]
[TestCase("in System.ReadOnlyMemory<LuaValue> values", "System.ReadOnlyMemory<Lua.LuaValue>")]
[TestCase("System.ReadOnlyMemory<LuaValue>? values", "System.ReadOnlyMemory<Lua.LuaValue>")]
[TestCase("params LuaValue[] values", "Lua.LuaValue[]")]
public void VariadicParameters_RejectUnsupportedShapes(string parameters, string typeName)
{
var result = CompileGeneratedSource(
$$"""
using Lua;
[LuaObject]
internal partial class UnsupportedObject
{
[LuaMember] public void Read({{parameters}}) => throw new System.NotSupportedException();
}
[LuaObject]
internal partial class HealthyObject
{
[LuaMember] public int Value => 1;
}
internal static class Consumer
{
public static LuaValue Convert() => new HealthyObject();
}
"""
);
AssertUnsupportedMemberDiagnostic(
result.Diagnostics,
result.UserSource,
"LUACS006",
"values",
typeName
);
}

[TestCase("System.ReadOnlySpan<LuaValue>")]
[TestCase("System.ReadOnlyMemory<LuaValue>")]
public void VariadicParameters_StillValidateAllowNull(string type)
{
var result = CompileGeneratedSource(
$$"""
using Lua;
[LuaObject]
internal partial class UnsupportedObject
{
[LuaMember] public void Read([System.Diagnostics.CodeAnalysis.AllowNull] {{type}} values) { }
}
[LuaObject]
internal partial class HealthyObject { }
"""
);
AssertUnsupportedMemberDiagnostic(
result.Diagnostics,
result.UserSource,
"LUACS008",
"values",
"values"
);
}

[TestCase("LuaValue[]", "System.Array.Empty<LuaValue>()", "Lua.LuaValue[]")]
[TestCase("LuaValue[,]", "new LuaValue[0, 0]", "Lua.LuaValue[*,*]")]
[TestCase("int[]", "System.Array.Empty<int>()", "int[]")]
Expand Down
Loading
Loading