From 08d4ef5e7ed6420c283f430e25ddb2bb56dbb7f5 Mon Sep 17 00:00:00 2001 From: itsakeyfut Date: Thu, 23 Jul 2026 18:50:22 +0900 Subject: [PATCH 1/3] ci: test across optimize modes, add a format gate, harden the workflow Catch codegen-dependent bugs (the inline-asm context switch is sensitive to optimization) by running the suite in Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall on ubuntu-latest and windows-latest. Add a zig fmt --check gate, a 10-minute per-job timeout (a broken switch hangs a resume loop rather than crashing), and cancel-in-progress concurrency so superseded runs stop. --- .github/workflows/ci.yml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3a226c..228e873 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,8 +6,14 @@ on: pull_request: branches: [main] +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + jobs: test: + runs-on: ${{ matrix.os }} + timeout-minutes: 10 strategy: fail-fast: false # fiber only supports x86_64 (System V ABI and Windows). GitHub's macOS @@ -15,7 +21,7 @@ jobs: # intentionally omitted from the matrix. matrix: os: [ubuntu-latest, windows-latest] - runs-on: ${{ matrix.os }} + optimize: [Debug, ReleaseSafe, ReleaseFast, ReleaseSmall] steps: - uses: actions/checkout@v4 @@ -24,7 +30,20 @@ jobs: version: 0.16.0 - name: Build - run: zig build + run: zig build -Doptimize=${{ matrix.optimize }} - name: Test - run: zig build test + run: zig build test -Doptimize=${{ matrix.optimize }} + + fmt: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + + - name: Format check + run: zig fmt --check . From 45966833b15909f58de5a3670cfc64a625f96dbc Mon Sep 17 00:00:00 2001 From: itsakeyfut Date: Thu, 23 Jul 2026 19:12:49 +0900 Subject: [PATCH 2/3] fix: mask MXCSR in the FP test helper to avoid a reserved-bit #GP The new optimize-mode CI matrix surfaced a crash: the FP-control test's getMxcsr helper returned a value with spurious reserved bits (16-31) set under optimized codegen on x86_64-linux (the Debug build and every Windows mode were clean), and setMxcsr then fed it to ldmxcsr, which raises #GP if any reserved bit is set. MXCSR only defines bits 0-15, so mask the value on both read and write. This is a test-helper fix only: the library's SysV switch uses raw naked asm over already-clean values and is unaffected. --- src/fiber.zig | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/fiber.zig b/src/fiber.zig index 832ea16..4f300c2 100644 --- a/src/fiber.zig +++ b/src/fiber.zig @@ -196,17 +196,22 @@ test "FP control state is preserved across fiber switches" { : [o] "=m" (v), ); } - return v; + // MXCSR only defines bits 0-15; bits 16-31 are reserved. Mask them off + // so a spurious high bit (observed from the "=m" output under optimized + // codegen on x86_64-linux) can never reach `ldmxcsr`, which raises #GP + // if any reserved bit is set. + return v & 0xffff; } fn setMxcsr(v: u32) void { + const clean = v & 0xffff; // never feed reserved bits to ldmxcsr (#GP) if (is_windows) { - var local = v; + var local = clean; asm volatile ("ldmxcsr (%[i])" : : [i] "r" (&local), : .{ .memory = true }); } else { - const local = v; + const local = clean; asm volatile ("ldmxcsr %[i]" : : [i] "m" (local), From 7153fa8747c6a59f25ba30bc6ccbd2ce9fcfa5e2 Mon Sep 17 00:00:00 2001 From: itsakeyfut Date: Thu, 23 Jul 2026 19:27:17 +0900 Subject: [PATCH 3/3] fix: access MXCSR/x87 CW via an rsp scratch to dodge Zig asm lowering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mask alone did not stop the crash: the #GP was the ldmxcsr memory operand's ADDRESS, not its value. Zig 0.16 lowers the candidate operand forms inconsistently on x86_64-linux — the "m"/"=m" constraint yields a wrong address under optimized codegen (runtime #GP), while the (%reg) dereference form fails to assemble in Debug (invalid memory operand). Move rsp below the red zone with lea (flags untouched), address the scratch as (%rsp), and pass values in register operands, dodging operand lowering entirely. Verified locally by cross-compiling the tests to x86_64-linux (all four optimize modes compile) and running natively on Windows (all four pass). --- src/fiber.zig | 109 +++++++++++++++++++++++--------------------------- 1 file changed, 49 insertions(+), 60 deletions(-) diff --git a/src/fiber.zig b/src/fiber.zig index 4f300c2..d63830f 100644 --- a/src/fiber.zig +++ b/src/fiber.zig @@ -178,74 +178,63 @@ 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; + // Read/write MXCSR and the x87 control word through a self-managed stack + // scratch. Zig 0.16 lowers the obvious operand forms inconsistently on + // x86_64-linux: the "m"/"=m" memory constraint produces a wrong operand + // address under optimized codegen (faulting `ldmxcsr` with a #GP), while + // the `(%reg)` register-dereference form fails to assemble in Debug + // ("invalid memory operand"). To dodge operand lowering entirely, move rsp + // below the 128-byte red zone with `lea` (which leaves the flags alone), + // address the scratch as `(%rsp)`, then restore rsp. Values move through + // plain register operands. fn getMxcsr() u32 { - var v: u32 = 0; - if (is_windows) { - asm volatile ("stmxcsr (%[o])" - : - : [o] "r" (&v), - : .{ .memory = true }); - } else { - asm volatile ("stmxcsr %[o]" - : [o] "=m" (v), - ); - } - // MXCSR only defines bits 0-15; bits 16-31 are reserved. Mask them off - // so a spurious high bit (observed from the "=m" output under optimized - // codegen on x86_64-linux) can never reach `ldmxcsr`, which raises #GP - // if any reserved bit is set. + var v: u32 = undefined; + asm volatile ( + \\ leaq -144(%%rsp), %%rsp + \\ stmxcsr (%%rsp) + \\ movl (%%rsp), %[out] + \\ leaq 144(%%rsp), %%rsp + : [out] "=r" (v), + : + : .{ .memory = true }); + // MXCSR only defines bits 0-15; keep the result clean. return v & 0xffff; } fn setMxcsr(v: u32) void { - const clean = v & 0xffff; // never feed reserved bits to ldmxcsr (#GP) - if (is_windows) { - var local = clean; - asm volatile ("ldmxcsr (%[i])" - : - : [i] "r" (&local), - : .{ .memory = true }); - } else { - const local = clean; - asm volatile ("ldmxcsr %[i]" - : - : [i] "m" (local), - ); - } + const clean = v & 0xffff; // ldmxcsr #GPs on reserved bits + asm volatile ( + \\ leaq -144(%%rsp), %%rsp + \\ movl %[val], (%%rsp) + \\ ldmxcsr (%%rsp) + \\ leaq 144(%%rsp), %%rsp + : + : [val] "r" (clean), + : .{ .memory = true }); } fn getCw() u16 { - var v: u16 = 0; - if (is_windows) { - asm volatile ("fnstcw (%[o])" - : - : [o] "r" (&v), - : .{ .memory = true }); - } else { - asm volatile ("fnstcw %[o]" - : [o] "=m" (v), - ); - } - return v; + var v: u32 = undefined; + asm volatile ( + \\ leaq -144(%%rsp), %%rsp + \\ fnstcw (%%rsp) + \\ movl (%%rsp), %[out] + \\ leaq 144(%%rsp), %%rsp + : [out] "=r" (v), + : + : .{ .memory = true }); + // fnstcw writes 2 bytes; the scratch's upper half is uninitialized, so + // keep only the control-word bits. + return @truncate(v & 0xffff); } fn setCw(v: u16) void { - 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), - ); - } + const wide: u32 = v; + asm volatile ( + \\ leaq -144(%%rsp), %%rsp + \\ movl %[val], (%%rsp) + \\ fldcw (%%rsp) + \\ leaq 144(%%rsp), %%rsp + : + : [val] "r" (wide), + : .{ .memory = true }); } };