Skip to content

Commit ec08d8b

Browse files
authored
Merge pull request #379 from nuskey8/codex/pr-lua52-vm-semantics
fix: align Lua 5.2 VM equality, modulo and loop rules
2 parents aad8b03 + bb83114 commit ec08d8b

6 files changed

Lines changed: 143 additions & 18 deletions

File tree

‎src/Lua/LuaValue.cs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -769,8 +769,9 @@ CancellationToken cancellationToken
769769
var count = await LuaVirtualMachine.Call(state, top, top, cancellationToken);
770770
var result = count > 0 ? stack.Get(top) : Nil;
771771
stack.SetTop(top + 1);
772-
// Return one value, padding missing results and discarding extras.
773-
stack.FastGet(top) = result;
772+
// Lua 5.2 converts a numeric __tostring result to text.
773+
stack.FastGet(top) =
774+
result.Type == LuaValueType.Number ? new LuaValue(result.ToString()) : result;
774775
return 1;
775776
}
776777
catch

‎src/Lua/Runtime/LuaVirtualMachine.cs‎

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,21 @@ out doRestart
810810
continue;
811811
}
812812

813+
// Lua 5.2 only invokes __eq for tables/userdata of the
814+
// same type. Primitive inequality needs no slow-path call.
815+
if (
816+
vb.Type != vc.Type
817+
|| vb.Type is not (LuaValueType.Table or LuaValueType.UserData)
818+
)
819+
{
820+
if (iA == 1)
821+
{
822+
context.Pc++;
823+
}
824+
825+
continue;
826+
}
827+
813828
if (
814829
ExecuteCompareOperationMetaMethod(
815830
vb,
@@ -950,7 +965,7 @@ out doRestart
950965
var step = Unsafe.Add(ref indexRef, 2).UnsafeReadDouble();
951966
var index = indexRef.UnsafeReadDouble() + step;
952967

953-
if (step >= 0 ? index <= limit : limit <= index)
968+
if (step > 0 ? index <= limit : limit <= index)
954969
{
955970
context.Pc += instruction.SBx;
956971
indexRef = index;
@@ -1128,13 +1143,9 @@ OpCode opcode
11281143
[MethodImpl(MethodImplOptions.NoInlining)]
11291144
internal static double Mod(double a, double b)
11301145
{
1131-
var mod = a % b;
1132-
if ((b > 0 && mod < 0) || (b < 0 && mod > 0))
1133-
{
1134-
mod += b;
1135-
}
1136-
1137-
return mod;
1146+
// Lua 5.2 uses this expression, including its floating-point rounding.
1147+
// A sign-adjusted remainder differs for values such as 5 and 0.1.
1148+
return a - (Math.Floor(a / b) * b);
11381149
}
11391150

11401151
static void SelfPostOperation(
@@ -2810,6 +2821,32 @@ CancellationToken cancellationToken
28102821
}
28112822
}
28122823

2824+
static bool TryGetCompareMetamethod(
2825+
LuaGlobalState state,
2826+
LuaValue left,
2827+
LuaValue right,
2828+
string name,
2829+
out LuaValue metamethod
2830+
)
2831+
{
2832+
if (name == Metamethods.Eq)
2833+
{
2834+
metamethod = default;
2835+
return left.Type == right.Type
2836+
&& left.Type is LuaValueType.Table or LuaValueType.UserData
2837+
&& state.TryGetMetatable(left, out var leftMetatable)
2838+
&& leftMetatable.TryGetValue(name, out metamethod)
2839+
&& state.TryGetMetatable(right, out var rightMetatable)
2840+
&& (
2841+
leftMetatable == rightMetatable
2842+
|| rightMetatable.TryGetValue(name, out var other) && metamethod == other
2843+
);
2844+
}
2845+
2846+
return left.TryGetMetamethod(state, name, out metamethod)
2847+
|| right.TryGetMetamethod(state, name, out metamethod);
2848+
}
2849+
28132850
[MethodImpl(MethodImplOptions.NoInlining)]
28142851
static bool ExecuteCompareOperationMetaMethod(
28152852
LuaValue vb,
@@ -2823,10 +2860,7 @@ out bool doRestart
28232860
doRestart = false;
28242861
var reverseLe = false;
28252862
ReCheck:
2826-
if (
2827-
vb.TryGetMetamethod(context.GlobalState, name, out var metamethod)
2828-
|| vc.TryGetMetamethod(context.GlobalState, name, out metamethod)
2829-
)
2863+
if (TryGetCompareMetamethod(context.GlobalState, vb, vc, name, out var metamethod))
28302864
{
28312865
var stack = context.Stack;
28322866
var newBase = stack.Count;
@@ -2959,10 +2993,7 @@ CancellationToken cancellationToken
29592993
var (name, description) = opCode.GetNameAndDescription();
29602994
var reverseLe = false;
29612995
ReCheck:
2962-
if (
2963-
vb.TryGetMetamethod(state.GlobalState, name, out var metamethod)
2964-
|| vc.TryGetMetamethod(state.GlobalState, name, out metamethod)
2965-
)
2996+
if (TryGetCompareMetamethod(state.GlobalState, vb, vc, name, out var metamethod))
29662997
{
29672998
var stack = state.Stack;
29682999
var top = stack.Count;

‎tests/Lua.Tests/ArithmeticExecutionRegressionTests.cs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,21 @@ public async Task ArithmeticBytecodeRoundTripAndSpecialNumbers()
8585
var results = await state.ExecuteAsync(restored);
8686
Assert.That(results[0].Read<double>(), Is.EqualTo(8));
8787
}
88+
89+
[Test]
90+
public async Task ZeroStepNumericForUsesLua52Direction()
91+
{
92+
using var state = CreateState();
93+
await state.DoStringAsync(
94+
"""
95+
local count = 0
96+
for i = 1, 2, 0 do count = count + 1; break end
97+
assert(count == 0)
98+
for i = 2, 1, 0 do count = count + 1; break end
99+
assert(count == 1)
100+
for i = 1, 1, -0.0 do count = count + 1; break end
101+
assert(count == 2)
102+
"""
103+
);
104+
}
88105
}

‎tests/Lua.Tests/CompilerTests.cs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,16 @@ public async Task SyntaxDepthFailureRestoresCounter()
126126
}
127127
Assert.That((await state.DoStringAsync("return 42"))[0].Read<double>(), Is.EqualTo(42));
128128
}
129+
130+
[TestCase("5", "0.1")]
131+
[TestCase("1e308", "3")]
132+
[TestCase("-5", "0.1")]
133+
[TestCase("5", "-0.1")]
134+
public async Task FoldedAndRuntimeModuloUseLua52Formula(string a, string b)
135+
{
136+
using var state = CreateState();
137+
var results = await state.DoStringAsync($"local a,b={a},{b}; return {a}%{b},a%b");
138+
Assert.That(results[0].Read<double>(), Is.Zero);
139+
Assert.That(results[1].Read<double>(), Is.Zero);
140+
}
129141
}

‎tests/Lua.Tests/NativeCallbackResultTests.cs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,30 @@ public async Task ManagedCallbacksDistinguishNoResultsFromNil(
203203
Assert.That(state.Stack.Pop().Read<double>(), Is.EqualTo(12345));
204204
Assert.That(state.CallStackFrameCount, Is.Zero);
205205
}
206+
207+
[Test]
208+
public async Task ToStringUsesOneStringResult([Values] bool hooks, [Values] bool suspend)
209+
{
210+
using var state = CreateState();
211+
state.Environment["format"] = new LuaFunction(
212+
async (context, token) =>
213+
{
214+
if (suspend)
215+
await Task.Yield();
216+
return context.Return(12, "extra");
217+
}
218+
);
219+
if (hooks)
220+
await state.DoStringAsync("debug.sethook(function() end,'cr')");
221+
await state.DoStringAsync(
222+
"""
223+
local x=setmetatable({}, {__tostring=format})
224+
assert(select('#',tostring(x))==1)
225+
assert(tostring(x)=='12')
226+
assert(string.format('%s:%s',x,'tail')=='12:tail')
227+
"""
228+
);
229+
Assert.That(state.Stack.Count, Is.Zero);
230+
Assert.That(state.CallStackFrameCount, Is.Zero);
231+
}
206232
}

‎tests/Lua.Tests/VirtualMachineExecutionTests.cs‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,4 +282,42 @@ await state.DoStringAsync(
282282
Assert.That(state.Stack.Pop().Read<double>(), Is.EqualTo(12345));
283283
Assert.That(state.CallStackFrameCount, Is.Zero);
284284
}
285+
286+
[Test]
287+
public async Task EqualityRequiresMatchingTypesAndSharedMetamethod()
288+
{
289+
using var state = CreateState();
290+
await state.DoStringAsync(
291+
"""
292+
local calls = 0
293+
local function eq(a,b) calls=calls+1; return true end
294+
local a = setmetatable({}, {__eq=eq})
295+
local b = {}
296+
assert(not (a == b) and not (b == a) and calls == 0)
297+
setmetatable(b, {__eq=function() return true end})
298+
assert(not (a == b) and calls == 0)
299+
setmetatable(b, {__eq=eq})
300+
assert(a == b and calls == 1)
301+
assert(a == a and calls == 1)
302+
assert(not (a == 1) and not (1 == a) and calls == 1)
303+
debug.setmetatable(1, {__eq=function() error('numeric __eq must not run') end})
304+
local x,y = 1,2
305+
assert(x ~= y)
306+
debug.setmetatable(1, nil)
307+
-- A shared metatable selects even a non-reflexive NaN tag method.
308+
-- Calling it must fail; separate metatables do not share that value.
309+
local nan=0/0
310+
local mt={__eq=nan}
311+
setmetatable(a, mt); setmetatable(b, mt)
312+
assert(not pcall(function() return a == b end))
313+
setmetatable(b, {__eq=nan})
314+
assert(not (a == b))
315+
"""
316+
);
317+
var a = state.Environment["a"] = new LuaTable();
318+
var b = state.Environment["b"] = new LuaTable();
319+
await state.DoStringAsync("setmetatable(a, {__eq=function() return true end})");
320+
Assert.That(await state.EqualsAsync(a, b), Is.False);
321+
Assert.That(await state.EqualsAsync(b, a), Is.False);
322+
}
285323
}

0 commit comments

Comments
 (0)