Skip to content

Commit 07feb78

Browse files
committed
Simplify resume API
Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
1 parent d73b14f commit 07feb78

File tree

3 files changed

+98
-86
lines changed

3 files changed

+98
-86
lines changed

examples/guided_tour.zig

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//! - Push and retrieve arrays
1414
//! - Work with tuples and table-based structures
1515
//! - Control garbage collection for memory management
16+
//! - Work with coroutines and threads
1617

1718
const std = @import("std");
1819
const luaz = @import("luaz");
@@ -573,5 +574,42 @@ pub fn main() !void {
573574
, .{}, void);
574575
}
575576

577+
// Coroutines and threads
578+
{
579+
print("\n-- Coroutines and Threads --\n", .{});
580+
581+
// Create a coroutine function in global scope
582+
_ = try lua.eval(
583+
\\function accumulator()
584+
\\ local sum = 0
585+
\\ while true do
586+
\\ local value = coroutine.yield(sum)
587+
\\ if value == nil then break end
588+
\\ sum = sum + value
589+
\\ end
590+
\\ return sum
591+
\\end
592+
, .{}, void);
593+
594+
const thread = lua.createThread();
595+
const func = try thread.globals().get("accumulator", luaz.Lua.Function);
596+
defer func.?.deinit();
597+
598+
// Start the coroutine - yields initial sum (0)
599+
const result1 = try func.?.call(.{}, i32);
600+
print("Start: sum={}\n", .{result1});
601+
602+
// Continue with values to accumulate
603+
const result2 = try func.?.call(.{10}, i32);
604+
print("Add 10: sum={}\n", .{result2});
605+
606+
const result3 = try func.?.call(.{25}, i32);
607+
print("Add 25: sum={}\n", .{result3});
608+
609+
// Send nil to finish
610+
const final_result = try func.?.call(.{@as(?i32, null)}, i32);
611+
print("Final: sum={}\n", .{final_result});
612+
}
613+
576614
print("\n=== Tour Complete! ===\n", .{});
577615
}

src/lua.zig

Lines changed: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,10 @@ pub const Lua = struct {
861861
/// Pushes the function onto the stack, followed by the arguments, then calls the function
862862
/// and returns the result converted to the specified type.
863863
///
864+
/// NOTE: When called on a function in a thread (created with
865+
/// `createThread()`), this method automatically uses resume semantics, allowing the
866+
/// function to yield. In the main state, it uses regular call semantics.
867+
///
864868
/// Examples:
865869
/// ```zig
866870
/// // Call a function with no arguments
@@ -871,6 +875,15 @@ pub const Lua = struct {
871875
///
872876
/// // Call a function returning multiple values
873877
/// const result = try func.call(.{}, struct { f64, f64 });
878+
///
879+
/// // Coroutine example - function can yield
880+
/// const thread = lua.createThread();
881+
/// const func = try thread.globals().get("coroutine_func", Function).?;
882+
/// defer func.deinit();
883+
///
884+
/// const result1 = try func.call(.{}, i32); // Start coroutine, may yield
885+
/// const result2 = try func.call(.{5}, i32); // Continue coroutine with arg 5
886+
/// const result3 = try func.call(.{}, i32); // Final result
874887
/// ```
875888
///
876889
/// Errors: `Error.OutOfMemory` if stack allocation fails, `Error.Runtime` if function execution fails
@@ -879,7 +892,9 @@ pub const Lua = struct {
879892

880893
stack.push(self.ref.lua, self.ref); // Push function ref
881894

882-
const result = self.ref.lua.call(args, R, false);
895+
// Use resume semantics if in a thread, call semantics if in main state
896+
const result = self.ref.lua.call(args, R, self.ref.lua.isThread());
897+
883898
return switch (result.status) {
884899
.ok, .yield => result.result.?,
885900
.errmem => error.OutOfMemory,
@@ -1002,43 +1017,6 @@ pub const Lua = struct {
10021017
};
10031018
}
10041019

1005-
/// Resume a coroutine with the given arguments and return type.
1006-
///
1007-
/// This method resumes execution of a suspended coroutine (thread) that was previously
1008-
/// yielded. It's designed to work with threads created by createThread().
1009-
///
1010-
/// **Important**: This method should only be called on thread objects created with
1011-
/// `createThread()`. Calling `resume_()` on the main Lua state will return an error
1012-
/// status (`.errrun`) with a null result, as the main thread cannot be resumed.
1013-
///
1014-
/// Parameters:
1015-
/// - `args`: Arguments to pass to the resumed coroutine (can be tuple, single value, or void)
1016-
/// - `R`: Compile-time return type expected from the coroutine
1017-
///
1018-
/// Returns:
1019-
/// A struct containing both the execution status and the result:
1020-
/// - `.status`: The execution status (.ok, .yield, .errrun, etc.)
1021-
/// - `.result`: The value returned/yielded by the coroutine (null for errors)
1022-
///
1023-
/// Example:
1024-
/// ```zig
1025-
/// const thread_lua = lua.createThread();
1026-
/// _ = try thread_lua.eval("function coro() coroutine.yield(42); return 100 end; return coro()", .{}, void);
1027-
///
1028-
/// const result1 = thread_lua.resume_(.{}, i32); // Yields 42
1029-
/// if (result1.result) |value| std.debug.print("Yielded: {}\n", .{value}); // Prints: Yielded: 42
1030-
///
1031-
/// const result2 = thread_lua.resume_(.{}, i32); // Returns 100
1032-
/// if (result2.result) |value| std.debug.print("Returned: {}\n", .{value}); // Prints: Returned: 100
1033-
///
1034-
/// // Error case - trying to resume main thread:
1035-
/// const main_result = lua.resume_(.{}, void); // lua is main thread
1036-
/// if (main_result.status == .errrun) std.debug.print("Cannot resume main thread\n", .{});
1037-
/// ```
1038-
pub fn resume_(self: Self, args: anytype, comptime R: type) CallResult(R) {
1039-
return self.call(args, R, true);
1040-
}
1041-
10421020
/// Get the current status of this coroutine thread.
10431021
///
10441022
/// Returns the execution state of the coroutine without resuming it.
@@ -1174,7 +1152,7 @@ pub const Lua = struct {
11741152
}
11751153

11761154
/// Calls (or resumes) a Lua function with the provided arguments and returns the result.
1177-
fn call(self: Self, args: anytype, comptime R: type, comptime is_resume: bool) CallResult(R) {
1155+
fn call(self: Self, args: anytype, comptime R: type, is_resume: bool) CallResult(R) {
11781156
// Count and push args - unified logic for both call types
11791157
const arg_count = blk: {
11801158
const args_type_info = @typeInfo(@TypeOf(args));

src/tests.zig

Lines changed: 43 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,27 +1126,23 @@ test "coroutine yield and resume" {
11261126
, .{}, void);
11271127

11281128
// Create thread - it shares the global environment
1129-
const thread_lua = lua.createThread();
1129+
const thread = lua.createThread();
11301130

11311131
// Load function onto thread stack and start coroutine
1132-
const func = try thread_lua.globals().get("yielder", Lua.Function);
1132+
const func = try thread.globals().get("yielder", Lua.Function);
11331133
defer func.?.deinit();
1134-
stack.push(thread_lua, func.?);
11351134

11361135
// First resume - should yield 1
1137-
const result1 = thread_lua.resume_(.{}, i32);
1138-
try expectEq(result1.status, .yield);
1139-
try expectEq(result1.result.?, 1);
1136+
const result1 = try func.?.call(.{}, i32);
1137+
try expectEq(result1, 1);
11401138

11411139
// Second resume - should yield 2
1142-
const result2 = thread_lua.resume_(.{}, i32);
1143-
try expectEq(result2.status, .yield);
1144-
try expectEq(result2.result.?, 2);
1140+
const result2 = try func.?.call(.{}, i32);
1141+
try expectEq(result2, 2);
11451142

11461143
// Third resume - should return 3 and finish
1147-
const result3 = thread_lua.resume_(.{}, i32);
1148-
try expectEq(result3.status, .ok);
1149-
try expectEq(result3.result.?, 3);
1144+
const result3 = try func.?.call(.{}, i32);
1145+
try expectEq(result3, 3);
11501146
}
11511147

11521148
test "coroutine with arguments on yield" {
@@ -1173,27 +1169,22 @@ test "coroutine with arguments on yield" {
11731169

11741170
const func = try thread.globals().get("accumulator", Lua.Function);
11751171
defer func.?.deinit();
1176-
stack.push(thread, func.?);
1177-
1178-
// First resume - starts the coroutine, yields 0
1179-
const result1 = thread.resume_(.{}, i32);
1180-
try expectEq(result1.status, .yield);
1181-
try expectEq(result1.result.?, 0);
1182-
1183-
// Resume with value 5
1184-
const result2 = thread.resume_(5, i32);
1185-
try expectEq(result2.status, .yield);
1186-
try expectEq(result2.result.?, 5);
1187-
1188-
// Resume with value 10
1189-
const result3 = thread.resume_(10, i32);
1190-
try expectEq(result3.status, .yield);
1191-
try expectEq(result3.result.?, 15);
1192-
1193-
// Resume with nil to finish
1194-
const result4 = thread.resume_(@as(?i32, null), i32);
1195-
try expectEq(result4.status, .ok);
1196-
try expectEq(result4.result.?, 15);
1172+
1173+
// Start the coroutine - yields 0
1174+
const result1_value = try func.?.call(.{}, i32);
1175+
try expectEq(result1_value, 0);
1176+
1177+
// Continue the coroutine with value 5
1178+
const result2_value = try func.?.call(.{5}, i32);
1179+
try expectEq(result2_value, 5);
1180+
1181+
// Continue the coroutine with value 10
1182+
const result3_value = try func.?.call(.{10}, i32);
1183+
try expectEq(result3_value, 15);
1184+
1185+
// Finish the coroutine with nil
1186+
const result4_value = try func.?.call(.{@as(?i32, null)}, i32);
1187+
try expectEq(result4_value, 15);
11971188
}
11981189

11991190
test "thread data via globals" {
@@ -1277,11 +1268,9 @@ test "simple thread function execution" {
12771268
// Load and run function in thread
12781269
const func = try thread_lua.globals().get("simple", Lua.Function);
12791270
defer func.?.deinit();
1280-
stack.push(thread_lua, func.?);
12811271

1282-
const result = thread_lua.resume_(.{}, i32);
1283-
try expectEq(result.status, .ok);
1284-
try expectEq(result.result.?, 42);
1272+
const result = try func.?.call(.{}, i32);
1273+
try expectEq(result, 42);
12851274
}
12861275

12871276
test "coroutine error handling" {
@@ -1301,12 +1290,10 @@ test "coroutine error handling" {
13011290

13021291
const func = try thread_lua.globals().get("error_coro", Lua.Function);
13031292
defer func.?.deinit();
1304-
stack.push(thread_lua, func.?);
13051293

1306-
// Resume should return error status
1307-
const error_result = thread_lua.resume_(.{}, void);
1308-
try expectEq(error_result.status, .errrun);
1309-
try expect(error_result.result == null);
1294+
// Resume should return error
1295+
const error_result = func.?.call(.{}, void);
1296+
try std.testing.expectError(error.Runtime, error_result);
13101297
}
13111298

13121299
test "resume from main thread should return error" {
@@ -1316,10 +1303,19 @@ test "resume from main thread should return error" {
13161303
// Verify this is the main thread
13171304
try expect(!lua.isThread());
13181305

1319-
// Try to resume the main thread - this should fail
1320-
const result = lua.resume_(.{}, void);
1306+
// Create a simple function (not a coroutine)
1307+
_ = try lua.eval(
1308+
\\function test_func()
1309+
\\ return 42
1310+
\\end
1311+
, .{}, void);
13211312

1322-
// Should return .errrun status (cannot resume dead coroutine)
1323-
try expectEq(result.status, .errrun);
1324-
try expect(result.result == null);
1313+
// In main thread, functions are called with pcall semantics, not resume
1314+
// This is tested implicitly - Function.call() automatically detects thread context
1315+
const func = try lua.globals().get("test_func", Lua.Function);
1316+
defer func.?.deinit();
1317+
1318+
// This will use pcall since we're in main thread
1319+
const result = try func.?.call(.{}, i32);
1320+
try expectEq(result, 42);
13251321
}

0 commit comments

Comments
 (0)