From 74e7cf7a4c6ccb34c00300f83369282c9abaa4bf Mon Sep 17 00:00:00 2001 From: Robbie Lyman Date: Mon, 21 Jul 2025 18:25:14 -0400 Subject: [PATCH 1/9] feat: add pushNumeric, toNumeric and checkNumeric These functions rely (lightly) on Zig's comptime to minimize some of the annoying overhead of writing `@intCast` and `@floatCast` all the time by making those builtins part of the function definition. However, since those builtins assert in builds with runtime safety enabled, these functions will crash the program if called with bad Lua input. More discussion is warranted about the tradeoffs of going this route before merging. Resolves #172. --- src/lib.zig | 38 ++++++++++++++++++++++++++++++++++++++ src/tests.zig | 10 ++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/lib.zig b/src/lib.zig index 53df4c1..00a3709 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -2035,6 +2035,18 @@ pub const Lua = opaque { c.lua_pushnil(@ptrCast(lua)); } + /// Pushes a numeric type with value `n` onto the stack + /// The conversion from the type of `n` to `Integer` or `Number` + /// is performed with `@_Cast` so will assert in modes with runtime safety enabled. + /// + /// * Pops: `0` + /// * Pushes: `1` + /// * Errors: `never` + pub fn pushNumeric(lua: *Lua, n: anytype) void { + if (@typeInfo(@TypeOf(n)) == .int) return lua.pushInteger(@intCast(n)); + lua.pushNumber(@floatCast(n)); + } + /// Pushes a float with value `n` onto the stack /// /// * Pops: `0` @@ -2594,6 +2606,19 @@ pub const Lua = opaque { c.lua_toclose(@ptrCast(lua), index); } + /// Converts the Lua value at the given `index` to a numeric type; + /// if T is an integer type, the Lua value is converted to an integer. + /// The conversion from `Integer` or `Number` to T is performed with `@_Cast`, + /// which will assert in builds with runtime safety enabled + /// + /// * Pops: `0` + /// * Pushes: `0` + /// * Errors: `never` + pub fn toNumeric(lua: *Lua, comptime T: type, index: i32) !T { + if (@typeInfo(T) == .int) return @intCast(try lua.toInteger(index)); + return @floatCast(try lua.toNumber(index)); + } + /// Converts the Lua value at the given `index` to a signed integer /// The Lua value must be an integer, or a number, or a string convertible to an integer /// Returns an error if the conversion failed @@ -3342,6 +3367,19 @@ pub const Lua = opaque { c.luaL_checkany(@ptrCast(lua), arg); } + /// Checks whether the function argument `arg` is a numeric type and converts it to type T + /// + /// The conversion is done with `@intCast` for numeric types, + /// so causes an assertion in modes with runtime safety enabled. + /// + /// * Pops: `0` + /// * Pushes: `0` + /// * Errors: `explained in text / on purpose` + pub fn checkNumeric(lua: *Lua, comptime T: type, arg: i32) T { + if (@typeInfo(T) == .int) return @intCast(lua.checkInteger(arg)); + return @floatCast(lua.checkNumber(arg)); + } + /// Checks whether the function argument `arg` is a number and returns this number cast to an i32 /// /// Not available in Lua 5.3 and 5.4 diff --git a/src/tests.zig b/src/tests.zig index 21cf279..6ee274c 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -3055,3 +3055,13 @@ test "error union for CFn" { try expectEqualStrings("MissingInteger", try lua.toString(-1)); }; } + +test "pushNumeric and toNumeric" { + const lua: *Lua = try .init(testing.allocator); + defer lua.deinit(); + + const num: u32 = 100; + lua.pushNumeric(num); + const pull = lua.toNumeric(u32, lua.getTop()); + try std.testing.expectEqual(num, pull); +} From 0ffdfc05d6853cb6e93f4954905e5aff866e1b7d Mon Sep 17 00:00:00 2001 From: Robbie Lyman Date: Tue, 19 Aug 2025 09:54:01 -0400 Subject: [PATCH 2/9] fix: remove pushNumeric --- src/lib.zig | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/lib.zig b/src/lib.zig index 00a3709..3355658 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -2035,18 +2035,6 @@ pub const Lua = opaque { c.lua_pushnil(@ptrCast(lua)); } - /// Pushes a numeric type with value `n` onto the stack - /// The conversion from the type of `n` to `Integer` or `Number` - /// is performed with `@_Cast` so will assert in modes with runtime safety enabled. - /// - /// * Pops: `0` - /// * Pushes: `1` - /// * Errors: `never` - pub fn pushNumeric(lua: *Lua, n: anytype) void { - if (@typeInfo(@TypeOf(n)) == .int) return lua.pushInteger(@intCast(n)); - lua.pushNumber(@floatCast(n)); - } - /// Pushes a float with value `n` onto the stack /// /// * Pops: `0` From 3ea22a692964b3ef1d21d65efb2555cdeb8f1df6 Mon Sep 17 00:00:00 2001 From: Robbie Lyman Date: Tue, 19 Aug 2025 09:54:43 -0400 Subject: [PATCH 3/9] fix: checkNumeric raises a lua error --- src/lib.zig | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/lib.zig b/src/lib.zig index 3355658..6ae3395 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -3357,14 +3357,26 @@ pub const Lua = opaque { /// Checks whether the function argument `arg` is a numeric type and converts it to type T /// - /// The conversion is done with `@intCast` for numeric types, - /// so causes an assertion in modes with runtime safety enabled. + /// Raises a Lua error if the argument is an integer type but std.math.cast fails /// /// * Pops: `0` /// * Pushes: `0` /// * Errors: `explained in text / on purpose` pub fn checkNumeric(lua: *Lua, comptime T: type, arg: i32) T { - if (@typeInfo(T) == .int) return @intCast(lua.checkInteger(arg)); + if (@typeInfo(T) == .int) return std.math.cast(T, lua.checkNumber(arg)) orelse { + const error_msg = comptime msg: { + var buf: [1024]u8 = undefined; + const info = @typeInfo(T).int; + const signedness = switch (info.signedness) { + .unsigned => "u", + .signed => "i", + }; + break :msg std.fmt.bufPrintZ(&buf, "Integer argument doesn't fit inside {s}{d} range [{d}, {d}]", .{ + signedness, info.bits, std.math.minInt(T), std.math.maxInt(T), + }) catch unreachable; + }; + lua.argError(arg, error_msg); + }; return @floatCast(lua.checkNumber(arg)); } From ffa220566cb8e06b055c64c5a8c8e729a178e6b3 Mon Sep 17 00:00:00 2001 From: Robbie Lyman Date: Tue, 19 Aug 2025 09:59:25 -0400 Subject: [PATCH 4/9] fix: toNumeric raises a Zig error --- src/lib.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib.zig b/src/lib.zig index 6ae3395..143bf67 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -2596,14 +2596,12 @@ pub const Lua = opaque { /// Converts the Lua value at the given `index` to a numeric type; /// if T is an integer type, the Lua value is converted to an integer. - /// The conversion from `Integer` or `Number` to T is performed with `@_Cast`, - /// which will assert in builds with runtime safety enabled /// /// * Pops: `0` /// * Pushes: `0` - /// * Errors: `never` + /// * Errors: `error.IntegerCastFailed` if `T` is an integer type and the value at index doesn't fit pub fn toNumeric(lua: *Lua, comptime T: type, index: i32) !T { - if (@typeInfo(T) == .int) return @intCast(try lua.toInteger(index)); + if (@typeInfo(T) == .int) return std.math.cast(try lua.toInteger(index)) orelse error.IntegerCastFailed; return @floatCast(try lua.toNumber(index)); } From 84076570f0534774ab0dbe4ac82a0f867db3d90d Mon Sep 17 00:00:00 2001 From: Robbie Lyman Date: Tue, 19 Aug 2025 10:02:25 -0400 Subject: [PATCH 5/9] fix: remove pushNumeric from test, typo in toNumeric --- src/lib.zig | 2 +- src/tests.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.zig b/src/lib.zig index 143bf67..a2a94be 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -2601,7 +2601,7 @@ pub const Lua = opaque { /// * Pushes: `0` /// * Errors: `error.IntegerCastFailed` if `T` is an integer type and the value at index doesn't fit pub fn toNumeric(lua: *Lua, comptime T: type, index: i32) !T { - if (@typeInfo(T) == .int) return std.math.cast(try lua.toInteger(index)) orelse error.IntegerCastFailed; + if (@typeInfo(T) == .int) return std.math.cast(T, try lua.toInteger(index)) orelse error.IntegerCastFailed; return @floatCast(try lua.toNumber(index)); } diff --git a/src/tests.zig b/src/tests.zig index 6ee274c..05ff570 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -3061,7 +3061,7 @@ test "pushNumeric and toNumeric" { defer lua.deinit(); const num: u32 = 100; - lua.pushNumeric(num); + lua.pushInteger(num); const pull = lua.toNumeric(u32, lua.getTop()); try std.testing.expectEqual(num, pull); } From 436ceb743560b1d6c514ed6e8d9968f4254cc200 Mon Sep 17 00:00:00 2001 From: Robbie Lyman Date: Tue, 19 Aug 2025 14:54:29 -0400 Subject: [PATCH 6/9] fix: error.IntegerCastFailed -> error.Overflow --- src/lib.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib.zig b/src/lib.zig index a2a94be..7c2e01b 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -2599,9 +2599,11 @@ pub const Lua = opaque { /// /// * Pops: `0` /// * Pushes: `0` - /// * Errors: `error.IntegerCastFailed` if `T` is an integer type and the value at index doesn't fit + /// * Errors: `error.Overflow` if `T` is an integer type and the value at index doesn't fit pub fn toNumeric(lua: *Lua, comptime T: type, index: i32) !T { - if (@typeInfo(T) == .int) return std.math.cast(T, try lua.toInteger(index)) orelse error.IntegerCastFailed; + if (@typeInfo(T) == .int) { + return std.math.cast(T, try lua.toInteger(index)) orelse error.Overflow; + } return @floatCast(try lua.toNumber(index)); } From 47d4e389316ad36278e29d8dc1de0cc6e006cc92 Mon Sep 17 00:00:00 2001 From: Robbie Lyman Date: Tue, 19 Aug 2025 14:55:36 -0400 Subject: [PATCH 7/9] fix: add test for checkNumeric --- src/lib.zig | 9 +++++---- src/tests.zig | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/lib.zig b/src/lib.zig index 7c2e01b..14adbdb 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -3363,7 +3363,8 @@ pub const Lua = opaque { /// * Pushes: `0` /// * Errors: `explained in text / on purpose` pub fn checkNumeric(lua: *Lua, comptime T: type, arg: i32) T { - if (@typeInfo(T) == .int) return std.math.cast(T, lua.checkNumber(arg)) orelse { + if (comptime @typeInfo(T) != .int) return @floatCast(lua.checkNumber(arg)); + return std.math.cast(T, lua.checkInteger(arg)) orelse { const error_msg = comptime msg: { var buf: [1024]u8 = undefined; const info = @typeInfo(T).int; @@ -3371,13 +3372,13 @@ pub const Lua = opaque { .unsigned => "u", .signed => "i", }; - break :msg std.fmt.bufPrintZ(&buf, "Integer argument doesn't fit inside {s}{d} range [{d}, {d}]", .{ + const output = std.fmt.bufPrintZ(&buf, "integer argument doesn't fit inside {s}{d} range [{d}, {d}]", .{ signedness, info.bits, std.math.minInt(T), std.math.maxInt(T), }) catch unreachable; + break :msg output[0..output.len :0].*; }; - lua.argError(arg, error_msg); + lua.argError(arg, &error_msg); }; - return @floatCast(lua.checkNumber(arg)); } /// Checks whether the function argument `arg` is a number and returns this number cast to an i32 diff --git a/src/tests.zig b/src/tests.zig index 05ff570..694afc1 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -3056,12 +3056,38 @@ test "error union for CFn" { }; } -test "pushNumeric and toNumeric" { +test "checkNumeric and toNumeric" { + const error_msg = "integer argument doesn't fit inside u8 range [0, 255]"; + const lua: *Lua = try .init(testing.allocator); defer lua.deinit(); - const num: u32 = 100; - lua.pushInteger(num); - const pull = lua.toNumeric(u32, lua.getTop()); - try std.testing.expectEqual(num, pull); + lua.pushFunction(zlua.wrap(struct { + fn f(l: *Lua) i32 { + _ = l.checkNumeric(u8, 1); + return 1; + } + }.f)); + const idx = lua.getTop(); + + lua.pushValue(idx); + lua.pushInteger(128); + try lua.protectedCall(.{ + .args = 1, + .results = 1, + }); + const val = lua.toNumeric(u8, lua.getTop()); + try std.testing.expectEqual(128, val); + + lua.pushValue(idx); + lua.pushInteger(256); + if (lua.protectedCall(.{ + .args = 1, + .results = 0, + })) |_| { + return error.ExpectedError; + } else |_| { + const string = lua.toStringEx(lua.getTop()); + try std.testing.expectEqualStrings(error_msg, string); + } } From 16806d3c0c4f5bc31b868a9a23fa44d373d09f8a Mon Sep 17 00:00:00 2001 From: Robbie Lyman Date: Tue, 19 Aug 2025 19:03:23 -0400 Subject: [PATCH 8/9] fix: error message inconsistency outside of test control --- src/tests.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests.zig b/src/tests.zig index 694afc1..ba62ea6 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -3088,6 +3088,8 @@ test "checkNumeric and toNumeric" { return error.ExpectedError; } else |_| { const string = lua.toStringEx(lua.getTop()); - try std.testing.expectEqualStrings(error_msg, string); + errdefer std.log.err("expected error message to contain: {s}", .{error_msg}); + errdefer std.log.err("error message: {s}", .{string}); + _ = std.mem.indexOf(u8, string, error_msg) orelse return error.BadErrorMessage; } } From e5d99be5ac733fb148a9ece5c0c7722053b7c50f Mon Sep 17 00:00:00 2001 From: Robbie Lyman Date: Tue, 19 Aug 2025 19:07:29 -0400 Subject: [PATCH 9/9] fix: toStringEx not available in 5.1 --- src/tests.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests.zig b/src/tests.zig index ba62ea6..44349ba 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -3087,7 +3087,7 @@ test "checkNumeric and toNumeric" { })) |_| { return error.ExpectedError; } else |_| { - const string = lua.toStringEx(lua.getTop()); + const string = try lua.toString(lua.getTop()); errdefer std.log.err("expected error message to contain: {s}", .{error_msg}); errdefer std.log.err("error message: {s}", .{string}); _ = std.mem.indexOf(u8, string, error_msg) orelse return error.BadErrorMessage;