diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 4b053e7..52bbd5f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ { "name": "zig-claude-kit", "source": "./plugins/zig-claude-kit", - "description": "Corrective context for Zig 0.15.x that fixes Claude's outdated training data. Covers I/O (Writergate), build.zig, format strings, ArrayList, BoundedArray, and usingnamespace.", + "description": "Corrective context for Zig 0.15.x and 0.16 that fixes Claude's outdated training data. Auto-detects target from build.zig.zon. Covers Writergate, Io-as-Interface, std.fs->std.Io, Juicy Main, indexOf->find, build.zig, format strings, ArrayList, BoundedArray, usingnamespace.", "strict": true }, { diff --git a/CLAUDE.md b/CLAUDE.md index de283dd..d762a3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,8 +7,9 @@ when working with code in this repository. A Claude Code plugin marketplace containing three plugins: -- **zig-claude-kit** -- corrective context for Zig 0.15.x - that fixes Claude's outdated training data +- **zig-claude-kit** -- corrective context for Zig 0.15.x and + 0.16 that fixes Claude's outdated training data; + auto-detects target from build.zig.zon - **tdd-pipeline** -- language-agnostic TDD pipeline with seven agents across separate stages - **cross-review** -- multi-model code review with @@ -26,8 +27,10 @@ plugins/ .claude-plugin/plugin.json # manifest (version here) skills/ # zig-init, zig-patterns, zig-check hooks/hooks.json # SessionStart hook - scripts/ # eval suite + session-start - docs/ # fragment, breaking changes ref + scripts/ # eval suite, audit-0.15/0.16, + # session-start, detect-zig-version + docs/ # claude-md-fragment-{0.15,0.16}.md, + # ZIG_BREAKING_CHANGES-{0.15,0.16}.md tdd-pipeline/ .claude-plugin/plugin.json # manifest (version here) skills/ # tdd-orchestrate, tdd-init @@ -80,13 +83,19 @@ file patterns. No code-level coupling between plugins. Run from `plugins/zig-claude-kit/`: ```bash -make eval # test all models -make eval-model MODEL=claude-haiku-4-5 # test one model -make compile-test MODEL=claude-sonnet-4-6 -make audit # probe current Zig +make audit # auto-detect Zig version +make audit-0.15 # validate 0.15.x claims +make audit-0.16 # validate 0.16 claims + +make eval TARGET=0.16 # blind-test default models +make eval-model MODEL=claude-haiku-4-5 TARGET=0.16 +make compile-test MODEL=claude-sonnet-4-6 TARGET=0.16 ``` -Requires `ANTHROPIC_API_KEY` and `uv`. +`make eval` requires `ANTHROPIC_API_KEY` and `uv`; `make audit` +requires `zig` on `PATH`. The `TARGET` variable labels output +probe directories; what actually validates the code is the +locally installed Zig. ## Writing Style diff --git a/plugins/zig-claude-kit/.claude-plugin/plugin.json b/plugins/zig-claude-kit/.claude-plugin/plugin.json index 566b312..17d8c77 100644 --- a/plugins/zig-claude-kit/.claude-plugin/plugin.json +++ b/plugins/zig-claude-kit/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "zig-claude-kit", - "description": "Corrective context for Zig 0.15.x that fixes Claude's outdated training data. Covers I/O (Writergate), build.zig, format strings, ArrayList, BoundedArray, usingnamespace, division, tokenize, args, JSON, and for-loop index.", - "version": "0.2.1", + "description": "Corrective context for Zig 0.15.x and 0.16 that fixes Claude's outdated training data. Auto-detects target version from build.zig.zon. Covers I/O (Writergate + Io-as-Interface), std.fs -> std.Io move, Juicy Main, std.mem.indexOf -> find, sync primitives moved to Io, @Type split, ArrayList, BoundedArray, usingnamespace, format strings, build.zig, division, tokenize, args, JSON, for-loop index.", + "version": "0.3.0", "author": { "name": "Travis Cole" }, diff --git a/plugins/zig-claude-kit/Makefile b/plugins/zig-claude-kit/Makefile index a6b27b0..eb4bfbb 100644 --- a/plugins/zig-claude-kit/Makefile +++ b/plugins/zig-claude-kit/Makefile @@ -1,24 +1,37 @@ -.PHONY: help eval eval-model compile-test audit clean +.PHONY: help eval eval-model compile-test audit audit-0.15 audit-0.16 clean help: ## Show available targets - @grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | \ - awk 'BEGIN {FS = ":.*## "}; {printf " %-16s %s\n", $$1, $$2}' + @grep -E '^[a-zA-Z_.0-9-]+:.*##' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*## "}; {printf " %-18s %s\n", $$1, $$2}' + +# Default Zig target for evals +TARGET ?= 0.16 # --- Model Evaluation --- -eval: ## Blind-test all default models (sonnet + opus 4.6) - uv run scripts/zig-knowledge-eval.py +eval: ## Blind-test default models (TARGET=0.16 or 0.15) + uv run scripts/zig-knowledge-eval.py --target $(TARGET) -eval-model: ## Blind-test a specific model (MODEL=claude-haiku-4-5) - uv run scripts/zig-knowledge-eval.py --models $(MODEL) +eval-model: ## Blind-test one model (MODEL=claude-haiku-4-5 TARGET=0.16) + uv run scripts/zig-knowledge-eval.py --target $(TARGET) --models $(MODEL) -compile-test: ## Compile-test saved probes (MODEL=claude-sonnet-4-6) - ./scripts/zig-knowledge-test.sh probes/$(MODEL) +compile-test: ## Compile-test saved probes (MODEL=... TARGET=0.16) + ./scripts/zig-knowledge-test.sh probes/$(TARGET)/$(MODEL) # --- Compiler Probes --- -audit: ## Validate breaking change claims against current Zig - ./scripts/zig-knowledge-audit.sh +audit: ## Validate breaking-change claims (auto-detects Zig version) + @if zig version 2>/dev/null | grep -q '^0\.15'; then \ + ./scripts/zig-knowledge-audit-0.15.sh; \ + else \ + ./scripts/zig-knowledge-audit-0.16.sh; \ + fi + +audit-0.15: ## Validate 0.15.x claims (requires Zig 0.15.x) + ./scripts/zig-knowledge-audit-0.15.sh + +audit-0.16: ## Validate 0.16 claims (requires Zig 0.16) + ./scripts/zig-knowledge-audit-0.16.sh # --- Cleanup --- diff --git a/plugins/zig-claude-kit/README.md b/plugins/zig-claude-kit/README.md index 0b01272..5b5d1eb 100644 --- a/plugins/zig-claude-kit/README.md +++ b/plugins/zig-claude-kit/README.md @@ -1,41 +1,64 @@ # zig-claude-kit -Claude generates broken Zig 0.15.x code. This plugin -fixes it by injecting correct patterns into your -project's CLAUDE.md. +Claude generates broken Zig code. This plugin fixes it by +injecting correct patterns into your project's CLAUDE.md. +Supports both **Zig 0.15.x** and **Zig 0.16** -- detected +automatically from `build.zig.zon`. ## The Problem -Claude's Zig training predates 0.15.x. Fourteen test -probes cover twelve broken patterns that produce code -which fails to compile. Testing against Opus 4.6 and -Sonnet 4.6 without project context confirmed all twelve -persist across fresh conversations. +Claude's Zig training predates 0.15.x and is doubly outdated +for 0.16 (the I/O-as-Interface release, 2026-04-14). Fourteen +test probes cover the patterns models consistently get wrong +in blind testing. ## What It Corrects -1. **Writergate** -- `getStdOut()`/`getStdErr()` removed; - buffered writer pattern required -2. **build.zig** -- `.root_source_file` moved inside - `.root_module = b.createModule(...)` -3. **Format specifiers** -- generic `{}` removed; use - `{s}`, `{d}`, `{any}`, or `{f}` for format methods -4. **usingnamespace** -- removed from language -5. **BoundedArray** -- removed; use +**Carried over from 0.15 (still wrong in 0.16):** + +1. `usingnamespace` -- removed from the language +2. `async` / `await` keywords -- removed from the language +3. `std.BoundedArray` -- removed; use `ArrayListUnmanaged.initBuffer` -6. **ArrayList.init()** -- managed API removed; use - `ArrayListUnmanaged{}` with allocator per call -7. **Signed division** -- `/` on runtime signed integers - requires `@divTrunc` -8. **tokenize** -- renamed to `tokenizeAny`, - `tokenizeScalar`, `tokenizeSequence` -9. **process.args()** -- now `argsAlloc(allocator)`; - returns owned slice -10. **For-loop index** -- requires explicit range: - `for (items, 0..) |item, i|` -11. **async/await** -- removed from the language -12. **JSON Parser** -- redesigned to - `std.json.parseFromSlice` +4. `std.ArrayList(T).init(allocator)` -- managed API removed +5. `/` on runtime signed integers -- requires `@divTrunc` +6. `std.mem.tokenize` -- renamed to `tokenizeAny` / + `tokenizeScalar` / `tokenizeSequence` +7. `std.json.Parser` -- redesigned to + `std.json.parseFromSlice` +8. `for (items) |item, i|` -- requires explicit + `for (items, 0..)` +9. Format method signature: `pub fn format(self, writer)` +10. `build.zig` uses `.root_module = b.createModule(...)` + +**New in 0.16:** + +11. `std.io` -> `std.Io` (and `std.fs.File` -> `std.Io.File`, + `std.fs.Dir` -> `std.Io.Dir`, `std.fs.cwd()` -> + `std.Io.Dir.cwd()`) +12. "Juicy Main": `pub fn main(init: std.process.Init) !void` + brings `gpa`, `io`, `arena`, `environ_map`, `preopens` +13. Every blocking call takes `io` -- `file.close(io)`, + `file.writeStreaming(io, ...)`, `dir.createDir(io, name)` +14. `std.mem.indexOf*` renamed to `find*` (note + `findScalarLast`, with `Last` after `Scalar`) +15. `std.os.environ` gone -- use `init.environ_map` +16. `std.process.argsAlloc` / `argsFree` gone -- use + `init.minimal.args.toSlice(allocator)` +17. `std.process.getCwd` -> `currentPath(io, buf)` +18. `std.process.Child.init(...).spawn()` -> spawn(io, {...})` +19. `std.Thread.Mutex` / `Condition` / `WaitGroup` / + `Pool` -- moved to `std.Io.*` (Pool replaced by + `std.Io.async` / `Group.async`) +20. `std.crypto.random.bytes` -> `io.random(&buf)` +21. `std.time.Instant` / `Timer` / `timestamp` -> single + `std.Io.Timestamp` +22. `@Type` -> 8 builtins (`@Int`, `@Tuple`, `@Struct`, etc.) +23. Managed hash maps gone -- `array_hash_map.Auto` / `String` + / `Custom` with `.empty` +24. Error renames: `RenameAcrossMountPoints` / + `NotSameFileSystem` -> `CrossDevice`, `SharingViolation` + -> `FileBusy`, `FileTooBig` -> `StreamTooLong` ## Install @@ -46,63 +69,51 @@ persist across fresh conversations. ## Use -Open a Zig project. The plugin detects Zig source files -and prompts you to run `/zig-init`. That command appends -corrections to your CLAUDE.md. Every agent reads them -as project context. +Open a Zig project. The plugin detects Zig source files, +reads `build.zig.zon`'s `minimum_zig_version` (or falls back +to `zig version` or the default 0.16), and prompts you to +run `/zig-init`. That command appends the matching +corrections to your CLAUDE.md. Every agent reads them as +project context. **Commands:** -- `/zig-init` -- inject corrections into CLAUDE.md -- `/zig-patterns` -- quick reference with code examples -- `/zig-check` -- audit source files for outdated APIs +- `/zig-init` -- inject version-matched corrections into + CLAUDE.md +- `/zig-patterns` -- quick reference for both 0.15 and 0.16 +- `/zig-check` -- audit source files for outdated APIs (uses + the detected version's ruleset) + +If detection picks the wrong version (e.g. you haven't bumped +`build.zig.zon` yet), set `minimum_zig_version` to match and +re-run `/zig-init`. ## Verify Run the blind-test suite to confirm corrections remain -necessary: +necessary against the current Zig: ```bash -make eval # test all models -make eval-model MODEL=claude-haiku-4-5 # test one model -make audit # probe current Zig -make compile-test MODEL=claude-sonnet-4-6 -``` - -**Prerequisites:** `ANTHROPIC_API_KEY` and `uv`. +make audit # auto-detects from `zig version` +make audit-0.15 # validate 0.15.x claims +make audit-0.16 # validate 0.16 claims -## Latest Results (2026-02-27) - -Tested against Zig 0.15.2, no project context. - -| Probe | Sonnet 4.6 | Opus 4.6 | -|-------|------------|----------| -| 01 stdout (Writergate) | FAIL | FAIL | -| 02 stderr (Writergate) | FAIL | FAIL* | -| 03 ArrayList | FAIL | FAIL | -| 04 BoundedArray | FAIL | FAIL | -| 05 tokenize | pass | FAIL | -| 06 testing | pass | pass | -| 07 process args | FAIL | FAIL | -| 08 JSON | pass | FAIL | -| 09 format method | FAIL | FAIL | -| 10 mixin (usingnamespace) | FAIL | FAIL | -| 11 division | pass | pass | -| 12 for loop with index | pass | pass | -| 13 build.zig | FAIL* | FAIL* | -| 14 async/await | FAIL | FAIL | +make eval TARGET=0.16 # blind-test default models +make eval-model MODEL=claude-haiku-4-5 TARGET=0.16 +make compile-test MODEL=claude-sonnet-4-6 TARGET=0.16 +``` -\* Compiled only due to lazy analysis. Manual inspection -confirmed wrong patterns. +**Prerequisites:** `ANTHROPIC_API_KEY` and `uv` for eval; +`zig` on `PATH` for audit and compile-test. When all probes pass without corrections, retire this plugin. ## Reference -- [Breaking Changes](docs/ZIG_BREAKING_CHANGES.md) -- - full reference with error diagnostics -- [CLAUDE.md Fragment](docs/claude-md-fragment.md) -- - corrections appended by `/zig-init` +- [Breaking Changes -- 0.15.x](docs/ZIG_BREAKING_CHANGES-0.15.md) +- [Breaking Changes -- 0.16](docs/ZIG_BREAKING_CHANGES-0.16.md) +- [CLAUDE.md Fragment -- 0.15.x](docs/claude-md-fragment-0.15.md) +- [CLAUDE.md Fragment -- 0.16](docs/claude-md-fragment-0.16.md) ## License diff --git a/plugins/zig-claude-kit/docs/ZIG_BREAKING_CHANGES.md b/plugins/zig-claude-kit/docs/ZIG_BREAKING_CHANGES-0.15.md similarity index 100% rename from plugins/zig-claude-kit/docs/ZIG_BREAKING_CHANGES.md rename to plugins/zig-claude-kit/docs/ZIG_BREAKING_CHANGES-0.15.md diff --git a/plugins/zig-claude-kit/docs/ZIG_BREAKING_CHANGES-0.16.md b/plugins/zig-claude-kit/docs/ZIG_BREAKING_CHANGES-0.16.md new file mode 100644 index 0000000..dc6e7f9 --- /dev/null +++ b/plugins/zig-claude-kit/docs/ZIG_BREAKING_CHANGES-0.16.md @@ -0,0 +1,899 @@ +# Zig 0.16 Breaking Changes - Training Override Sheet + +This document corrects Claude's outdated training data with current +Zig 0.16 reality. 0.16 (released 2026-04-14) is a Writergate-scale +churn release on top of 0.15.x. The headline is **"I/O as an +Interface"**, and it ripples through `std.fs`, `std.process`, +`std.Thread`, `std.crypto`, `std.time`, and the language itself. + +If you learned Zig from 0.13/0.14 (the "managed ArrayList" generation), +or from 0.15 (the "Writergate" generation), **most of what you know +about touching the outside world is now wrong**. I/O, filesystem, +processes, threads, randomness, time, and even `main` itself have new +shapes. + +This sheet covers only the patterns models consistently get wrong in +blind testing. For the full landscape, consult the official 0.16 +release notes. + +## Quick Reference Table + +| Old (<=0.15.x) | New (0.16) | +|------------------------------------------------|---------------------------------------------------------------------| +| `pub fn main() !void` | `pub fn main(init: std.process.Init) !void` ("Juicy Main") | +| `std.io` namespace | `std.Io` (capitalized; old name deprecated) | +| `std.fs.Dir` | `std.Io.Dir` | +| `std.fs.File` | `std.Io.File` | +| `std.fs.cwd()` | `std.Io.Dir.cwd()` | +| `file.close()` | `file.close(io)` | +| `file.write(bytes)` / `writeAll` | `file.writeStreaming(io, ...)` / `writeStreamingAll(io, ...)` | +| `file.read(buf)` | `file.readStreaming(io, ...)` | +| `dir.makeDir(name)` | `dir.createDir(io, name)` | +| `dir.makePath(p)` | `dir.createDirPath(io, p)` | +| `file.chmod(mode)` | `file.setPermissions(io, ...)` | +| `file.getEndPos()` / `setEndPos()` | `file.length(io)` / `setLength(io, ...)` | +| `std.mem.indexOf(u8, haystack, needle)` | `std.mem.find(u8, haystack, needle)` | +| `std.mem.indexOfScalar(u8, s, c)` | `std.mem.findScalar(u8, s, c)` | +| `std.mem.lastIndexOf(...)` | `std.mem.findLast(...)` | +| `std.mem.lastIndexOfScalar(...)` | `std.mem.findScalarLast(...)` (note: `Last` after `Scalar`) | +| `std.os.environ` | gone -- use `init.environ_map` from `std.process.Init` | +| `std.process.argsAlloc(allocator)` | `init.minimal.args.toSlice(allocator)` | +| `std.process.getCwd(buf)` / `getCwdAlloc(a)` | `std.process.currentPath(io, buf)` / `currentPathAlloc(io, a)` | +| `std.process.Child.init(...).spawn()` | `std.process.spawn(io, .{ .argv, .stdin, ... })` | +| `std.process.execv(arena, argv)` | `std.process.replace(io, .{ .argv })` | +| `std.posix.PROT.READ \| std.posix.PROT.WRITE` | `.{ .READ = true, .WRITE = true }` | +| `std.posix.mlock(slice)` | `std.process.lockMemory(slice, .{})` | +| `std.Thread.Mutex` / `Condition` / `Semaphore` | `std.Io.Mutex` / `Io.Condition` / `Io.Semaphore` | +| `std.Thread.Pool` + `spawnWg` | `std.Io.async` / `std.Io.Group.async` (Pool removed) | +| `std.crypto.random.bytes(&buf)` | `io.random(&buf)` | +| `std.time.Instant` / `Timer` / `timestamp` | `std.Io.Timestamp` (one type) / `std.Io.Timestamp.now` | +| `@Type(.{ .int = .{ ... } })` | `@Int(.unsigned, 10)` (and 7 sibling builtins) | +| `@cImport({ @cInclude(...) })` | `b.addTranslateC(...)` in build.zig | +| `error.RenameAcrossMountPoints` / `NotSameFileSystem` | `error.CrossDevice` | +| `error.SharingViolation` | `error.FileBusy` | +| `error.FileTooBig` (readFileAlloc) | `error.StreamTooLong` | +| Managed `AutoArrayHashMap` etc. | `std.array_hash_map.Auto` etc. with `.empty` | + +## Error Messages That Mean Your Training Is Wrong + +``` +"no member named 'io'" in std namespace +-> std.io renamed to std.Io (capitalized) + +"no member named 'File'" / "no member named 'Dir'" in std.fs +-> Moved: std.Io.File / std.Io.Dir + +"expected 2 arguments, found 1" on file.close() +-> close() now takes io. Same for write, read, openFile, etc. + +"no member named 'makeDir'" +-> Renamed to createDir(io, name) + +"no member named 'argsAlloc'" / "argsFree" +-> Use init.minimal.args.toSlice(allocator) instead + +"no member named 'environ'" in std.os +-> Use init.environ_map plumbed from main + +"no member named 'getCwd'" in std.process +-> Renamed to currentPath / currentPathAlloc (takes io) + +"no member named 'indexOf'" / "indexOfScalar" in std.mem +-> Renamed to find / findScalar (and findLast, findScalarLast) + +"no member named 'Pool'" in std.Thread +-> Removed; use std.Io.async or std.Io.Group.async + +"no member named 'Instant'" / "Timer" in std.time +-> Use std.Io.Timestamp + +"no member named 'Type'" / "@Type undefined" +-> Split into @Int, @Tuple, @Struct, @Union, @Enum, @Pointer, @Fn, + @EnumLiteral. No @Float / @Array / @Optional -- write the literal. + +"error: returning address of expired local variable" +-> Trivial &local returns now rejected. Use `return undefined;` if + you actually want an invalid pointer. + +"runtime vector indexing forbidden" +-> Coerce vector to array first + +"no field named 'name' on std.process.Init" +-> Did you mean init.minimal? args / environ live there. +``` + +## The Headline: I/O as an Interface + +**The single biggest change in 0.16.** Anything that potentially +**blocks control flow** or **introduces nondeterminism** now takes an +`Io` parameter -- file I/O, networking, timers, random, sleep, sync +primitives, child processes, even fetching the cwd. + +### Implementations of `Io` + +- `Io.Threaded` -- threaded backend. Default chosen by Juicy Main. + Feature-complete and well-tested. Use `-fno-single-threaded` for + task-level concurrency, `-fsingle-threaded` to disable it. +- `Io.Evented` -- experimental userspace stack switching. +- `Io.Uring` -- Linux io_uring (PoC). +- `Io.Kqueue` -- macOS/BSD kqueue (PoC). +- `Io.Dispatch` -- macOS GCD. +- `Io.failing` -- simulates a system supporting no operations. + +### "Juicy Main" + +The first parameter of `pub fn main` may be one of: + +1. **Missing** -- `pub fn main() void` still legal, but blind. +2. **`process.Init.Minimal`** -- only argv and environ in raw form. +3. **`process.Init`** -- full set: `gpa`, `io`, `arena`, + `environ_map`, `preopens`, plus nested `minimal`. + +**Old:** +```zig +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + var stdout_buffer: [8192]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); + const stdout = &stdout_writer.interface; + defer stdout.flush() catch {}; + + try stdout.print("Hello\n", .{}); +} +``` + +**New (0.16):** +```zig +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const gpa = init.gpa; + const io = init.io; + _ = gpa; + + try std.Io.File.stdout().writeStreamingAll(io, "Hello\n"); + + const args = try init.minimal.args.toSlice(init.arena.allocator()); + for (args) |arg| std.log.info("arg: {s}", .{arg}); +} +``` + +`main`'s contract changed; you now receive `gpa`, `io`, an `arena`, an +`environ_map`, and `preopens` for free. + +### When You Don't Have an `Io` + +```zig +var threaded: std.Io.Threaded = .init_single_threaded; +const io = threaded.io(); +``` + +Treat this like `std.heap.page_allocator` -- a last resort. The +recommended fix is **plumb `Io` through as a parameter**, ideally from +`main`. + +### Tests Get a Free `Io` + +```zig +test "demo" { + const io = std.testing.io; + const file = try std.Io.Dir.cwd().openFile(io, "hello.txt", .{}); + defer file.close(io); +} +``` + +Like `std.testing.allocator`, there is now `std.testing.io`. + +## Stdout / stderr / stdin + +The 0.16 lang ref's "Hello World" uses the **unbuffered** path: + +```zig +pub fn main(init: std.process.Init) !void { + try std.Io.File.stdout().writeStreamingAll(init.io, "Hello, World!\n"); +} +``` + +### Buffered Writer Pattern + +```zig +pub fn main(init: std.process.Init) !void { + var stdout_buffer: [4096]u8 = undefined; + var stdout_writer = + std.Io.File.stdout().writerStreaming(init.io, &stdout_buffer); + const stdout = &stdout_writer.interface; + defer stdout.flush() catch {}; + + var stderr_buffer: [4096]u8 = undefined; + var stderr_writer = + std.Io.File.stderr().writerStreaming(init.io, &stderr_buffer); + const stderr = &stderr_writer.interface; + defer stderr.flush() catch {}; + + try stdout.print("hello: {s}\n", .{name}); +} +``` + +Use `writerStreaming` (not `writer`) for stdout/stderr -- the +positional `writer()` ignores O_APPEND and overwrites instead of +appending on macOS. + +Reader and writer signatures are **symmetric**: +```zig +pub fn reader(file: File, io: Io, buffer: []u8) Reader +pub fn readerStreaming(file: File, io: Io, buffer: []u8) Reader +pub fn writer(file: File, io: Io, buffer: []u8) Writer +pub fn writerStreaming(file: File, io: Io, buffer: []u8) Writer +``` + +### Removed I/O Types + +- `std.io.GenericReader` / `AnyReader` -- collapsed into `std.Io.Reader` +- `std.Io.GenericWriter` / `AnyWriter` / `null_writer` / `CountingReader` +- `FixedBufferStream` -- replaced by `Reader.fixed` / `Writer.fixed` + +**Old:** +```zig +var fbs = std.io.fixedBufferStream(data); +const reader = fbs.reader(); +``` + +**New:** +```zig +var reader: std.Io.Reader = .fixed(data); +var writer: std.Io.Writer = .fixed(buffer); +``` + +## Filesystem: `std.fs` -> `std.Io` + +All `fs` APIs moved to `Io`. Release notes describe this as "a lot of +breaking changes, but unlike Writergate, this changeset does not +require much critical thinking." Typical upgrade: + +**Old:** +```zig +file.close(); +``` + +**New:** +```zig +file.close(io); +``` + +### Namespace Moves + +| Old | New | +|----------------------------------|----------------------------------| +| `std.fs.Dir` | `std.Io.Dir` | +| `std.fs.File` | `std.Io.File` | +| `std.fs.cwd` | `std.Io.Dir.cwd` | +| `std.fs.path` | `std.Io.Dir.path` (alias) | +| `std.fs.max_path_bytes` | `std.Io.Dir.max_path_bytes` | +| `std.fs.has_executable_bit` | `std.Io.File.Permissions.has_executable_bit` | + +### Self-Executable Helpers + +| Old | New | +|--------------------------------|------------------------------------| +| `fs.openSelfExe` | `std.process.openExecutable` | +| `fs.selfExePath` | `std.process.executablePath` | +| `fs.selfExePathAlloc` | `std.process.executablePathAlloc` | +| `fs.selfExeDirPath` | `std.process.executableDirPath` | +| `fs.Dir.setAsCwd` | `std.process.setCurrentDir` | + +### Dir Method Renames + +| Old | New | +|----------------------------------|--------------------------------------| +| `Dir.makeDir` | `Dir.createDir` | +| `Dir.makePath` | `Dir.createDirPath` | +| `Dir.makeOpenDir` | `Dir.createDirPathOpen` | +| `Dir.atomicSymLink` | `Dir.symLinkAtomic` | +| `Dir.chmod` | `Dir.setPermissions` | +| `Dir.chown` | `Dir.setOwner` | +| `Dir.realpath` | `Dir.realPathFile` | +| `Dir.realpathAlloc` | `Dir.realPathFileAlloc` | + +`Dir.rename` now requires two `Dir` parameters plus `Io`. + +### File Method Renames + +| Old | New | +|----------------------------------|--------------------------------------| +| `File.Mode` | `File.Permissions` | +| `File.default_mode` | `File.Permissions.default_file` | +| `File.setEndPos` / `getEndPos` | `File.setLength` / `File.length` | +| `File.chmod` / `chown` | `File.setPermissions` / `setOwner` | +| `File.updateTimes` | `File.setTimestamps` / `setTimestampsNow` | +| `File.read` / `readv` | `File.readStreaming` | +| `File.pread` / `preadv` | `File.readPositional` | +| `File.write` / `writev` | `File.writeStreaming` | +| `File.writeAll` | `File.writeStreamingAll` | +| `File.pwrite` / `pwritev` | `File.writePositional` | +| `File.pwriteAll` | `File.writePositionalAll` | + +### Signature Reshapes -- Not Mechanical + +**`readFileAlloc`:** +```zig +// Old +const contents = try std.fs.cwd().readFileAlloc(allocator, name, 1234); + +// New +const contents = try std.Io.Dir.cwd().readFileAlloc( + io, name, allocator, .limited(1234), +); +``` + +Limit semantics changed: reaching the limit returns the error +(`error.StreamTooLong`, formerly `FileTooBig`). + +**`readToEndAlloc`:** +```zig +// Old +const contents = try file.readToEndAlloc(allocator, 1234); + +// New +var read_buffer: [4096]u8 = undefined; +var file_reader = file.reader(io, &read_buffer); +const contents = try file_reader.interface.allocRemaining( + allocator, .limited(1234), +); +``` + +### Atomic File Rewrite + +```zig +var atomic_file = try dest_dir.createFileAtomic(io, dest_path, .{ + .permissions = perms, + .make_path = true, + .replace = true, +}); +defer atomic_file.deinit(io); + +var buffer: [1024]u8 = undefined; +var file_writer = atomic_file.file.writer(io, &buffer); +// ... use file_writer +try file_writer.flush(); +try atomic_file.replace(io); +``` + +Buffer ownership moved from the atomic-file struct to the writer. + +## `std.mem.indexOf*` -> `find*` + +The rename rule: "find" returns the index of a substring; "pos" is a +starting-index parameter; "last" searches from the end; "scalar" means +the substring is one element. + +| Old | New | +|------------------------------|---------------------------| +| `indexOf` | `find` | +| `indexOfScalar` | `findScalar` | +| `indexOfPos` | `findPos` | +| `indexOfAny` | `findAny` | +| `lastIndexOf` | `findLast` | +| `lastIndexOfScalar` | `findScalarLast` | +| `lastIndexOfAny` | `findLastAny` | + +**Note:** 0.16 puts the `Last` qualifier *after* `Scalar`, not before +(`findScalarLast`, not `findLastScalar`). There is no `findLastPos`. + +### New `cut*` Family + +`cut`, `cutPrefix`, `cutSuffix`, `cutScalar`, `cutLast`, +`cutScalarLast`. Split a string at the first (or last) occurrence of a +delimiter, returning the prefix/suffix pair -- the equivalent of Go's +`strings.Cut`. + +## Process State (Args & Env) Is No Longer Global + +`std.os.environ` was a footgun: declared global but not populatable +without libc, and unsoundly mutable from threads. **As of 0.16, +environment variables are available only through `main`'s parameter.** + +### Reading Args + +**Old:** +```zig +const args = try std.process.argsAlloc(allocator); +defer std.process.argsFree(allocator, args); +for (args[1..]) |arg| {} +``` + +**New (Init.Minimal, iterator):** +```zig +pub fn main(init: std.process.Init.Minimal) void { + var args = init.args.iterate(); + while (args.next()) |arg| { + std.log.info("arg: {s}", .{arg}); + } +} +``` + +**New (full Init, slice):** +```zig +pub fn main(init: std.process.Init) !void { + const args = try init.minimal.args.toSlice(init.arena.allocator()); + for (args) |arg| std.log.info("arg: {s}", .{arg}); +} +``` + +### Reading Environment + +**Old:** +```zig +const home = std.os.getenv("HOME"); // global +``` + +**New (full Init):** +```zig +pub fn main(init: std.process.Init) !void { + for (init.environ_map.keys(), init.environ_map.values()) |k, v| { + std.log.info("env: {s}={s}", .{ k, v }); + } +} +``` + +**New (Init.Minimal, lazy):** +```zig +pub fn main(init: std.process.Init.Minimal) !void { + var arena_allocator: std.heap.ArenaAllocator = + .init(std.heap.page_allocator); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + + const home = init.environ.getPosix("HOME"); // ?[]const u8 + const editor = try init.environ.getAlloc(arena, "EDITOR"); + _ = home; _ = editor; +} +``` + +Functions that need env vars accept either specific values or a +`*const process.Environ.Map` parameter -- same plumbing as `Allocator` +and `Io`. + +## `std.process` / `std.posix` Rewrites + +### Current Working Directory Renamed + +```zig +// Old +const cwd = try std.process.getCwd(buffer); +const cwd_alloc = try std.process.getCwdAlloc(allocator); + +// New +const cwd = try std.process.currentPath(io, buffer); +const cwd_alloc = try std.process.currentPathAlloc(io, allocator); +``` + +### Spawning a Child Process + +```zig +// Old +var child = std.process.Child.init(argv, gpa); +child.stdin_behavior = .Pipe; +child.stdout_behavior = .Pipe; +try child.spawn(io); + +// New +var child = try std.process.spawn(io, .{ + .argv = argv, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .pipe, +}); +``` + +`std.process.Child.run` -> `std.process.run(allocator, io, .{...})`. + +### Replacing the Current Process Image + +```zig +// Old +const err = std.process.execv(arena, argv); + +// New +const err = std.process.replace(io, .{ .argv = argv }); +``` + +### Type-Safe POSIX Flags + +```zig +// Old +std.posix.PROT.READ | std.posix.PROT.WRITE + +// New +.{ .READ = true, .WRITE = true } +``` + +mlock moved to `process`: + +```zig +// Old +try std.posix.mlock(); +try std.posix.mlockall(slice, std.posix.MCL_CURRENT); + +// New +try std.process.lockMemory(slice, .{}); +try std.process.lockMemoryAll(.{ .current = true }); +``` + +### `std.posix` Removals + +"Most `std.posix` and `std.os.windows` functions existed at an awkward +**medium-level abstraction** and have thus been removed. If you were +using any functions removed from those namespaces, you must now +choose a direction: **go higher (use `std.Io`) or go lower (use +`std.posix.system` directly)**." + +`ucontext_t` and friends are also removed. + +## Containers + +### Managed Hash Maps Removed + +```zig +// Old +var map: std.AutoArrayHashMap(K, V) = .init(allocator); +defer map.deinit(); + +// New +var map: std.array_hash_map.Auto(K, V) = .empty; +defer map.deinit(allocator); +``` + +| Old | New | +|----------------------------------|------------------------------| +| `ArrayHashMap` (managed) | gone -- use `array_hash_map.Custom` | +| `AutoArrayHashMap` (managed) | gone -- use `array_hash_map.Auto` | +| `StringArrayHashMap` (managed) | gone -- use `array_hash_map.String` | +| `AutoArrayHashMapUnmanaged` | `array_hash_map.Auto` | +| `StringArrayHashMapUnmanaged` | `array_hash_map.String` | +| `ArrayHashMapUnmanaged` | `array_hash_map.Custom` | + +Drop the "Managed" variants entirely; the unmanaged variants got the +short names. Pass `allocator` to each method. + +### `ArrayList` Uses `.empty` + +```zig +var list: std.ArrayList(u8) = .empty; +defer list.deinit(gpa); +try list.append(gpa, 'a'); +``` + +The "managed" form is gone -- only `unmanaged`-style remains, and it +took over the short name. + +### Other Container Changes + +- `SegmentedList` -- removed, no replacement. +- `PriorityQueue` / `PriorityDequeue` -- lose allocator field, use + `.empty`. Methods renamed: `add` -> `push`, `remove*` -> `pop*`. +- BitSet / EnumSet -- `initEmpty` / `initFull` replaced by `.empty` / + `.full` decl literals. + +## Sync Primitives: `Thread.*` -> `Io.*` + +Sync APIs moved to `std.Io` so synchronized code integrates with the +chosen I/O backend (a contended mutex blocks the thread under +`Io.Threaded`, switches stacks under `Io.Evented`). + +| Old | New | +|---------------------------|--------------------| +| `std.Thread.ResetEvent` | `std.Io.Event` | +| `std.Thread.WaitGroup` | `std.Io.Group` | +| `std.Thread.Futex` | `std.Io.Futex` | +| `std.Thread.Mutex` | `std.Io.Mutex` | +| `std.Thread.Condition` | `std.Io.Condition` | +| `std.Thread.Semaphore` | `std.Io.Semaphore` | +| `std.Thread.RwLock` | `std.Io.RwLock` | +| `std.once` | removed | +| `std.Thread.Mutex.Recursive` | removed | + +### `std.Thread.Pool` Removed + +```zig +// Old +fn doAllTheWork(pool: *std.Thread.Pool) void { + var wg: std.Thread.WaitGroup = .{}; + pool.spawnWg(wg, doSomeWork, .{ pool, &wg, item }); + wg.wait(); +} + +// New +fn doAllTheWork(io: std.Io) !void { + var g: std.Io.Group = .init; + errdefer g.cancel(io); + g.async(io, doSomeWork, .{ io, &g, item }); + try g.await(io); +} +``` + +Lock-free atomics do **not** require `Io` integration. + +## Allocators + +### `ArenaAllocator` Is Lock-Free Thread-Safe + +No API change. Drop any `ThreadSafeAllocator` wrapping you had. Roughly +matches single-threaded performance up to ~7 concurrent threads. + +### `heap.ThreadSafeAllocator` Removed + +The release notes call it "an anti-pattern". Make the underlying +allocator lock-free instead. + +### `DebugAllocator` Renamed + +`GeneralPurposeAllocator` -> `DebugAllocator`. For release builds, +prefer `std.heap.smp_allocator` if you don't have a clear arena +lifetime. + +## Random / Time / Format + +### Entropy + +```zig +// Old +std.crypto.random.bytes(&buf); +posix.getrandom(&buf); + +// New +io.random(&buf); +io.randomSecure(&buf); // bypasses any in-process RNG state +``` + +### Time + +| Old | New | +|---------------------------|------------------------------------| +| `std.time.Instant` | `std.Io.Timestamp` | +| `std.time.Timer` | `std.Io.Timestamp` | +| `std.time.timestamp()` | `std.Io.Timestamp.now` | + +### Format + +| Old | New | +|---------------------------|------------------------------------| +| `std.fmt.Formatter` | `std.fmt.Alt` | +| `std.fmt.format` | `std.Io.Writer.print` | +| `std.fmt.FormatOptions` | `std.fmt.Options` | +| `std.fmt.bufPrintZ` | `std.fmt.bufPrintSentinel` | + +The custom `format` method signature +(`pub fn format(self: T, writer: *std.Io.Writer) !void`) is unchanged +from 0.15. + +## Error Renames + +| Old | New | +|--------------------------------------|----------------------------------| +| `error.RenameAcrossMountPoints` | `error.CrossDevice` | +| `error.NotSameFileSystem` | `error.CrossDevice` | +| `error.SharingViolation` | `error.FileBusy` | +| `error.EnvironmentVariableNotFound` | `error.EnvironmentVariableMissing` | +| `error.FileTooBig` (`readFileAlloc`) | `error.StreamTooLong` | +| `Dir.rename` non-empty dest | `error.DirNotEmpty` (was `PathAlreadyExists`) | + +## Language-Level Changes + +### `@Type` Split Into Eight Builtins + +`@Type` is gone. Replacements: + +```zig +@EnumLiteral() type +@Int(comptime signedness: std.builtin.Signedness, comptime bits: u16) type +@Tuple(comptime field_types: []const type) type +@Pointer(size, attrs, Element, sentinel) type +@Fn(param_types, param_attrs, ReturnType, attrs) type +@Struct(layout, BackingInt, field_names, field_types, field_attrs) type +@Union(layout, ArgType, field_names, field_types, field_attrs) type +@Enum(TagInt, mode, field_names, field_values) type +``` + +```zig +// Old +@Type(.{ .int = .{ .signedness = .unsigned, .bits = 10 } }) +// New +@Int(.unsigned, 10) + +// Old +@Type(.enum_literal) +// New +@EnumLiteral() +``` + +There is **no** `@Float`, `@Array`, `@Opaque`, `@Optional`, or +`@ErrorUnion` -- write the literal type (`f32`, `[N]T`, `opaque {}`, +`?T`, `E!T`). No `@ErrorSet` either; declare with `error{ ... }`. + +### `@cImport` Deprecated + +`@cImport` still exists but is deprecated. Use `b.addTranslateC(...)` +in `build.zig`: + +```zig +// build.zig +const translate_c = b.addTranslateC(.{ + .root_source_file = b.path("src/c.h"), + .target = target, + .optimize = optimize, +}); +translate_c.linkSystemLibrary("glfw", .{}); + +const exe = b.addExecutable(.{ + .name = "app", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .imports = &.{ .{ + .name = "c", + .module = translate_c.createModule(), + } }, + }), +}); +``` + +Then `const c = @import("c");` in source. + +### Runtime Vector Indexing Forbidden + +```zig +// New: coerce to array first +const vt = @typeInfo(@TypeOf(vec)).vector; +const arr: [vt.len]vt.child = vec; +for (&arr) |elem| { _ = elem; } +``` + +### Trivial Local-Address Returns Forbidden + +```zig +fn foo() *i32 { + var x: i32 = 1234; + return &x; // error: returning address of expired local +} +``` + +Spell it as `return undefined;` if you genuinely want an invalid +pointer. + +### Small Int -> Float Auto-Coercion + +`u24` -> `f32` coerces implicitly (precision bits fit). `u25` still +requires `@floatFromInt`. + +### `@floor`/`@ceil`/`@round`/`@trunc` Convert to Integers + +```zig +fn round_to_int(value: f32) u8 { + return @round(value); // returns u8 directly +} +``` + +`@intFromFloat` is now redundant with `@trunc` and is deprecated. + +## Build System + +### `build.zig.zon` Required Fields + +`zig build` **fails** when: +- A dependency has no `fingerprint` field, or +- A dependency's `name` is a string rather than an enum literal. + +Legacy hash format support is removed. + +### Local Forks + +```sh +zig build --fork=/path/to/local/checkout +``` + +The fork path must contain a `build.zig.zon` with matching `name` and +`fingerprint`. + +### Packages in `zig-pkg/` + +Dependencies land in `zig-pkg/` next to `build.zig` (was +`$GLOBAL_ZIG_CACHE/p/$HASH`). Don't commit `zig-pkg/`. + +### New Flags + +- `--test-timeout 500ms` -- per-`test`-block timeout (real time). +- `--error-style verbose|minimal|verbose_clear|minimal_clear` -- + replaces removed `--prominent-compile-errors`. +- `--multiline-errors indent|newline|none`. + +## Carry-Over from 0.15.x + +These breaking changes from 0.15 are **still in effect** in 0.16 -- +your training is wrong about them too. + +### Removed Language Features (from 0.15) + +- `usingnamespace` -- replaced by zero-bit fields + `@fieldParentPtr` +- `async` / `await` keywords -- now library features (`std.Io.async`) +- `@frameSize` builtin + +### Format Method Signature (from 0.15) + +```zig +// WRONG (your training) +pub fn format(self: T, comptime fmt: []const u8, + options: std.fmt.FormatOptions, writer: anytype) !void { ... } + +// RIGHT (0.15 and 0.16) +pub fn format(self: T, writer: *std.Io.Writer) + std.Io.Writer.Error!void { + try writer.print("{d}", .{self.x}); +} +``` + +Use `{f}` in format strings to call format methods, not `{}`. + +### ArrayList Allocator-Per-Method (from 0.15) + +```zig +var list: std.ArrayList(u8) = .empty; +defer list.deinit(allocator); +try list.append(allocator, 'a'); +``` + +### Division on Signed Integers (from 0.15) + +```zig +// RIGHT +const q = @divTrunc(a, b); +const r = @rem(a, b); +``` + +`/` and `%` on runtime signed integers are still compile errors. + +### Tokenize Renames (from 0.15) + +```zig +std.mem.tokenizeAny(u8, text, " \t"); +std.mem.tokenizeScalar(u8, text, ' '); +std.mem.tokenizeSequence(u8, text, "=="); +``` + +### For-Loop Explicit Index (from 0.15) + +```zig +for (items, 0..) |item, i| {} +``` + +### `BoundedArray` Replacement (from 0.15) + +```zig +var buffer: [64]u8 = undefined; +var stack = std.ArrayListUnmanaged(u8).initBuffer(&buffer); +``` + +### JSON Redesign (from 0.15) + +```zig +const parsed = try std.json.parseFromSlice(T, allocator, text, .{}); +defer parsed.deinit(); +``` + +### Testing Parameter Order (from 0.15) + +```zig +try std.testing.expectEqual(expected, actual); +``` + +## When You're Stuck + +1. Grep this file for the symbol or error message +2. Check the 0.16 release notes +3. Check the 0.16 lang ref +4. As a last resort, read `lib/std/Io/...` source in your installed + Zig -- the lang ref is incomplete, the source is not + +The language changed fundamentally. Your instincts are wrong. Always +verify. diff --git a/plugins/zig-claude-kit/docs/claude-md-fragment.md b/plugins/zig-claude-kit/docs/claude-md-fragment-0.15.md similarity index 100% rename from plugins/zig-claude-kit/docs/claude-md-fragment.md rename to plugins/zig-claude-kit/docs/claude-md-fragment-0.15.md diff --git a/plugins/zig-claude-kit/docs/claude-md-fragment-0.16.md b/plugins/zig-claude-kit/docs/claude-md-fragment-0.16.md new file mode 100644 index 0000000..12a9a36 --- /dev/null +++ b/plugins/zig-claude-kit/docs/claude-md-fragment-0.16.md @@ -0,0 +1,197 @@ +## Zig 0.16 Training Corrections + +Your Zig training is outdated. This project uses Zig 0.16, which is a +**Writergate-scale churn release** -- "I/O as an Interface" plus a +filesystem move from `std.fs` to `std.Io`. Most of what you learned +about touching the outside world is now wrong. + +### The Patterns You Get Wrong + +Before writing ANY Zig code, internalize these corrections: + +1. **`pub fn main() !void`** -- still legal but blind. Use + `pub fn main(init: std.process.Init) !void` ("Juicy Main") to get + `gpa`, `io`, `arena`, `environ_map`, `preopens`, plus + `init.minimal.args` / `init.minimal.environ`. +2. **`std.io`** -- renamed to `std.Io` (capitalized). The old name is + deprecated. +3. **`std.fs.File` / `std.fs.Dir`** -- moved to `std.Io.File` / + `std.Io.Dir`. `std.fs.cwd()` -> `std.Io.Dir.cwd()`. +4. **Every blocking call now takes `io`**: `file.close(io)`, + `file.writeStreaming(io, ...)`, `dir.createDir(io, name)`, + `file.length(io)`, `file.setPermissions(io, ...)`. +5. **`Dir.makeDir` / `makePath`** -- renamed to `createDir` / + `createDirPath`. +6. **`File.write/writeAll`** -- renamed to `writeStreaming` / + `writeStreamingAll`. Positional variants are `writePositional` / + `writePositionalAll`. +7. **`std.mem.indexOf*`** -- renamed to `find*` (`indexOf` -> `find`, + `indexOfScalar` -> `findScalar`, `lastIndexOf` -> `findLast`, + `lastIndexOfScalar` -> `findScalarLast`). Note `Last` comes after + `Scalar`. +8. **`std.os.environ`** -- gone. Use `init.environ_map` plumbed + through from `main`. +9. **`std.process.argsAlloc(allocator)`** -- gone. Use + `init.minimal.args.toSlice(allocator)` or the iterator + `init.minimal.args.iterate()`. +10. **`std.process.Child.init(...).spawn()`** -- gone. Use + `std.process.spawn(io, .{ .argv, .stdin, .stdout, .stderr })`. +11. **`std.process.getCwd` / `getCwdAlloc`** -- renamed to + `std.process.currentPath(io, buf)` / + `std.process.currentPathAlloc(io, allocator)`. +12. **`std.posix.PROT.READ | std.posix.PROT.WRITE`** -- replaced by + type-safe struct: `.{ .READ = true, .WRITE = true }`. +13. **`std.Thread.Mutex` / `Condition` / `Semaphore` / `WaitGroup`** + -- moved to `std.Io.Mutex` / `Io.Condition` / `Io.Semaphore` / + `Io.Group`. `std.Thread.Pool` removed -- use `std.Io.async` / + `std.Io.Group.async`. +14. **`std.crypto.random.bytes(&buf)`** -- replaced by + `io.random(&buf)`. Use `io.randomSecure(&buf)` when entropy must + bypass any in-process RNG state. +15. **`std.time.Instant` / `Timer` / `timestamp`** -- collapsed into + `std.Io.Timestamp` (one type) and `std.Io.Timestamp.now`. +16. **`@Type(.{ .int = ... })`** -- replaced by eight new builtins: + `@Int(.unsigned, 10)`, `@EnumLiteral()`, `@Tuple`, `@Pointer`, + `@Fn`, `@Struct`, `@Union`, `@Enum`. There is no `@Float`, + `@Array`, `@Optional`, or `@ErrorUnion` -- write the literal type. +17. **`@cImport({ @cInclude(...) })`** -- deprecated. Use + `b.addTranslateC(...)` in `build.zig`. +18. **`error.RenameAcrossMountPoints` / `NotSameFileSystem`** -> + `error.CrossDevice`. **`error.SharingViolation`** -> + `error.FileBusy`. **`error.FileTooBig` (readFileAlloc)** -> + `error.StreamTooLong`. +19. **`Dir.rename` on non-empty destination** -- returns + `error.DirNotEmpty`, not `error.PathAlreadyExists`. +20. **Managed hash maps** -- gone. `AutoArrayHashMap` -> + `std.array_hash_map.Auto`. Same for `StringArrayHashMap` -> + `String`, `ArrayHashMap` -> `Custom`. Use `.empty` initializer. +21. **`std.BoundedArray`** -- still gone (from 0.15). Use + `ArrayListUnmanaged.initBuffer`. +22. **`usingnamespace`** -- still gone. Use zero-bit fields with + `@fieldParentPtr`. +23. **`async` / `await` keywords** -- still gone. Concurrency now via + `std.Io.async` / `std.Io.Group.async`. + +### "Juicy Main" -- Memorize This + +```zig +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const gpa = init.gpa; + const io = init.io; + const arena = init.arena.allocator(); + _ = gpa; _ = arena; + + try std.Io.File.stdout().writeStreamingAll(io, "Hello, world!\n"); + + const args = try init.minimal.args.toSlice(init.arena.allocator()); + for (args) |arg| std.log.info("arg: {s}", .{arg}); +} +``` + +`std.process.Init` bundles a pre-initialized allocator (`gpa`), an +`Io` instance (`io`), an `arena`, an `environ_map`, and `preopens`. +The nested `init.minimal` carries the raw `args` and `environ`. + +If you genuinely don't need args/env, `pub fn main() !void` still +works. If you need only raw argv/environ without an allocator, +`pub fn main(init: std.process.Init.Minimal) !void` is the middle +option. + +### Buffered stdout/stderr (Writergate, 0.16 edition) + +```zig +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var stdout_buf: [4096]u8 = undefined; + var stdout_writer = + std.Io.File.stdout().writerStreaming(io, &stdout_buf); + const stdout = &stdout_writer.interface; + defer stdout.flush() catch {}; + + var stderr_buf: [4096]u8 = undefined; + var stderr_writer = + std.Io.File.stderr().writerStreaming(io, &stderr_buf); + const stderr = &stderr_writer.interface; + defer stderr.flush() catch {}; + + try stdout.print("count: {d}\n", .{42}); + _ = stderr; +} +``` + +Use `writerStreaming` (not `writer`) for stdout/stderr -- the +positional `writer()` form ignores O_APPEND and breaks shell `>>` +redirects on macOS. Reader and writer are symmetric: +`pub fn reader(file, io, buffer) Reader` / +`pub fn writer(file, io, buffer) Writer`. + +### When You Don't Have an `Io` + +Plumb it through. If you absolutely cannot, the escape hatch is: + +```zig +var threaded: std.Io.Threaded = .init_single_threaded; +const io = threaded.io(); +``` + +Treat this like `std.heap.page_allocator` -- a last resort. + +### Tests Get a Free `Io` + +```zig +test "demo" { + const io = std.testing.io; + const file = try std.Io.Dir.cwd().openFile(io, "hello.txt", .{}); + defer file.close(io); +} +``` + +Same shape as `std.testing.allocator`. + +### `ArrayList` and Hash Maps Use `.empty` + +```zig +var list: std.ArrayList(u8) = .empty; +defer list.deinit(gpa); +try list.append(gpa, 'a'); + +var map: std.array_hash_map.Auto(K, V) = .empty; +defer map.deinit(gpa); +``` + +The "managed" forms with bound allocators are gone -- pass the +allocator into every mutating method and `deinit`. + +### `build.zig` Pattern + +`addExecutable` still wraps the source in a module. Translate-C +moved into the build graph: + +```zig +const exe = b.addExecutable(.{ + .name = "app", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), +}); +``` + +For C interop, replace `@cImport` blocks with `b.addTranslateC(...)` +in `build.zig` and `@import("c")` in the source. + +### Shell Rules + +Run commands exactly as shown. Do NOT append shell syntax +like `2>&1`, `; echo "EXIT: $?"`, or pipe redirections. +The Bash tool already captures stdout, stderr, and exit codes. + +### Quick Lookup + +When you hit a compile error, run +`/zig-claude-kit:zig-patterns` for the full reference table, +code patterns, and error message diagnostics. diff --git a/plugins/zig-claude-kit/scripts/detect-zig-version.sh b/plugins/zig-claude-kit/scripts/detect-zig-version.sh new file mode 100755 index 0000000..d9dafc0 --- /dev/null +++ b/plugins/zig-claude-kit/scripts/detect-zig-version.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# detect-zig-version.sh - Identify which Zig version the project targets. +# +# Outputs "0.15" or "0.16" on stdout. Defaults to 0.16 (the current +# release) when nothing is detected. Never errors. +# +# Detection order: +# 1. build.zig.zon's minimum_zig_version field +# 2. Installed `zig version` +# 3. Fallback: 0.16 + +set -u + +# 1. Read minimum_zig_version from build.zig.zon +if [ -f "build.zig.zon" ]; then + version=$(grep -oE 'minimum_zig_version[[:space:]]*=[[:space:]]*"[^"]+"' \ + build.zig.zon 2>/dev/null \ + | sed -E 's/.*"([^"]+)"/\1/') + case "$version" in + 0.15*) echo "0.15"; exit 0 ;; + 0.16*) echo "0.16"; exit 0 ;; + 0.17*|0.18*|0.19*|0.[2-9]*|[1-9].*) echo "0.16"; exit 0 ;; + esac +fi + +# 2. Try installed zig compiler +if command -v zig >/dev/null 2>&1; then + version=$(zig version 2>/dev/null) + case "$version" in + 0.15*) echo "0.15"; exit 0 ;; + 0.16*) echo "0.16"; exit 0 ;; + 0.17*|0.18*|0.19*|0.[2-9]*|[1-9].*) echo "0.16"; exit 0 ;; + esac +fi + +# 3. Default to current release +echo "0.16" diff --git a/plugins/zig-claude-kit/scripts/session-start.sh b/plugins/zig-claude-kit/scripts/session-start.sh index 78de413..8fb5b43 100755 --- a/plugins/zig-claude-kit/scripts/session-start.sh +++ b/plugins/zig-claude-kit/scripts/session-start.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Detect Zig projects missing 0.15.x corrections. +# Detect Zig projects missing training corrections. # Runs at Claude Code session start via plugin hook. # Only act in Zig projects @@ -14,7 +14,9 @@ if grep -q "Writergate" CLAUDE.md 2>/dev/null; then exit 0 fi -FRAGMENT="$(dirname "$0")/../docs/claude-md-fragment.md" +SCRIPT_DIR="$(dirname "$0")" +ZIG_VERSION=$("$SCRIPT_DIR/detect-zig-version.sh") +FRAGMENT="$SCRIPT_DIR/../docs/claude-md-fragment-${ZIG_VERSION}.md" # Inject corrections as immediate context if [ -f "$FRAGMENT" ]; then @@ -23,15 +25,17 @@ if [ -f "$FRAGMENT" ]; then fi # Instruct Claude to alert the user immediately -cat <<'INSTRUCTIONS' +cat < "$file" + + if zig test "$file" --color off 2>/dev/null 1>/dev/null; then + actual="pass" + else + actual="fail" + fi + + if [[ "$actual" == "$expected" ]]; then + printf " ${GREEN}PASS${RESET} %-40s %s\n" \ + "$name" "$description" + PASS_COUNT=$((PASS_COUNT + 1)) + else + printf " ${RED}FAIL${RESET} %-40s %s ${DIM}(expected %s, got %s)${RESET}\n" \ + "$name" "$description" "$expected" "$actual" + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi +} + +echo "" +printf "${BOLD}Zig 0.16 Knowledge Audit${RESET}\n" +printf "${DIM}Testing 0.16 breaking change claims against zig $(zig version)${RESET}\n" + +# -- I/O namespace move -- + +printf "\n${BOLD}-- I/O namespace move (std.io -> std.Io) --${RESET}\n" + +probe "old_std_fs_File_stdout" \ + "std.fs.File.stdout() moved" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + var buf: [4096]u8 = undefined; + var w = std.fs.File.stdout().writer(&buf); + _ = &w; +} +ZIGEOF +)" + +probe "new_std_Io_File_stdout" \ + "std.Io.File.stdout() works" \ + "pass" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + const f = std.Io.File.stdout(); + _ = f; +} +ZIGEOF +)" + +probe "old_getStdOut" \ + "std.io.getStdOut() still removed" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + const stdout = std.io.getStdOut().writer(); + _ = stdout; +} +ZIGEOF +)" + +# -- Filesystem move -- + +printf "\n${BOLD}-- Filesystem (std.fs -> std.Io) --${RESET}\n" + +probe "old_std_fs_cwd" \ + "std.fs.cwd() moved" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + const dir = std.fs.cwd(); + _ = dir; +} +ZIGEOF +)" + +probe "new_std_Io_Dir_cwd" \ + "std.Io.Dir.cwd() works" \ + "pass" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + const dir = std.Io.Dir.cwd(); + _ = dir; +} +ZIGEOF +)" + +probe "old_makeDir" \ + "Dir.makeDir renamed to createDir" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + const io = std.testing.io; + try std.Io.Dir.cwd().makeDir("nope"); + _ = io; +} +ZIGEOF +)" + +probe "old_File_writeAll_no_io" \ + "File.writeAll without io fails" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + const f = std.Io.File.stdout(); + try f.writeAll("hi"); +} +ZIGEOF +)" + +# -- mem.indexOf -> find rename -- + +printf "\n${BOLD}-- mem.indexOf -> find rename --${RESET}\n" + +probe "old_mem_indexOf" \ + "std.mem.indexOf renamed to find" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + _ = std.mem.indexOf(u8, "hello", "ll"); +} +ZIGEOF +)" + +probe "new_mem_find" \ + "std.mem.find replacement works" \ + "pass" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + _ = std.mem.find(u8, "hello", "ll"); +} +ZIGEOF +)" + +probe "new_mem_findScalar" \ + "std.mem.findScalar replacement works" \ + "pass" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + _ = std.mem.findScalar(u8, "hello", 'e'); +} +ZIGEOF +)" + +# -- Process state -- + +printf "\n${BOLD}-- Process state (args/env no longer global) --${RESET}\n" + +probe "old_argsAlloc" \ + "std.process.argsAlloc removed" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + const args = try std.process.argsAlloc(std.testing.allocator); + defer std.process.argsFree(std.testing.allocator, args); +} +ZIGEOF +)" + +probe "old_os_environ" \ + "std.os.environ removed" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + _ = std.os.environ; +} +ZIGEOF +)" + +probe "old_getCwd" \ + "std.process.getCwd removed" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + var buf: [1024]u8 = undefined; + _ = try std.process.getCwd(&buf); +} +ZIGEOF +)" + +# -- Sync primitives moved -- + +printf "\n${BOLD}-- Sync primitives (Thread.* -> Io.*) --${RESET}\n" + +probe "old_Thread_Mutex" \ + "std.Thread.Mutex moved to Io" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + var m: std.Thread.Mutex = .{}; + _ = &m; +} +ZIGEOF +)" + +probe "new_Io_Mutex" \ + "std.Io.Mutex works" \ + "pass" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + var m: std.Io.Mutex = .{}; + _ = &m; +} +ZIGEOF +)" + +probe "old_Thread_Pool" \ + "std.Thread.Pool removed" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + var pool: std.Thread.Pool = undefined; + _ = &pool; +} +ZIGEOF +)" + +# -- Time / Crypto -- + +printf "\n${BOLD}-- Time / Random --${RESET}\n" + +probe "old_time_Instant" \ + "std.time.Instant removed" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + var i: std.time.Instant = undefined; + _ = &i; +} +ZIGEOF +)" + +probe "new_Io_Timestamp" \ + "std.Io.Timestamp works" \ + "pass" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + var t: std.Io.Timestamp = undefined; + _ = &t; +} +ZIGEOF +)" + +# -- @Type split -- + +printf "\n${BOLD}-- @Type split into 8 builtins --${RESET}\n" + +probe "old_Type_int" \ + "@Type(.{ .int = ...}) removed" \ + "fail" \ + "$(cat <<'ZIGEOF' +test "probe" { + const T = @Type(.{ .int = .{ .signedness = .unsigned, .bits = 10 } }); + _ = T; +} +ZIGEOF +)" + +probe "new_Int_builtin" \ + "@Int(.unsigned, 10) works" \ + "pass" \ + "$(cat <<'ZIGEOF' +test "probe" { + const T = @Int(.unsigned, 10); + _ = T; +} +ZIGEOF +)" + +# -- Containers -- + +printf "\n${BOLD}-- Containers (managed hash maps removed) --${RESET}\n" + +probe "old_AutoArrayHashMap_init" \ + "AutoArrayHashMap.init removed" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + var m = std.AutoArrayHashMap(u32, u32).init(std.testing.allocator); + defer m.deinit(); +} +ZIGEOF +)" + +probe "new_array_hash_map_Auto" \ + "array_hash_map.Auto with .empty works" \ + "pass" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + var m: std.array_hash_map.Auto(u32, u32) = .empty; + defer m.deinit(std.testing.allocator); +} +ZIGEOF +)" + +# -- Carry-over from 0.15 -- + +printf "\n${BOLD}-- Carry-over from 0.15 (still broken in 0.16) --${RESET}\n" + +probe "old_usingnamespace" \ + "usingnamespace still removed" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +const Mixin = struct { pub fn hello() void {} }; +const Foo = struct { pub usingnamespace Mixin; }; +test "probe" { Foo.hello(); } +ZIGEOF +)" + +probe "old_async_await" \ + "async/await still removed" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +fn asyncFn() !void {} +test "probe" { _ = async asyncFn(); } +ZIGEOF +)" + +probe "old_BoundedArray" \ + "std.BoundedArray still removed" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + var arr = std.BoundedArray(u8, 64){}; + _ = &arr; +} +ZIGEOF +)" + +probe "old_division_signed" \ + "Signed / on runtime ints still rejected" \ + "fail" \ + "$(cat <<'ZIGEOF' +test "probe" { + var a: i32 = 10; + var b: i32 = 3; + _ = &a; _ = &b; + const result = a / b; + _ = result; +} +ZIGEOF +)" + +probe "new_divTrunc" \ + "@divTrunc still works" \ + "pass" \ + "$(cat <<'ZIGEOF' +test "probe" { + var a: i32 = 10; + var b: i32 = 3; + _ = &a; _ = &b; + const r = @divTrunc(a, b); + _ = r; +} +ZIGEOF +)" + +probe "old_mem_tokenize" \ + "std.mem.tokenize still renamed" \ + "fail" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + var it = std.mem.tokenize(u8, "hello world", " "); + _ = it.next(); +} +ZIGEOF +)" + +probe "new_mem_tokenizeAny" \ + "std.mem.tokenizeAny still works" \ + "pass" \ + "$(cat <<'ZIGEOF' +const std = @import("std"); +test "probe" { + var it = std.mem.tokenizeAny(u8, "hello world", " "); + _ = it.next(); +} +ZIGEOF +)" + +# -- Summary -- + +total=$((PASS_COUNT + FAIL_COUNT)) +echo "" +printf "${BOLD}Summary${RESET}: %d probes, " "$total" +printf "${GREEN}%d confirmed${RESET}, " "$PASS_COUNT" +if [[ $FAIL_COUNT -gt 0 ]]; then + printf "${RED}%d surprises${RESET}" "$FAIL_COUNT" +else + printf "0 surprises" +fi +echo "" + +if [[ $FAIL_COUNT -gt 0 ]]; then + echo "" + echo "Surprises indicate docs/ZIG_BREAKING_CHANGES-0.16.md needs updating." + exit 1 +fi diff --git a/plugins/zig-claude-kit/scripts/zig-knowledge-eval.py b/plugins/zig-claude-kit/scripts/zig-knowledge-eval.py index bfdc872..d0ce88a 100755 --- a/plugins/zig-claude-kit/scripts/zig-knowledge-eval.py +++ b/plugins/zig-claude-kit/scripts/zig-knowledge-eval.py @@ -4,14 +4,21 @@ # dependencies = ["anthropic"] # /// """ -Evaluate Claude models' Zig 0.15.x knowledge. +Evaluate Claude models' Zig knowledge. Sends prompts to the Claude API with no project context, extracts generated Zig code, and compile-tests it to measure how many -patterns each model gets right vs. wrong. +patterns each model gets right vs. wrong on the locally installed +Zig. + +The prompts are language-version-agnostic ("write hello world" etc.), +but the "right answer" differs between 0.15.x and 0.16. Use --target +to label probe output directories; the compile result against the +installed compiler is what actually matters. Usage: uv run scripts/zig-knowledge-eval.py + uv run scripts/zig-knowledge-eval.py --target 0.16 uv run scripts/zig-knowledge-eval.py --models claude-sonnet-4-6 uv run scripts/zig-knowledge-eval.py --skip-compile """ @@ -146,7 +153,7 @@ def query_model( def main(): parser = argparse.ArgumentParser( - description="Evaluate Claude models' Zig 0.15.x knowledge", + description="Evaluate Claude models' Zig knowledge", ) parser.add_argument( "--models", @@ -154,11 +161,17 @@ def main(): default=DEFAULT_MODELS, help="Models to evaluate (default: %(default)s)", ) + parser.add_argument( + "--target", + choices=["0.15", "0.16"], + default="0.16", + help="Zig version label for output dir (default: %(default)s)", + ) parser.add_argument( "--output-dir", type=Path, - default=Path("probes"), - help="Output directory (default: probes/)", + default=None, + help="Output directory (default: probes//)", ) parser.add_argument( "--skip-compile", @@ -167,6 +180,9 @@ def main(): ) args = parser.parse_args() + if args.output_dir is None: + args.output_dir = Path("probes") / args.target + client = anthropic.Anthropic() script_dir = Path(__file__).parent test_script = script_dir / "zig-knowledge-test.sh" diff --git a/plugins/zig-claude-kit/skills/zig-check/SKILL.md b/plugins/zig-claude-kit/skills/zig-check/SKILL.md index 632aa66..e5c82be 100644 --- a/plugins/zig-claude-kit/skills/zig-check/SKILL.md +++ b/plugins/zig-claude-kit/skills/zig-check/SKILL.md @@ -1,23 +1,30 @@ --- description: > - Audit Zig source files for Zig 0.15.x mistakes -- checks for + Audit Zig source files for outdated patterns. Auto-detects whether + the project targets 0.15.x or 0.16 and applies the matching ruleset: removed APIs (getStdOut, usingnamespace, BoundedArray, async), - missing flush, wrong ArrayList usage, ambiguous format strings, - signed division, and renamed stdlib functions. + wrong I/O / fs / process / Thread patterns, missing flush, + ArrayList usage, ambiguous format strings, signed division, and + renamed stdlib functions. disable-model-invocation: true argument-hint: "[file]" --- # /zig-check [file] -Audit Zig source files for common Zig 0.15.x mistakes. If a file -path is given, check that file. Otherwise check all `src/*.zig` -files that were modified in the current git diff (staged and -unstaged). +Audit Zig source files for outdated APIs. If a file path is given, +check that file. Otherwise check all `src/*.zig` files that were +modified in the current git diff (staged and unstaged). ## Procedure -### 1. Determine target files +### 1. Detect the project's Zig version + +Run `${CLAUDE_PLUGIN_ROOT}/scripts/detect-zig-version.sh` from the +project root. It prints `0.15` or `0.16`. Apply the matching rules +below. + +### 2. Determine target files If argument provided: - Check only that file @@ -25,65 +32,153 @@ If argument provided: If no argument: - Run `git diff --name-only` and `git diff --cached --name-only` - Filter to `src/*.zig` files -- If no modified Zig files, report "No modified Zig files to - check" and exit +- If no modified Zig files, report "No modified Zig files to check" + and exit -### 2. Read each target file +### 3. Read each target file Use the Read tool to read the full contents of each file. -### 3. Check for violations +### 4. Check for violations + +Search each file for patterns. Report each with file path, line +number, and the specific issue. -Search each file for these patterns. Report each violation with -file path, line number, and the specific issue. +--- + +## Rules That Apply to Both Versions -#### Critical (must fix): +These corrections apply regardless of detected version. -1. **Deleted API usage:** - - `std.io.getStdOut` or `std.io.getStdErr` -- use buffered - writer pattern +1. **Removed language features:** - `usingnamespace` -- removed from language - - `async` / `await` -- removed from language + - `async` / `await` keywords -- removed from language - `std.BoundedArray` -- use `ArrayListUnmanaged.initBuffer` - `std.json.Parser` -- use `std.json.parseFromSlice` -2. **Renamed API usage:** - - `std.mem.tokenize(` without `Any`/`Scalar`/`Sequence` -- - use `tokenizeAny`, `tokenizeScalar`, or - `tokenizeSequence` - - `std.process.args()` without `Alloc` -- use - `std.process.argsAlloc(allocator)` +2. **Renamed stdlib functions:** + - `std.mem.tokenize(` without `Any`/`Scalar`/`Sequence` -- use + `tokenizeAny`, `tokenizeScalar`, or `tokenizeSequence` -3. **takeDelimiterExclusive in while loops:** - - `while` loop calling `takeDelimiterExclusive` -- use - `appendRemaining` instead (hangs on stdin) +3. **Ambiguous format strings:** + - `"{}"` in print/format calls -- must use `{s}`, `{d}`, `{any}`, + etc. Use `{f}` to call custom format methods. + +4. **Signed division without builtins:** + - `/` or `%` on runtime signed integer variables -- use + `@divTrunc`, `@divFloor`, `@divExact`, `@rem`, `@mod` -4. **Missing writer flush:** - - `stdout_writer` or `stderr_writer` created without a - corresponding `defer ... flush() catch {}` +5. **Old for-loop index syntax:** + - `for (items) |item, i|` without explicit index range -- use + `for (items, 0..) |item, i|` -5. **ArrayList without allocator:** - - `.append(`, `.appendSlice(`, `.deinit()` etc. without - allocator as first argument (for `ArrayListUnmanaged`) +6. **ArrayList without allocator:** + - `.append(`, `.appendSlice(`, `.deinit()` without allocator + as first argument (for ArrayListUnmanaged / 0.16 ArrayList) -6. **Ambiguous format strings:** - - `"{}"` in print/format calls -- must use `{s}`, `{d}`, - `{any}`, etc. +7. **Old format method signature:** + - `pub fn format(self: T, comptime fmt: ...)` -- new signature + is `pub fn format(self: T, writer: *std.Io.Writer) ...` -7. **Signed division without builtins:** - - `/` or `%` on runtime signed integer variables -- use - `@divTrunc`, `@divFloor`, `@divExact`, `@rem`, `@mod` +--- + +## Rules When Project Targets 0.15.x + +8. **Deleted APIs (0.15):** + - `std.io.getStdOut` / `std.io.getStdErr` -- use buffered writer + pattern via `std.fs.File.stdout()` + +9. **takeDelimiterExclusive in while loops:** + - `while` loop calling `takeDelimiterExclusive` -- use + `appendRemaining` instead (hangs on stdin) -8. **Old for-loop index syntax:** - - `for (items) |item, i|` without explicit index range -- - use `for (items, 0..) |item, i|` +10. **Missing writer flush:** + - `stdout_writer` / `stderr_writer` created without a + corresponding `defer ... flush() catch {}` -### 4. Report results +11. **Old process args:** + - `std.process.args()` returning iterator (without `Alloc`) -- + use `std.process.argsAlloc(allocator)` + +--- + +## Rules When Project Targets 0.16 + +8. **Deleted / moved APIs (0.16 on top of 0.15):** + - `std.io.getStdOut` / `std.io.getStdErr` -- doubly wrong; use + `std.Io.File.stdout()` and `writerStreaming(io, &buf)` + - `std.fs.File.stdout()` -- moved to `std.Io.File.stdout()` + - `std.fs.File` / `std.fs.Dir` -- moved to `std.Io.File` / + `std.Io.Dir` + - `std.fs.cwd()` -- moved to `std.Io.Dir.cwd()` + - `std.os.environ` -- gone; use `init.environ_map` + - `std.process.argsAlloc` / `argsFree` -- gone; use + `init.minimal.args.toSlice(allocator)` + - `std.process.getCwd` / `getCwdAlloc` -- renamed to + `currentPath` / `currentPathAlloc` (and take `io`) + - `std.process.Child.init(...).spawn()` -- use + `std.process.spawn(io, .{...})` + - `std.posix.PROT.READ | std.posix.PROT.WRITE` -- use + `.{ .READ = true, .WRITE = true }` + - `std.posix.mlock` family -- moved to `std.process.lockMemory` + - `std.Thread.Mutex` / `Condition` / `Semaphore` / `WaitGroup` + / `ResetEvent` / `Futex` / `RwLock` -- moved to `std.Io.*` + - `std.Thread.Pool` -- gone; use `std.Io.async` / + `std.Io.Group.async` + - `std.crypto.random.bytes` -- use `io.random(&buf)` + - `std.time.Instant` / `Timer` / `timestamp` -- collapsed into + `std.Io.Timestamp` + - `@Type(.{ .int = ... })` -- use `@Int(.unsigned, N)` (and the + seven other builtins: `@EnumLiteral`, `@Tuple`, `@Pointer`, + `@Fn`, `@Struct`, `@Union`, `@Enum`) + - `@cImport({ @cInclude(...) })` -- deprecated; use + `b.addTranslateC(...)` in `build.zig` + +9. **Renamed stdlib functions (0.16):** + - `std.mem.indexOf` -- use `std.mem.find` + - `std.mem.indexOfScalar` -- use `std.mem.findScalar` + - `std.mem.indexOfPos` -- use `std.mem.findPos` + - `std.mem.indexOfAny` -- use `std.mem.findAny` + - `std.mem.lastIndexOf` -- use `std.mem.findLast` + - `std.mem.lastIndexOfScalar` -- use `std.mem.findScalarLast` + (note: `Last` comes after `Scalar`) + - `Dir.makeDir(name)` -- use `Dir.createDir(io, name)` + - `Dir.makePath(path)` -- use `Dir.createDirPath(io, path)` + - `File.chmod` / `Dir.chmod` -- use `setPermissions(io, ...)` + - `File.setEndPos` / `getEndPos` -- use `setLength(io)` / + `length(io)` + - `File.write` / `writeAll` / `read` -- use `writeStreaming(io)` / + `writeStreamingAll(io)` / `readStreaming(io)` + +10. **Missing `io` parameter:** + - `file.close()` -- now requires `file.close(io)` + - Other file/dir methods missing `io` as first arg + +11. **`main` signature missing init:** + - `pub fn main() !void` where the function reads args/env -- + should be `pub fn main(init: std.process.Init) !void` + - Reading `std.os.environ` or calling `std.process.argsAlloc` + inside such a `main` is doubly wrong + +12. **Error name renames (0.16):** + - `error.RenameAcrossMountPoints` / `NotSameFileSystem` -- + use `error.CrossDevice` + - `error.SharingViolation` -- use `error.FileBusy` + - `error.FileTooBig` from `readFileAlloc` -- use + `error.StreamTooLong` + +13. **Managed hash maps:** + - `std.AutoArrayHashMap(K, V).init(allocator)` -- use + `std.array_hash_map.Auto(K, V) = .empty` + - Same for `StringArrayHashMap` -> `array_hash_map.String` + - Same for `ArrayHashMap` -> `array_hash_map.Custom` + +### 5. Report results Format output as: ``` -## /zig-check Results +## /zig-check Results (Zig X.Y target) ### @@ -101,24 +196,19 @@ Summary: X critical across Z files If no issues found in any file: ``` -## /zig-check Results +## /zig-check Results (Zig X.Y target) -All files pass. No Zig 0.15.x issues found. +All files pass. No issues found. ``` -### 5. Suggest fixes - -For each critical issue, include a one-line fix suggestion: - -- "Replace `std.io.getStdOut()` with buffered writer pattern - (see zig-patterns skill)" -- "Replace `while (reader.takeDelimiterExclusive(...))` with - `reader.appendRemaining()`" -- "Add `defer stdout.flush() catch {};` after writer creation" -- "Change `list.append(val)` to `list.append(allocator, val)`" -- "Change `"{}"` to `"{s}"` (or appropriate specifier)" -- "Replace `a / b` with `@divTrunc(a, b)` for signed integers" -- "Replace `std.mem.tokenize` with `std.mem.tokenizeAny`" -- "Replace `std.process.args()` with `std.process.argsAlloc`" -- "Change `for (items) |x, i|` to `for (items, 0..) |x, i|`" -- "Replace `std.json.Parser` with `std.json.parseFromSlice`" +Always include the detected target version in the header so the +user can confirm the right ruleset was applied. + +### 6. Suggest fixes + +For each critical issue, include a one-line fix suggestion citing +the right replacement API. Reference the version-specific breaking +changes doc when relevant: + +- 0.15: `${CLAUDE_PLUGIN_ROOT}/docs/ZIG_BREAKING_CHANGES-0.15.md` +- 0.16: `${CLAUDE_PLUGIN_ROOT}/docs/ZIG_BREAKING_CHANGES-0.16.md` diff --git a/plugins/zig-claude-kit/skills/zig-init/SKILL.md b/plugins/zig-claude-kit/skills/zig-init/SKILL.md index 33165db..c23021a 100644 --- a/plugins/zig-claude-kit/skills/zig-init/SKILL.md +++ b/plugins/zig-claude-kit/skills/zig-init/SKILL.md @@ -1,38 +1,51 @@ --- description: > - Add Zig 0.15.x training corrections to this project's - CLAUDE.md. Run this in any Zig project to fix Claude's - outdated patterns for I/O, ArrayList, format strings, - build.zig, BoundedArray, and usingnamespace. + Add Zig training corrections to this project's CLAUDE.md. + Auto-detects whether the project targets Zig 0.15.x or 0.16 + (from build.zig.zon's minimum_zig_version, falling back to + `zig version`) and injects the matching corrections. --- # /zig-init -Add Zig 0.15.x corrections to this project's CLAUDE.md. +Add Zig training corrections to this project's CLAUDE.md. ## Procedure -### 1. Read the corrections fragment +### 1. Detect the project's Zig version -Read the file at -`${CLAUDE_PLUGIN_ROOT}/docs/claude-md-fragment.md`. -This contains the Zig 0.15.x training corrections -formatted as a CLAUDE.md section. +Run `${CLAUDE_PLUGIN_ROOT}/scripts/detect-zig-version.sh` from +the project root. It prints either `0.15` or `0.16`. The script +defaults to `0.16` when nothing is detectable. -### 2. Check current CLAUDE.md +### 2. Read the matching corrections fragment -- If no `CLAUDE.md` exists in the project root, create - one with just a `# CLAUDE.md` header followed by the - fragment content. +Use the detected version to pick the fragment: + +- `0.15` -> `${CLAUDE_PLUGIN_ROOT}/docs/claude-md-fragment-0.15.md` +- `0.16` -> `${CLAUDE_PLUGIN_ROOT}/docs/claude-md-fragment-0.16.md` + +### 3. Check current CLAUDE.md + +- If no `CLAUDE.md` exists in the project root, create one with + just a `# CLAUDE.md` header followed by the fragment content. - If `CLAUDE.md` exists, check if it already contains - "Writergate". If so, report "Zig corrections already - present" and stop. -- If `CLAUDE.md` exists but lacks the corrections, - append the fragment content to the end of the file. + "Writergate". If so, report "Zig corrections already present" + and stop. (Note: this matches either version's fragment; the + user can manually swap fragments if their project's target + version changed.) +- If `CLAUDE.md` exists but lacks the corrections, append the + fragment content to the end of the file. -### 3. Report result +### 4. Report result -Tell the user what you did: -- "Created CLAUDE.md with Zig 0.15.x corrections" +Tell the user what you did and which version was detected: + +- "Created CLAUDE.md with Zig 0.16.x corrections" - "Added Zig 0.15.x corrections to existing CLAUDE.md" - "Zig corrections already present in CLAUDE.md" + +If detection fell back to the default (0.16) because no +`build.zig.zon` was present and `zig` was not installed, +mention that the user can re-run `/zig-init` once they've set +`minimum_zig_version` in `build.zig.zon`. diff --git a/plugins/zig-claude-kit/skills/zig-patterns/SKILL.md b/plugins/zig-claude-kit/skills/zig-patterns/SKILL.md index 009ce91..504d7c5 100644 --- a/plugins/zig-claude-kit/skills/zig-patterns/SKILL.md +++ b/plugins/zig-claude-kit/skills/zig-patterns/SKILL.md @@ -1,86 +1,259 @@ --- description: > - Correct Zig 0.15.x patterns for I/O, ArrayList, format strings, - and build.zig. Use when writing or reviewing any Zig code -- - Claude's training data is outdated for these APIs. + Correct Zig patterns for I/O, ArrayList, format strings, and + build.zig. Auto-detects whether the project targets 0.15.x or + 0.16. Use when writing or reviewing any Zig code -- Claude's + training data is outdated for these APIs. --- -# Zig 0.15.x Patterns -- Quick Reference +# Zig Patterns -- Quick Reference -Your Zig training is outdated. These are the CORRECT patterns. -For full details with error diagnostics, read -`${CLAUDE_PLUGIN_ROOT}/docs/ZIG_BREAKING_CHANGES.md`. +Your Zig training is outdated. Before writing patterns, run +`${CLAUDE_PLUGIN_ROOT}/scripts/detect-zig-version.sh` to learn +whether the current project targets **Zig 0.15.x** or **Zig +0.16**. The two versions have different "right answers" for +I/O, args, environment, and filesystem code. -## I/O: Buffered Writers (Writergate) +For full details with error diagnostics, read the +version-specific reference: + +- 0.15.x: `${CLAUDE_PLUGIN_ROOT}/docs/ZIG_BREAKING_CHANGES-0.15.md` +- 0.16: `${CLAUDE_PLUGIN_ROOT}/docs/ZIG_BREAKING_CHANGES-0.16.md` + +When the user invokes this skill, identify which version the +project targets and show the matching patterns below. If +unsure, default to 0.16 (the current release as of 2026-04-14). + +--- + +## Zig 0.16 Patterns + +### "Juicy Main" + +```zig +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const gpa = init.gpa; + const io = init.io; + const arena = init.arena.allocator(); + _ = gpa; _ = arena; + + try std.Io.File.stdout().writeStreamingAll(io, "Hello!\n"); +} +``` + +`std.process.Init` bundles `gpa`, `io`, `arena`, `environ_map`, +`preopens`, plus `init.minimal.{args,environ}`. + +### Buffered stdout/stderr (0.16) + +```zig +pub fn main(init: std.process.Init) !void { + const io = init.io; + + var stdout_buffer: [4096]u8 = undefined; + var stdout_writer = + std.Io.File.stdout().writerStreaming(io, &stdout_buffer); + const stdout = &stdout_writer.interface; + defer stdout.flush() catch {}; + + try stdout.print("Hello, {s}\n", .{name}); +} +``` + +`std.io.getStdOut()` and `std.fs.File.stdout()` are both wrong +in 0.16. Use `std.Io.File.stdout()` and pass `io` to +`writerStreaming`. Use `writerStreaming` (not `writer`) so +shell `>>` redirects work on macOS. + +### Args and environment (no longer global) + +```zig +// Slice via Juicy Main +const args = try init.minimal.args.toSlice(init.arena.allocator()); +for (args) |arg| std.log.info("{s}", .{arg}); + +// Iterator via Init.Minimal +var it = init.minimal.args.iterate(); +while (it.next()) |arg| {} + +// Env via Juicy Main +const home = init.environ_map.get("HOME"); +``` + +`std.os.environ`, `std.process.argsAlloc`, and +`std.process.argsFree` are gone. + +### Filesystem (moved to std.Io) + +```zig +const dir = std.Io.Dir.cwd(); +const file = try dir.openFile(io, "x.txt", .{}); +defer file.close(io); + +try dir.createDir(io, "newdir"); +try dir.createDirPath(io, "deep/nested/path"); +``` + +`std.fs.File` -> `std.Io.File`, `std.fs.Dir` -> `std.Io.Dir`, +every blocking method takes `io`. + +### `std.mem.indexOf*` renamed to `find*` + +```zig +const i = std.mem.find(u8, haystack, needle); +const j = std.mem.findScalar(u8, s, ' '); +const k = std.mem.findLast(u8, haystack, needle); +const l = std.mem.findScalarLast(u8, s, '/'); // Last after Scalar +``` + +### Process / spawning + +```zig +const child = try std.process.spawn(io, .{ + .argv = argv, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .pipe, +}); + +const cwd = try std.process.currentPathAlloc(io, allocator); +defer allocator.free(cwd); +``` + +### Sync primitives (moved to Io) + +```zig +var mutex: std.Io.Mutex = .{}; +var cond: std.Io.Condition = .{}; +var group: std.Io.Group = .init; +errdefer group.cancel(io); +group.async(io, task, .{io}); +try group.await(io); +``` + +`std.Thread.Pool` is gone. Lock-free atomics still work without +`Io`. + +### Tests get a free `Io` + +```zig +test "io test" { + const io = std.testing.io; + const file = try std.Io.Dir.cwd().openFile(io, "x", .{}); + defer file.close(io); +} +``` + +### Hash maps + +```zig +var map: std.array_hash_map.Auto(K, V) = .empty; +defer map.deinit(gpa); +try map.put(gpa, k, v); +``` + +### `ArrayList` (carried from 0.15) + +```zig +var list: std.ArrayList(u8) = .empty; +defer list.deinit(gpa); +try list.append(gpa, 'a'); +``` + +### Builtins replacing `@Type` + +```zig +const T = @Int(.unsigned, 10); +const Pair = @Tuple(&.{ u32, [2]f64 }); +const tag = @EnumLiteral(); +``` + +No `@Float`, `@Array`, `@Optional`, `@ErrorUnion` -- write the +literal type. + +--- + +## Zig 0.15.x Patterns + +### I/O: Buffered Writers (Writergate, 0.15 edition) ```zig -// main() setup var stdout_buffer: [4096]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &stdout_writer.interface; defer stdout.flush() catch {}; -// stdin reader var stdin_buffer: [4096]u8 = undefined; var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer); const stdin = &stdin_reader.interface; - -// File reader -var file_buffer: [4096]u8 = undefined; -var file_reader = file.reader(&file_buffer); -const reader = &file_reader.interface; ``` WRONG: `std.io.getStdOut()`, `std.io.getStdErr()` -- deleted. ALWAYS `defer flush() catch {}` before buffer goes out of scope. -Use `writerStreaming()` instead of `writer()` when output -must respect O_APPEND (e.g. shell `>>` redirects). The -positional `writer()` uses `pwritev` at offset 0, ignoring -O_APPEND on macOS. +Use `writerStreaming()` instead of `writer()` when output must +respect O_APPEND (e.g. shell `>>` redirects). -## Reading Input: appendRemaining +### Reading Input: appendRemaining ```zig -// Read all input at once var content_list = std.ArrayListUnmanaged(u8){}; defer content_list.deinit(allocator); reader.appendRemaining( allocator, &content_list, .{ .max = 1 << 30 }, -) catch |err| { - return err; -}; +) catch |err| return err; const content = content_list.items; ``` WRONG: `while (reader.takeDelimiterExclusive('\n'))` in a loop. -That pattern hangs on stdin in unit tests. -## ArrayList: Allocator on Every Call +### ArrayList: Allocator on Every Call ```zig var list = std.ArrayListUnmanaged(u8){}; defer list.deinit(allocator); try list.append(allocator, value); -try list.appendSlice(allocator, slice); ``` -WRONG: `list.append(value)` without allocator. +### Process Args: Owned Slice + +```zig +const args = try std.process.argsAlloc(allocator); +defer std.process.argsFree(allocator, args); +for (args[1..]) |arg| {} +``` -## Division: Signed Integers +### Build System: root_module ```zig -// WRONG (your training) -const result = a / b; // runtime signed integers +const exe = b.addExecutable(.{ + .name = "app", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), +}); +``` + +--- + +## Shared Across Both Versions + +These corrections apply to both 0.15.x and 0.16. + +### Division: Signed Integers -// RIGHT (Zig 0.15.x) +```zig const result = @divTrunc(a, b); // or @divFloor, @divExact const remainder = @rem(a, b); // or @mod ``` WRONG: `/` and `%` on runtime signed integers -- compile error. -## Format Strings: Explicit Specifiers +### Format Strings: Explicit Specifiers ```zig try writer.print("{s}: {d} bytes\n", .{ name, count }); @@ -89,81 +262,58 @@ try writer.print("{s}: {d} bytes\n", .{ name, count }); WRONG: `"{}"` -- must use `{s}`, `{d}`, `{any}`, etc. Use `{f}` to call custom format methods. -## Tokenization: Renamed Functions +### Tokenization: Renamed Functions ```zig -// WRONG (your training) -var it = std.mem.tokenize(u8, text, " "); - -// RIGHT (Zig 0.15.x) -var it = std.mem.tokenizeAny(u8, text, " "); // multi-char delimiters -var it = std.mem.tokenizeScalar(u8, text, ' '); // single char -var it = std.mem.tokenizeSequence(u8, text, "=="); // exact sequence +var it = std.mem.tokenizeAny(u8, text, " "); // multi-char +var it = std.mem.tokenizeScalar(u8, text, ' '); // single +var it = std.mem.tokenizeSequence(u8, text, "=="); // exact ``` -## Process Args: Owned Slice +### For Loops: Explicit Index Range ```zig -// WRONG (your training) -var args = std.process.args(); -while (args.next()) |arg| {} - -// RIGHT (Zig 0.15.x) -const args = try std.process.argsAlloc(allocator); -defer std.process.argsFree(allocator, args); -for (args[1..]) |arg| {} // skip program name +for (items, 0..) |item, i| {} +for (a, b, c) |x, y, z| {} +for (names, ages, 0..) |n, a, i| {} ``` -## For Loops: Explicit Index Range +### JSON: parseFromSlice ```zig -// WRONG (your training) -for (items) |item, i| {} - -// RIGHT (Zig 0.15.x) -for (items, 0..) |item, i| {} // explicit index -for (a, b, c) |x, y, z| {} // multiple arrays -for (names, ages, 0..) |n, a, i| {} // multi-array with index -``` - -## JSON: Complete Redesign - -```zig -// WRONG (your training) -var parser = std.json.Parser.init(allocator, false); -defer parser.deinit(); -var tree = try parser.parse(json_text); - -// RIGHT (Zig 0.15.x) const parsed = try std.json.parseFromSlice(T, allocator, text, .{}); defer parsed.deinit(); const value = parsed.value; ``` -## Build System: root_module +### Testing: Parameter Order ```zig -const exe = b.addExecutable(.{ - .name = "app", - .root_module = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }), -}); +// Expected FIRST, actual SECOND +try std.testing.expectEqual(expected, actual); +try std.testing.expectEqualSlices(u8, expected, actual); +try std.testing.expectEqualStrings(expected, actual); ``` -WRONG: `.root_source_file` at top level of addExecutable. +### Removed Language Features + +- `usingnamespace` -- gone. Use zero-bit fields with + `@fieldParentPtr`. +- `async` / `await` keywords -- gone. In 0.16 use `std.Io.async`. +- `std.BoundedArray` -- gone. Use + `ArrayListUnmanaged.initBuffer`. -## Testing: Parameter Order +### Format Method Signature ```zig -// Expected FIRST, actual SECOND -try std.testing.expectEqual(expected, actual); -try std.testing.expectEqualSlices(u8, expected, actual); -try std.testing.expectEqualStrings(expected, actual); +pub fn format(self: T, writer: *std.Io.Writer) + std.Io.Writer.Error!void { + try writer.print("{d}", .{self.x}); +} ``` +Use `{f}` in format strings to call format methods, not `{}`. + ## Shell Rules Run commands exactly as shown. Do NOT append `2>&1`,