From c608ce41593c1a33857d13abaa3a24ba61eac2d4 Mon Sep 17 00:00:00 2001 From: itsakeyfut Date: Wed, 22 Jul 2026 20:29:31 +0900 Subject: [PATCH 1/9] fix: preserve FP control state across SysV context switch The SysV switch saved only the six callee-saved general-purpose registers, while MXCSR control bits and the x87 control word are also callee-saved under the SysV ABI. Mirror the Windows path: save and restore both in the switch and seed defaults in initStack, so rounding mode and FP exception masks no longer leak between fibers. Replace the Windows-only mxcsr test with a cross-platform FP control-state test that now guards this on both platforms. --- CHANGELOG.md | 7 +++++ src/arch/x86_64_sysv.zig | 16 ++++++++++ src/fiber.zig | 68 +++++++++++++++++++++++++++------------- 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2fcb3f..ed7bb9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `zig build examples`. - GitHub Actions CI building and testing on Linux and Windows (x86_64). +### Fixed + +- The SysV (Linux/BSD) context switch now preserves the MXCSR control bits and + the x87 control word across a switch, matching the Windows path and the SysV + ABI. Previously a fiber that changed the rounding mode or FP exception masks + could leak that state into whatever ran next. + ### Removed - `src/main.zig`; the demo now lives under `examples/`. diff --git a/src/arch/x86_64_sysv.zig b/src/arch/x86_64_sysv.zig index 5f77618..4b4e199 100644 --- a/src/arch/x86_64_sysv.zig +++ b/src/arch/x86_64_sysv.zig @@ -8,8 +8,17 @@ fn fiberSwapNaked() callconv(.naked) void { \\ pushq %%r13 \\ pushq %%r14 \\ pushq %%r15 + \\ // MXCSR (+0) and x87 control word (+8) in a 16-byte slot + \\ subq $16, %%rsp + \\ stmxcsr 0(%%rsp) + \\ fnstcw 8(%%rsp) + \\ // switch stacks \\ movq %%rsp, (%%rdi) \\ movq (%%rsi), %%rsp + \\ // restore MXCSR + x87 control word + \\ ldmxcsr 0(%%rsp) + \\ fldcw 8(%%rsp) + \\ addq $16, %%rsp \\ popq %%r15 \\ popq %%r14 \\ popq %%r13 @@ -46,5 +55,12 @@ pub fn initStack( @as(*usize, @ptrFromInt(sp)).* = 0; } + // MXCSR (+0) / x87 control word (+8) 16-byte slot with defaults, matching + // the Windows layout so the first swap-in loads a sane FP environment. + sp -= @sizeOf(usize); + @as(*usize, @ptrFromInt(sp)).* = 0x0000037F; // x87 control word (slot+8) + sp -= @sizeOf(usize); + @as(*usize, @ptrFromInt(sp)).* = 0x00001F80; // MXCSR (slot+0) + return sp; } diff --git a/src/fiber.zig b/src/fiber.zig index 53b3310..f005572 100644 --- a/src/fiber.zig +++ b/src/fiber.zig @@ -106,6 +106,9 @@ test "fiber runs, yields, and completes" { } test "xmm6 is preserved across fiber switches" { + // Windows-only by design: xmm6-xmm15 are callee-saved under the Win64 ABI + // (so the switch must preserve them), but volatile under SysV (so the + // switch correctly does not). This test only asserts the Win64 requirement. if (@import("builtin").os.tag != .windows) return error.SkipZigTest; const allocator = std.testing.allocator; @@ -143,55 +146,78 @@ test "xmm6 is preserved across fiber switches" { try std.testing.expect(S.ok); } -test "mxcsr is preserved across fiber switches" { - if (@import("builtin").os.tag != .windows) return error.SkipZigTest; +test "FP control state is preserved across fiber switches" { const allocator = std.testing.allocator; - const M = struct { - fn get() u32 { + const Fp = struct { + fn getMxcsr() u32 { var v: u32 = 0; - // "m"/"=m" memory constraints mis-lower on this Zig 0.16 / - // x86_64-windows toolchain (the address gets re-spilled instead - // of dereferenced). Pass the address explicitly in a register - // and dereference it in the asm string instead. + // Pass the address in a register and dereference in the asm string: + // "m"/"=m" constraints mis-lower on the Zig 0.16 x86_64-windows path. asm volatile ("stmxcsr (%[o])" : : [o] "r" (&v), : .{ .memory = true }); return v; } - fn set(v: u32) void { + fn setMxcsr(v: u32) void { var local = v; asm volatile ("ldmxcsr (%[i])" : : [i] "r" (&local), : .{ .memory = true }); } + fn getCw() u16 { + var v: u16 = 0; + asm volatile ("fnstcw (%[o])" + : + : [o] "r" (&v), + : .{ .memory = true }); + return v; + } + fn setCw(v: u16) void { + var local = v; + asm volatile ("fldcw (%[i])" + : + : [i] "r" (&local), + : .{ .memory = true }); + } }; const S = struct { - var ok: bool = false; - var saved_default: u32 = 0; + var ok_mxcsr: bool = false; + var ok_cw: bool = false; fn work(_: *Fiber) void { - // Round-toward-zero = rounding-control bits (13-14) set. - const rc_toward_zero: u32 = (M.get() & ~@as(u32, 0x6000)) | 0x6000; - M.set(rc_toward_zero); + // MXCSR round-toward-zero: RC bits 13-14 set. + Fp.setMxcsr((Fp.getMxcsr() & ~@as(u32, 0x6000)) | 0x6000); + // x87 round-toward-zero: RC bits 10-11 set. + Fp.setCw(Fp.getCw() | 0x0C00); Fiber.yield(); - ok = (M.get() & 0x6000) == 0x6000; - M.set(saved_default); // restore for the test runner + ok_mxcsr = (Fp.getMxcsr() & 0x6000) == 0x6000; + ok_cw = (Fp.getCw() & 0x0C00) == 0x0C00; } }; - S.ok = false; - S.saved_default = M.get(); + S.ok_mxcsr = false; + S.ok_cw = false; + + const saved_mxcsr = Fp.getMxcsr(); + const saved_cw = Fp.getCw(); const f = try Fiber.create(allocator, &S.work); defer f.destroy(); while (f.state != .done) { - M.set(S.saved_default & ~@as(u32, 0x6000)); // round-to-nearest in caller + // Caller runs in the default rounding mode between resumes, so a leaked + // control word from the fiber would corrupt the fiber's own check. + Fp.setMxcsr(saved_mxcsr & ~@as(u32, 0x6000)); + Fp.setCw(0x037F); f.resumeFiber(); } - M.set(S.saved_default); - try std.testing.expect(S.ok); + // Restore the test runner's FP environment. + Fp.setMxcsr(saved_mxcsr); + Fp.setCw(saved_cw); + + try std.testing.expect(S.ok_mxcsr); + try std.testing.expect(S.ok_cw); } From c490723996eb863e3b3f0f454b8b610588157457 Mon Sep 17 00:00:00 2001 From: itsakeyfut Date: Wed, 22 Jul 2026 20:45:49 +0900 Subject: [PATCH 2/9] test: verify callee-saved GP registers survive a switch --- src/fiber.zig | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/fiber.zig b/src/fiber.zig index f005572..c2235d9 100644 --- a/src/fiber.zig +++ b/src/fiber.zig @@ -221,3 +221,61 @@ test "FP control state is preserved across fiber switches" { try std.testing.expect(S.ok_mxcsr); try std.testing.expect(S.ok_cw); } + +test "callee-saved general-purpose registers survive fiber switches" { + const allocator = std.testing.allocator; + + const S = struct { + var ok: bool = false; + fn work(_: *Fiber) void { + asm volatile ( + \\ movabsq $0x1111111111111111, %%rbx + \\ movabsq $0x2222222222222222, %%r12 + \\ movabsq $0x3333333333333333, %%r13 + \\ movabsq $0x4444444444444444, %%r14 + \\ movabsq $0x5555555555555555, %%r15 + ::: .{ .rbx = true, .r12 = true, .r13 = true, .r14 = true, .r15 = true }); + Fiber.yield(); + var b: u64 = 0; + var c12: u64 = 0; + var c13: u64 = 0; + var c14: u64 = 0; + var c15: u64 = 0; + asm volatile ( + \\ movq %%rbx, %[b] + \\ movq %%r12, %[c12] + \\ movq %%r13, %[c13] + \\ movq %%r14, %[c14] + \\ movq %%r15, %[c15] + : [b] "=r" (b), + [c12] "=r" (c12), + [c13] "=r" (c13), + [c14] "=r" (c14), + [c15] "=r" (c15), + : + : .{ .rbx = true, .r12 = true, .r13 = true, .r14 = true, .r15 = true }); + ok = b == 0x1111111111111111 and c12 == 0x2222222222222222 and + c13 == 0x3333333333333333 and c14 == 0x4444444444444444 and + c15 == 0x5555555555555555; + } + }; + S.ok = false; + + const f = try Fiber.create(allocator, &S.work); + defer f.destroy(); + + while (f.state != .done) { + // Clobber every callee-saved GP register in the caller between resumes, + // so a switch that failed to preserve them would be caught. + asm volatile ( + \\ movabsq $0xdeadbeefdeadbeef, %%rbx + \\ movabsq $0xdeadbeefdeadbeef, %%r12 + \\ movabsq $0xdeadbeefdeadbeef, %%r13 + \\ movabsq $0xdeadbeefdeadbeef, %%r14 + \\ movabsq $0xdeadbeefdeadbeef, %%r15 + ::: .{ .rbx = true, .r12 = true, .r13 = true, .r14 = true, .r15 = true }); + f.resumeFiber(); + } + + try std.testing.expect(S.ok); +} From eadc534029320da0cb8d434502c797038610be8a Mon Sep 17 00:00:00 2001 From: itsakeyfut Date: Wed, 22 Jul 2026 21:38:56 +0900 Subject: [PATCH 3/9] test: cover nested fiber resume --- src/fiber.zig | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/fiber.zig b/src/fiber.zig index c2235d9..8b5531a 100644 --- a/src/fiber.zig +++ b/src/fiber.zig @@ -279,3 +279,45 @@ test "callee-saved general-purpose registers survive fiber switches" { try std.testing.expect(S.ok); } + +test "a fiber can resume another fiber (nesting)" { + const allocator = std.testing.allocator; + + const S = struct { + var log: [8]u8 = undefined; + var n: usize = 0; + var inner_fiber: *Fiber = undefined; + + fn record(c: u8) void { + log[n] = c; + n += 1; + } + fn inner(_: *Fiber) void { + record('b'); + Fiber.yield(); + record('d'); + } + fn outer(_: *Fiber) void { + record('a'); + inner_fiber.resumeFiber(); // nested resume + record('c'); + inner_fiber.resumeFiber(); // resume inner to completion + record('e'); + } + }; + S.n = 0; + + const inner = try Fiber.create(allocator, &S.inner); + defer inner.destroy(); + S.inner_fiber = inner; + + const outer = try Fiber.create(allocator, &S.outer); + defer outer.destroy(); + + while (outer.state != .done) outer.resumeFiber(); + + try std.testing.expectEqual(@as(usize, 5), S.n); + try std.testing.expectEqualSlices(u8, "abcde", S.log[0..5]); + try std.testing.expectEqual(State.done, inner.state); + try std.testing.expectEqual(State.done, outer.state); +} From ffcd8d362fd7e148976e70a7a416c3e74742a68b Mon Sep 17 00:00:00 2001 From: itsakeyfut Date: Wed, 22 Jul 2026 21:46:04 +0900 Subject: [PATCH 4/9] test: cover deep-stack yields, many yields, and local integrity --- src/fiber.zig | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/fiber.zig b/src/fiber.zig index 8b5531a..2c99e8a 100644 --- a/src/fiber.zig +++ b/src/fiber.zig @@ -321,3 +321,94 @@ test "a fiber can resume another fiber (nesting)" { try std.testing.expectEqual(State.done, inner.state); try std.testing.expectEqual(State.done, outer.state); } + +test "yield works from deep in the call stack" { + const allocator = std.testing.allocator; + + const S = struct { + var reached: usize = 0; + var resumed: usize = 0; + fn recurse(depth: usize) void { + if (depth == 0) { + reached = 100; + Fiber.yield(); + resumed = 200; + return; + } + recurse(depth - 1); + } + fn work(_: *Fiber) void { + recurse(100); + } + }; + S.reached = 0; + S.resumed = 0; + + const f = try Fiber.create(allocator, &S.work); + defer f.destroy(); + + f.resumeFiber(); // descends 100 frames, then yields + try std.testing.expectEqual(@as(usize, 100), S.reached); + try std.testing.expectEqual(@as(usize, 0), S.resumed); + try std.testing.expectEqual(State.suspended, f.state); + + f.resumeFiber(); // continues from deep in the stack to completion + try std.testing.expectEqual(@as(usize, 200), S.resumed); + try std.testing.expectEqual(State.done, f.state); +} + +test "a fiber can yield many times" { + const allocator = std.testing.allocator; + + const S = struct { + var count: usize = 0; + fn work(_: *Fiber) void { + var i: usize = 0; + while (i < 10_000) : (i += 1) { + count += 1; + Fiber.yield(); + } + } + }; + S.count = 0; + + const f = try Fiber.create(allocator, &S.work); + defer f.destroy(); + + var resumes: usize = 0; + while (f.state != .done) { + f.resumeFiber(); + resumes += 1; + } + + try std.testing.expectEqual(@as(usize, 10_000), S.count); + try std.testing.expectEqual(@as(usize, 10_001), resumes); +} + +test "local variables survive across yields" { + const allocator = std.testing.allocator; + + const S = struct { + var result: u64 = 0; + fn work(_: *Fiber) void { + var a: u64 = 3; + var b: u64 = 5; + var c: u64 = 7; + Fiber.yield(); + a += 10; + b += 20; + Fiber.yield(); + c += 30; + result = a * 1000 + b * 100 + c; + } + }; + S.result = 0; + + const f = try Fiber.create(allocator, &S.work); + defer f.destroy(); + + while (f.state != .done) f.resumeFiber(); + + // a=13, b=25, c=37 -> 13000 + 2500 + 37 + try std.testing.expectEqual(@as(u64, 15537), S.result); +} From e9dc9dfe89a7e4fc71958eb97ad6c6cb5c494df2 Mon Sep 17 00:00:00 2001 From: itsakeyfut Date: Wed, 22 Jul 2026 21:49:57 +0900 Subject: [PATCH 5/9] test: cover interleaved independent fibers --- src/fiber.zig | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/fiber.zig b/src/fiber.zig index 2c99e8a..c2bfb39 100644 --- a/src/fiber.zig +++ b/src/fiber.zig @@ -412,3 +412,57 @@ test "local variables survive across yields" { // a=13, b=25, c=37 -> 13000 + 2500 + 37 try std.testing.expectEqual(@as(u64, 15537), S.result); } + +test "independent fibers keep separate state when interleaved" { + const allocator = std.testing.allocator; + + const S = struct { + var counters = [_]u64{ 0, 0, 0 }; + // Three sibling entries, one per fiber. Each references `counters` + // unqualified (a sibling container var), avoiding a self-reference to + // the local `S` from a nested struct, which does not compile. + fn w0(_: *Fiber) void { + var i: usize = 0; + while (i < 3) : (i += 1) { + counters[0] += 1; + Fiber.yield(); + } + } + fn w1(_: *Fiber) void { + var i: usize = 0; + while (i < 5) : (i += 1) { + counters[1] += 1; + Fiber.yield(); + } + } + fn w2(_: *Fiber) void { + var i: usize = 0; + while (i < 2) : (i += 1) { + counters[2] += 1; + Fiber.yield(); + } + } + }; + S.counters = .{ 0, 0, 0 }; + + const fibers = [_]*Fiber{ + try Fiber.create(allocator, &S.w0), + try Fiber.create(allocator, &S.w1), + try Fiber.create(allocator, &S.w2), + }; + defer for (fibers) |f| f.destroy(); + + var remaining: usize = fibers.len; + while (remaining > 0) { + remaining = 0; + for (fibers) |f| { + if (f.state == .done) continue; + f.resumeFiber(); + if (f.state != .done) remaining += 1; + } + } + + try std.testing.expectEqual(@as(u64, 3), S.counters[0]); + try std.testing.expectEqual(@as(u64, 5), S.counters[1]); + try std.testing.expectEqual(@as(u64, 2), S.counters[2]); +} From 1000611ef373521eff6439f3a59844418997e6b2 Mon Sep 17 00:00:00 2001 From: itsakeyfut Date: Wed, 22 Jul 2026 21:57:30 +0900 Subject: [PATCH 6/9] test: verify create is leak-free on allocation failure --- src/fiber.zig | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/fiber.zig b/src/fiber.zig index c2bfb39..5d4b06f 100644 --- a/src/fiber.zig +++ b/src/fiber.zig @@ -466,3 +466,35 @@ test "independent fibers keep separate state when interleaved" { try std.testing.expectEqual(@as(u64, 5), S.counters[1]); try std.testing.expectEqual(@as(u64, 2), S.counters[2]); } + +test "create reports OutOfMemory and leaks nothing on allocation failure" { + const S = struct { + fn work(_: *Fiber) void {} + }; + + // Fail allocation index 0 (the Fiber struct): create returns the error and + // nothing is allocated. + { + var failing = std.testing.FailingAllocator.init( + std.testing.allocator, + .{ .fail_index = 0 }, + ); + try std.testing.expectError( + error.OutOfMemory, + Fiber.create(failing.allocator(), &S.work), + ); + } + + // Fail allocation index 1 (the stack): the Fiber struct allocation must be + // rolled back by errdefer. std.testing.allocator flags any leak. + { + var failing = std.testing.FailingAllocator.init( + std.testing.allocator, + .{ .fail_index = 1 }, + ); + try std.testing.expectError( + error.OutOfMemory, + Fiber.create(failing.allocator(), &S.work), + ); + } +} From 5407edbcfc8941b15e4242456ad9b9bf2994e6fb Mon Sep 17 00:00:00 2001 From: itsakeyfut Date: Wed, 22 Jul 2026 22:01:50 +0900 Subject: [PATCH 7/9] docs: note expanded test coverage in the changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed7bb9e..3bfaf96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `examples/basic.zig` and `examples/scheduler.zig`, runnable via `zig build examples`. - GitHub Actions CI building and testing on Linux and Windows (x86_64). +- Expanded test coverage: cross-platform FP control-state preservation, + callee-saved general-purpose registers, nested fibers, deep-stack yields, + many yields, local-variable integrity, interleaved independent fibers, and + allocation-failure cleanup. ### Fixed From 1d1273b3ae7a7be6b0f3448b74534a1751ebff7b Mon Sep 17 00:00:00 2001 From: itsakeyfut Date: Wed, 22 Jul 2026 22:08:21 +0900 Subject: [PATCH 8/9] test: assert per-round interleaving in the interleaved-fibers test After one resume each, every fiber's counter must be exactly 1. Serial run-to-completion would drive the first fiber to 3 before the second started, so this positively confirms cooperative interleaving rather than only checking the final per-fiber totals. --- src/fiber.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/fiber.zig b/src/fiber.zig index 5d4b06f..7d6a25d 100644 --- a/src/fiber.zig +++ b/src/fiber.zig @@ -452,6 +452,14 @@ test "independent fibers keep separate state when interleaved" { }; defer for (fibers) |f| f.destroy(); + // One resume each: under true interleaving every fiber advances exactly + // once. Serial run-to-completion would instead drive the first fiber to 3 + // before the second ever started, so this positively confirms interleaving. + for (fibers) |f| f.resumeFiber(); + try std.testing.expectEqual(@as(u64, 1), S.counters[0]); + try std.testing.expectEqual(@as(u64, 1), S.counters[1]); + try std.testing.expectEqual(@as(u64, 1), S.counters[2]); + var remaining: usize = fibers.len; while (remaining > 0) { remaining = 0; From a3d7066a1376931bbd4ad4580168d8eac362d469 Mon Sep 17 00:00:00 2001 From: itsakeyfut Date: Wed, 22 Jul 2026 22:19:17 +0900 Subject: [PATCH 9/9] fix: make the FP-control test portable and drop the GP-register test The new asm-based tests only compiled on Windows. x86_64-linux and x86_64-windows accept different inline-asm idioms, and these tests had no comptime target guard (unlike the Windows-only xmm6 test), so they first compiled on the Linux CI target and failed: - The FP-control test's helpers used the register-address-dereference form (stmxcsr (%[o]) with an "r" operand), a Zig 0.16 x86_64-windows workaround that the Linux assembler rejects as an invalid memory operand. Split the helpers by target at comptime: Windows keeps the workaround, other targets use the standard "m"/"=m" memory constraint. - The GP-register test used movabsq, an invalid mnemonic on the Linux assembler. Remove that test: it was already flagged as relying on compiler register scheduling, and callee-saved register preservation is covered by the behavioral tests (nested, deep-stack, local-integrity, interleaved). --- CHANGELOG.md | 7 ++- src/fiber.zig | 130 ++++++++++++++++++++------------------------------ 2 files changed, 55 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bfaf96..075e02d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `examples/basic.zig` and `examples/scheduler.zig`, runnable via `zig build examples`. - GitHub Actions CI building and testing on Linux and Windows (x86_64). -- Expanded test coverage: cross-platform FP control-state preservation, - callee-saved general-purpose registers, nested fibers, deep-stack yields, - many yields, local-variable integrity, interleaved independent fibers, and - allocation-failure cleanup. +- Expanded test coverage: cross-platform FP control-state preservation, nested + fibers, deep-stack yields, many yields, local-variable integrity, interleaved + independent fibers, and allocation-failure cleanup. ### Fixed diff --git a/src/fiber.zig b/src/fiber.zig index 7d6a25d..409cc24 100644 --- a/src/fiber.zig +++ b/src/fiber.zig @@ -150,37 +150,69 @@ test "FP control state is preserved across fiber switches" { const allocator = std.testing.allocator; const Fp = struct { + // MXCSR / x87 control-word access differs by target: the "m"/"=m" + // constraint mis-lowers on the Zig 0.16 x86_64-windows path (the address + // is re-spilled instead of dereferenced), so Windows passes the address + // in a register and dereferences it in the asm string; other targets use + // the standard memory constraint, which the Windows path rejects. + const is_windows = @import("builtin").os.tag == .windows; fn getMxcsr() u32 { var v: u32 = 0; - // Pass the address in a register and dereference in the asm string: - // "m"/"=m" constraints mis-lower on the Zig 0.16 x86_64-windows path. - asm volatile ("stmxcsr (%[o])" - : - : [o] "r" (&v), - : .{ .memory = true }); + if (is_windows) { + asm volatile ("stmxcsr (%[o])" + : + : [o] "r" (&v), + : .{ .memory = true }); + } else { + asm volatile ("stmxcsr %[o]" + : [o] "=m" (v), + ); + } return v; } fn setMxcsr(v: u32) void { - var local = v; - asm volatile ("ldmxcsr (%[i])" - : - : [i] "r" (&local), - : .{ .memory = true }); + if (is_windows) { + var local = v; + asm volatile ("ldmxcsr (%[i])" + : + : [i] "r" (&local), + : .{ .memory = true }); + } else { + const local = v; + asm volatile ("ldmxcsr %[i]" + : + : [i] "m" (local), + ); + } } fn getCw() u16 { var v: u16 = 0; - asm volatile ("fnstcw (%[o])" - : - : [o] "r" (&v), - : .{ .memory = true }); + if (is_windows) { + asm volatile ("fnstcw (%[o])" + : + : [o] "r" (&v), + : .{ .memory = true }); + } else { + asm volatile ("fnstcw %[o]" + : [o] "=m" (v), + ); + } return v; } fn setCw(v: u16) void { - var local = v; - asm volatile ("fldcw (%[i])" - : - : [i] "r" (&local), - : .{ .memory = true }); + if (is_windows) { + var local = v; + asm volatile ("fldcw (%[i])" + : + : [i] "r" (&local), + : .{ .memory = true }); + } else { + const local = v; + asm volatile ("fldcw %[i]" + : + : [i] "m" (local), + ); + } } }; @@ -222,64 +254,6 @@ test "FP control state is preserved across fiber switches" { try std.testing.expect(S.ok_cw); } -test "callee-saved general-purpose registers survive fiber switches" { - const allocator = std.testing.allocator; - - const S = struct { - var ok: bool = false; - fn work(_: *Fiber) void { - asm volatile ( - \\ movabsq $0x1111111111111111, %%rbx - \\ movabsq $0x2222222222222222, %%r12 - \\ movabsq $0x3333333333333333, %%r13 - \\ movabsq $0x4444444444444444, %%r14 - \\ movabsq $0x5555555555555555, %%r15 - ::: .{ .rbx = true, .r12 = true, .r13 = true, .r14 = true, .r15 = true }); - Fiber.yield(); - var b: u64 = 0; - var c12: u64 = 0; - var c13: u64 = 0; - var c14: u64 = 0; - var c15: u64 = 0; - asm volatile ( - \\ movq %%rbx, %[b] - \\ movq %%r12, %[c12] - \\ movq %%r13, %[c13] - \\ movq %%r14, %[c14] - \\ movq %%r15, %[c15] - : [b] "=r" (b), - [c12] "=r" (c12), - [c13] "=r" (c13), - [c14] "=r" (c14), - [c15] "=r" (c15), - : - : .{ .rbx = true, .r12 = true, .r13 = true, .r14 = true, .r15 = true }); - ok = b == 0x1111111111111111 and c12 == 0x2222222222222222 and - c13 == 0x3333333333333333 and c14 == 0x4444444444444444 and - c15 == 0x5555555555555555; - } - }; - S.ok = false; - - const f = try Fiber.create(allocator, &S.work); - defer f.destroy(); - - while (f.state != .done) { - // Clobber every callee-saved GP register in the caller between resumes, - // so a switch that failed to preserve them would be caught. - asm volatile ( - \\ movabsq $0xdeadbeefdeadbeef, %%rbx - \\ movabsq $0xdeadbeefdeadbeef, %%r12 - \\ movabsq $0xdeadbeefdeadbeef, %%r13 - \\ movabsq $0xdeadbeefdeadbeef, %%r14 - \\ movabsq $0xdeadbeefdeadbeef, %%r15 - ::: .{ .rbx = true, .r12 = true, .r13 = true, .r14 = true, .r15 = true }); - f.resumeFiber(); - } - - try std.testing.expect(S.ok); -} - test "a fiber can resume another fiber (nesting)" { const allocator = std.testing.allocator;