diff --git a/CLAUDE.md b/CLAUDE.md index 8767a67c..295ca830 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -492,6 +492,23 @@ Safe workflow: 2. Only run `zig build test -Dupdate-goldens` when you've intentionally changed output (new feature, new test, changed formatting). 3. After regenerating, review the diff (`git diff examples/expected/ issues/expected/`) to confirm no error messages or empty output were captured. +**Scope a regen to specific examples with `-Dname`.** A *full* `-Dupdate-goldens` +re-runs and rewrites all ~690 snapshots, so a single flaky/host-divergent example +(AOT links, cross-arch `target` examples, anything that intermittently fails) can +silently clobber a good snapshot. To capture just the example(s) you added, pass +their full repo-relative `.sx` path(s), comma-separated — this rewrites ONLY those +and touches nothing else: + +```sh +zig build test -Dname=examples/0625-comptime-weld-struct-field.sx -Dupdate-goldens +zig build test -Dname=examples/0625-foo.sx,examples/0626-bar.sx # verify just these +``` + +`-Dname` also busts the test-run cache (the corpus enumerates `.sx`/`expected/` +files at RUNTIME, so editing a snapshot alone does NOT force a re-run — a plain +`zig build test` may be served a cached result). Changing `-Dname` — or any +compiler source — forces a fresh run. + ### Adding a new language feature There is no monolithic smoke file — each feature is its own focused example. diff --git a/build.zig b/build.zig index a43d7938..dfa95ca0 100644 --- a/build.zig +++ b/build.zig @@ -218,6 +218,20 @@ pub fn build(b: *std.Build) void { "Regenerate example/issue snapshots instead of verifying them (use with `zig build test`)", ) orelse false; corpus_opts.addOption(bool, "update_goldens", update_goldens); + // `zig build test -Dname=examples/0213-foo.sx[,examples/0214-bar.sx]` restricts + // the corpus runner to ONLY the named example(s) — full repo-relative `.sx` + // paths, comma-separated. Empty = run every example. Use it to verify or + // regenerate (-Dupdate-goldens) a specific example without re-running (or + // clobbering the snapshots of) the rest of the corpus. Because the value is + // baked into the corpus options module, changing it also busts the cached + // test-run result (the runner enumerates .sx/expected files at RUNTIME, so a + // bare snapshot edit alone would otherwise be served from cache). + const name_filter = b.option( + []const u8, + "name", + "Run only the named example(s): comma-separated repo-relative .sx paths (e.g. examples/0213-foo.sx)", + ) orelse ""; + corpus_opts.addOption([]const u8, "name", name_filter); mod.addOptions("corpus_paths", corpus_opts); const mod_tests = b.addTest(.{ diff --git a/src/corpus_run.test.zig b/src/corpus_run.test.zig index 103284b1..b03a1410 100644 --- a/src/corpus_run.test.zig +++ b/src/corpus_run.test.zig @@ -160,6 +160,21 @@ fn readOptional(io: std.Io, gpa: std.mem.Allocator, abs_path: []const u8) ?[]u8 return std.Io.Dir.readFileAlloc(.cwd(), io, abs_path, gpa, .limited(MAX_OUTPUT)) catch null; } +/// True when `rel_path` (a repo-relative `.sx` path like `examples/0213-foo.sx`) +/// matches the `-Dname` filter — a comma-separated list of full `.sx` paths. +/// Each entry is whitespace-trimmed and a leading `./` is ignored, so both +/// `examples/0213-foo.sx` and `./examples/0213-foo.sx` match. +fn nameMatchesFilter(filter: []const u8, rel_path: []const u8) bool { + var it = std.mem.splitScalar(u8, filter, ','); + while (it.next()) |raw| { + var entry = std.mem.trim(u8, raw, " \t\r\n"); + if (std.mem.startsWith(u8, entry, "./")) entry = entry[2..]; + if (entry.len == 0) continue; + if (std.mem.eql(u8, entry, rel_path)) return true; + } + return false; +} + /// Per-example build/run directives, parsed from an optional `.build` /// JSON sidecar (replaces the old standalone `.aot` marker). Output snapshots /// (.exit/.stdout/.stderr/.ir) stay separate — they are regenerated data, not @@ -281,6 +296,14 @@ fn sweepRoot( _ = work_state.reset(.retain_capacity); const a = work_state.allocator(); + // `-Dname=` filter: when set, run ONLY the named example(s) (full + // repo-relative `.sx` paths, comma-separated). A non-matching example is + // dropped silently — not counted as ran or skipped. + if (corpus_paths.name.len > 0) { + const this_rel = try std.fmt.allocPrint(a, "{s}/{s}.sx", .{ root_base, name }); + if (!nameMatchesFilter(corpus_paths.name, this_rel)) continue; + } + const sx_abs = try std.fs.path.join(a, &.{ root_dir, try std.fmt.allocPrint(a, "{s}.sx", .{name}) }); std.Io.Dir.access(.cwd(), io, sx_abs, .{}) catch { // marker without source skipped += 1;