Merge branch 'main' into fix-build-update

This commit is contained in:
Chris Boesch
2026-06-03 17:39:58 +02:00
7 changed files with 109 additions and 82 deletions

View File

@@ -111,7 +111,7 @@ directory.
Every Ziglings exercise contains mistakes on purpose. Every Ziglings exercise contains mistakes on purpose.
To keep our automated tests happy, each exercise also To keep our automated tests happy, each exercise also
has a patch in `patches/healed` that “heals” it. has a patch in `patches/patches` that “heals” it.
When you change an exercise, you will usually need to update When you change an exercise, you will usually need to update
its patch too. Thats where our little helper Gollum comes in: its patch too. Thats where our little helper Gollum comes in:

View File

@@ -30,7 +30,7 @@ comptime {
} }
} }
// Elrond owns the entire Ziglings logic now! // Elrond the Wise owns the entire Ziglings logic now!
// build.zig only builds it and forwards the chosen options as CLI flags. // build.zig only builds it and forwards the chosen options as CLI flags.
// Building just this one Run step keeps the build output readable and lets // Building just this one Run step keeps the build output readable and lets
// Elrond iterate without the configure-phase cache getting in the way. // Elrond iterate without the configure-phase cache getting in the way.
@@ -46,6 +46,7 @@ pub fn build(b: *Build) !void {
const rand = b.option(bool, "random", "Select random exercise"); const rand = b.option(bool, "random", "Select random exercise");
const start = b.option(usize, "s", "Start at exercise"); const start = b.option(usize, "s", "Start at exercise");
const reset = b.option(bool, "reset", "Reset exercise progress"); const reset = b.option(bool, "reset", "Reset exercise progress");
const logo = b.option(bool, "logo", "Display Ziglings logo");
const sep = std.fs.path.sep_str; const sep = std.fs.path.sep_str;
const healed_path = if (override_healed_path) |path| const healed_path = if (override_healed_path) |path|
@@ -57,7 +58,7 @@ pub fn build(b: *Build) !void {
const elrond = b.addExecutable(.{ const elrond = b.addExecutable(.{
.name = "elrond", .name = "elrond",
.root_module = b.createModule(.{ .root_module = b.createModule(.{
.root_source_file = b.path("src/elrond.zig"), .root_source_file = b.path("rivendell/elrond.zig"),
.target = b.graph.host, .target = b.graph.host,
}), }),
}); });
@@ -87,6 +88,8 @@ pub fn build(b: *Build) !void {
run.addArg("--random"); run.addArg("--random");
} else if (start) |s| { } else if (start) |s| {
run.addArg(b.fmt("--start={d}", .{s})); run.addArg(b.fmt("--start={d}", .{s}));
} else if (logo) |_| {
run.addArg("--logo");
} }
const ziglings_step = b.step("ziglings", "Run ziglings"); const ziglings_step = b.step("ziglings", "Run ziglings");

View File

@@ -10,6 +10,8 @@ pub fn main() void {
for (animals) |a| printAnimal(a); for (animals) |a| printAnimal(a);
std.debug.print("done.\n", .{}); std.debug.print("done.\n", .{});
std.debug.print("Answer to everything? {d}\n", .{calculateTheUltimateQuestionOfLife()});
} }
// This function is _supposed_ to print an animal name in parentheses // This function is _supposed_ to print an animal name in parentheses
@@ -35,3 +37,24 @@ fn printAnimal(animal: u8) void {
std.debug.print("Unknown", .{}); std.debug.print("Unknown", .{});
} }
// This function is supposed to calculate the answer to the
// ultimate question of life, the universe, and everything,
// but it needs to be deferred as far in the future as possible,
// in order to gather more data.
//
// When there are multiple defers in a single block, they are executed in reverse order.
// This example might seem silly, but it's important to know when e.g.
// deinitializing containers whose elements need to be deinitialized first.
fn calculateTheUltimateQuestionOfLife() u32 {
var x: u32 = 100;
// Try reordering the statements to get the answer 42
{
defer x = x / 10;
defer x = x + 11;
defer x = x * 2;
}
return x;
}

View File

@@ -36,10 +36,10 @@ pub fn main(init: std.process.Init) !void {
const file = try output_dir.openFile(io, "zigling.txt", .{}); const file = try output_dir.openFile(io, "zigling.txt", .{});
defer file.close(io); defer file.close(io);
// initialize an array of u8 with all letter 'A' // initialize an array of u8 entirely with the letter 'A'
// we need to pick the size of the array, 64 seems like a good number // we need to pick the size of the array, 64 seems like a good number
// fix the initialization below // do you remember the array repetition function?
var content = ['A']*64; var content: ??? = ???('A');
// this should print out : `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA` // this should print out : `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`
std.debug.print("{s}\n", .{content}); std.debug.print("{s}\n", .{content});

View File

@@ -1,6 +1,6 @@
--- exercises/028_defer2.zig 2023-10-03 22:15:22.122241138 +0200 --- exercises/028_defer2.zig 2026-06-02 06:08:12.713672612 +0200
+++ answers/028_defer2.zig 2023-10-05 20:04:06.966098530 +0200 +++ answers/028_defer2.zig 2026-06-02 06:08:43.262234023 +0200
@@ -18,7 +18,7 @@ @@ -20,7 +20,7 @@
fn printAnimal(animal: u8) void { fn printAnimal(animal: u8) void {
std.debug.print("(", .{}); std.debug.print("(", .{});
@@ -9,3 +9,15 @@
if (animal == 'g') { if (animal == 'g') {
std.debug.print("Goat", .{}); std.debug.print("Goat", .{});
@@ -51,9 +51,9 @@
// Try reordering the statements to get the answer 42
{
- defer x = x / 10;
- defer x = x + 11;
defer x = x * 2;
+ defer x = x + 11;
+ defer x = x / 10;
}
return x;

View File

@@ -1,10 +1,10 @@
--- exercises/110_files2.zig 2026-05-04 17:08:38.913033915 +0200 --- exercises/110_files2.zig 2026-05-31 14:54:55.061019159 +0200
+++ answers/110_files2.zig 2026-05-04 17:08:37.112995948 +0200 +++ answers/110_files2.zig 2026-05-31 14:54:42.655691238 +0200
@@ -39,7 +39,7 @@ @@ -39,7 +39,7 @@
// initialize an array of u8 with all letter 'A' // initialize an array of u8 entirely with the letter 'A'
// we need to pick the size of the array, 64 seems like a good number // we need to pick the size of the array, 64 seems like a good number
// fix the initialization below // do you remember the array repetition function?
- var content = ['A']*64; - var content: ??? = ???('A');
+ var content: [64]u8 = @splat('A'); + var content: [64]u8 = @splat('A');
// this should print out : `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA` // this should print out : `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`
std.debug.print("{s}\n", .{content}); std.debug.print("{s}\n", .{content});

View File

@@ -1,4 +1,8 @@
// Elrond: Ziglings' exercise checker and guide. //
// "You have a long way to go,
// and the path is not easy."
// Elrond, The Hobbit or The Fellowship of the Ring
//
// //
// In the reworked Zig build system (configurer/maker split) a build Step no // In the reworked Zig build system (configurer/maker split) a build Step no
// longer carries a `makeFn`, and the configure phase is cached and not re-run // longer carries a `makeFn`, and the configure phase is cached and not re-run
@@ -33,10 +37,9 @@ pub const logo =
\\ \\
; ;
// How Elrond was invoked. // How Elrond was called.
const Mode = enum { const Mode = enum {
// `zig build`: iterate from after the last solved exercise to the first // `zig build`: iterate from after the last solved exercise to the first unsolved one (or the end).
// unsolved one (or the end).
normal, normal,
// `zig build -Dn=n`: check exactly one exercise. // `zig build -Dn=n`: check exactly one exercise.
named, named,
@@ -57,8 +60,7 @@ pub const Exercise = struct {
// main_file must have the format key_name.zig. // main_file must have the format key_name.zig.
main_file: []const u8, main_file: []const u8,
// Desired output. A program passes if its output, excluding trailing // Desired output. A program passes if its output, excluding trailing whitespace, equals this string.
// whitespace, equals this string.
output: []const u8, output: []const u8,
// Optional hint shown if the program does not succeed. // Optional hint shown if the program does not succeed.
@@ -86,11 +88,9 @@ pub const Exercise = struct {
return std.fs.path.stem(self.main_file); return std.fs.path.stem(self.main_file);
} }
// Key of the main file: the string before the '_' with zero padding // Key of the main file: the string before the '_' with zero padding removed. "001_hello.zig" -> "1".
// removed. "001_hello.zig" -> "1".
pub fn key(self: Exercise) []const u8 { pub fn key(self: Exercise) []const u8 {
const end_index = std.mem.indexOfScalar(u8, self.main_file, '_') orelse const end_index = std.mem.indexOfScalar(u8, self.main_file, '_') orelse unreachable;
unreachable;
var start_index: usize = 0; var start_index: usize = 0;
while (self.main_file[start_index] == '0') start_index += 1; while (self.main_file[start_index] == '0') start_index += 1;
return self.main_file[start_index..end_index]; return self.main_file[start_index..end_index];
@@ -102,11 +102,13 @@ pub const Exercise = struct {
} }
}; };
// Ansi colors.
var use_color_escapes = false; var use_color_escapes = false;
var red_text: []const u8 = ""; var red_text: []const u8 = "";
var red_bold_text: []const u8 = ""; var red_bold_text: []const u8 = "";
var red_dim_text: []const u8 = ""; var red_dim_text: []const u8 = "";
var green_text: []const u8 = ""; var green_text: []const u8 = "";
var yellow_text: []const u8 = "";
var bold_text: []const u8 = ""; var bold_text: []const u8 = "";
var reset_text: []const u8 = ""; var reset_text: []const u8 = "";
@@ -126,6 +128,7 @@ fn setupColors(io: std.Io) void {
red_bold_text = "\x1b[31;1m"; red_bold_text = "\x1b[31;1m";
red_dim_text = "\x1b[31;2m"; red_dim_text = "\x1b[31;2m";
green_text = "\x1b[32m"; green_text = "\x1b[32m";
yellow_text = "\x1b[33m";
bold_text = "\x1b[1m"; bold_text = "\x1b[1m";
reset_text = "\x1b[0m"; reset_text = "\x1b[0m";
} }
@@ -133,9 +136,9 @@ fn setupColors(io: std.Io) void {
pub fn main(init: std.process.Init) !void { pub fn main(init: std.process.Init) !void {
const io = init.io; const io = init.io;
const gpa = init.arena.allocator(); const arena = init.arena.allocator();
const args = try init.minimal.args.toSlice(gpa); const args = try init.minimal.args.toSlice(arena);
setupColors(io); setupColors(io);
@@ -150,17 +153,7 @@ pub fn main(init: std.process.Init) !void {
for (1..args.len) |n| { for (1..args.len) |n| {
const arg = args[n]; const arg = args[n];
if (std.mem.eql(u8, arg, "--logo")) { if (std.mem.eql(u8, arg, "--logo")) {
print("{s}", .{logo}); print("{s}{s}{s}", .{ yellow_text, logo, reset_text });
return;
} else if (std.mem.eql(u8, arg, "--reset")) {
std.Io.Dir.cwd().deleteFile(io, progress_filename) catch |err| switch (err) {
error.FileNotFound => {},
else => {
print("Unable to remove progress file: {}\n", .{err});
std.process.exit(1);
},
};
print("Progress reset, {s} removed.\n", .{progress_filename});
return; return;
} else if (prefix(arg, "--zig=")) |v| { } else if (prefix(arg, "--zig=")) |v| {
zig_exe = v; zig_exe = v;
@@ -188,7 +181,7 @@ pub fn main(init: std.process.Init) !void {
print("{s}", .{logo}); print("{s}", .{logo});
const ctx: Context = .{ .io = io, .gpa = gpa, .zig_exe = zig_exe, .work_path = work_path }; const ctx: Context = .{ .io = io, .arena = arena, .zig_exe = zig_exe, .work_path = work_path };
switch (mode) { switch (mode) {
.named => { .named => {
@@ -220,7 +213,7 @@ pub fn main(init: std.process.Init) !void {
}, },
.normal => { .normal => {
// Start after the last solved exercise recorded in .progress.txt. // Start after the last solved exercise recorded in .progress.txt.
const solved = readProgress(io, gpa); const solved = readProgress(io, arena);
var start_index: usize = 0; var start_index: usize = 0;
for (exercises, 0..) |ex, idx| { for (exercises, 0..) |ex, idx| {
if (solved < ex.number()) { if (solved < ex.number()) {
@@ -245,15 +238,15 @@ fn prefix(arg: []const u8, pre: []const u8) ?[]const u8 {
// Shared, read-only run context threaded through the helpers. // Shared, read-only run context threaded through the helpers.
const Context = struct { const Context = struct {
io: std.Io, io: std.Io,
gpa: std.mem.Allocator, arena: std.mem.Allocator,
zig_exe: []const u8, zig_exe: []const u8,
work_path: []const u8, work_path: []const u8,
}; };
const Error = error{Failed}; const Error = error{Failed};
// Iterates exercises from `start_index` to the end, stopping at the first // Iterates exercises from `start_index` to the end, stopping at the first failure.
// failure. Progress is written after each passed exercise. // Progress is written after each passed exercise.
fn iterateFrom(ctx: Context, start_index: usize) Error!void { fn iterateFrom(ctx: Context, start_index: usize) Error!void {
for (exercises[start_index..]) |ex| { for (exercises[start_index..]) |ex| {
try runOne(ctx, ex, .normal); try runOne(ctx, ex, .normal);
@@ -284,12 +277,12 @@ fn runOne(ctx: Context, ex: Exercise, mode: Mode) Error!void {
}, },
} }
writeProgress(ctx.io, ctx.gpa, ex.number()) catch {}; writeProgress(ctx.io, ctx.arena, ex.number()) catch {};
} }
fn hintAndHelp(ex: Exercise, mode: Mode) void { fn hintAndHelp(ex: Exercise, mode: Mode) void {
if (ex.hint) |hint| if (ex.hint) |hint|
print("\n{s}Ziglings hint: {s}{s}", .{ bold_text, hint, reset_text }); print("\n{s}{s}Ziglings hint: {s}{s}", .{ bold_text, green_text, hint, reset_text });
help(ex, mode); help(ex, mode);
} }
@@ -313,24 +306,22 @@ fn printProgress(num: usize, max: usize) void {
fn runExe(ctx: Context, ex: Exercise) !void { fn runExe(ctx: Context, ex: Exercise) !void {
const io = ctx.io; const io = ctx.io;
const gpa = ctx.gpa; const arena = ctx.arena;
print("Compiling {s}...\n", .{ex.main_file}); print("Compiling {s}...\n", .{ex.main_file});
const path = std.fs.path.join(gpa, &.{ ctx.work_path, ex.main_file }) catch const path = std.fs.path.join(arena, &.{ ctx.work_path, ex.main_file }) catch @panic("OOM");
@panic("OOM");
var argv = std.ArrayList([]const u8).initCapacity(gpa, 8) catch @panic("OOM"); var argv = std.ArrayList([]const u8).initCapacity(arena, 8) catch @panic("OOM");
defer argv.deinit(gpa); argv.append(arena, ctx.zig_exe) catch @panic("OOM");
argv.append(gpa, ctx.zig_exe) catch @panic("OOM"); argv.append(arena, "run") catch @panic("OOM");
argv.append(gpa, "run") catch @panic("OOM");
if (ex.link_libc) { if (ex.link_libc) {
argv.append(gpa, "-lc") catch @panic("OOM"); argv.append(arena, "-lc") catch @panic("OOM");
argv.append(gpa, "-fllvm") catch @panic("OOM"); argv.append(arena, "-fllvm") catch @panic("OOM");
} }
argv.append(gpa, path) catch @panic("OOM"); argv.append(arena, path) catch @panic("OOM");
// `zig run` compiles and runs in one step using Zig's own cache. // `zig run` compiles and runs in one step using Zig's own cache.
const result = Process.run(gpa, io, .{ const result = Process.run(arena, io, .{
.argv = argv.items, .argv = argv.items,
.stdout_limit = .limited(1024 * 1024), .stdout_limit = .limited(1024 * 1024),
.stderr_limit = .limited(1024 * 1024), .stderr_limit = .limited(1024 * 1024),
@@ -344,28 +335,27 @@ fn runExe(ctx: Context, ex: Exercise) !void {
resetLine(); resetLine();
print("Checking {s}...\n", .{ex.main_file}); print("Checking {s}...\n", .{ex.main_file});
return checkOutput(io, gpa, ex, result); return checkOutput(io, arena, ex, result);
} }
fn runTest(ctx: Context, ex: Exercise) !void { fn runTest(ctx: Context, ex: Exercise) !void {
const io = ctx.io; const io = ctx.io;
const gpa = ctx.gpa; const arena = ctx.arena;
print("Compiling {s}...\n", .{ex.main_file}); print("Compiling {s}...\n", .{ex.main_file});
const path = std.fs.path.join(gpa, &.{ ctx.work_path, ex.main_file }) catch const path = std.fs.path.join(arena, &.{ ctx.work_path, ex.main_file }) catch
@panic("OOM"); @panic("OOM");
var argv = std.ArrayList([]const u8).initCapacity(gpa, 8) catch @panic("OOM"); var argv = std.ArrayList([]const u8).initCapacity(arena, 8) catch @panic("OOM");
defer argv.deinit(gpa); argv.append(arena, ctx.zig_exe) catch @panic("OOM");
argv.append(gpa, ctx.zig_exe) catch @panic("OOM"); argv.append(arena, "test") catch @panic("OOM");
argv.append(gpa, "test") catch @panic("OOM");
if (ex.link_libc) { if (ex.link_libc) {
argv.append(gpa, "-lc") catch @panic("OOM"); argv.append(arena, "-lc") catch @panic("OOM");
argv.append(gpa, "-fllvm") catch @panic("OOM"); argv.append(arena, "-fllvm") catch @panic("OOM");
} }
argv.append(gpa, path) catch @panic("OOM"); argv.append(arena, path) catch @panic("OOM");
const result = Process.run(gpa, io, .{ const result = Process.run(arena, io, .{
.argv = argv.items, .argv = argv.items,
.stdout_limit = .limited(1024 * 1024), .stdout_limit = .limited(1024 * 1024),
.stderr_limit = .limited(1024 * 1024), .stderr_limit = .limited(1024 * 1024),
@@ -382,7 +372,7 @@ fn runTest(ctx: Context, ex: Exercise) !void {
return checkTest(ex, result); return checkTest(ex, result);
} }
fn checkOutput(io: std.Io, gpa: std.mem.Allocator, ex: Exercise, result: Process.RunResult) !void { fn checkOutput(io: std.Io, arena: std.mem.Allocator, ex: Exercise, result: Process.RunResult) !void {
switch (result.term) { switch (result.term) {
.exited => |code| if (code != 0) { .exited => |code| if (code != 0) {
// `zig run` puts both compile errors and runtime panics on stderr; // `zig run` puts both compile errors and runtime panics on stderr;
@@ -400,7 +390,7 @@ fn checkOutput(io: std.Io, gpa: std.mem.Allocator, ex: Exercise, result: Process
} }
const raw_output = if (ex.check_stdout) result.stdout else result.stderr; const raw_output = if (ex.check_stdout) result.stdout else result.stderr;
const output = trimLines(gpa, raw_output) catch @panic("OOM"); const output = trimLines(arena, raw_output) catch @panic("OOM");
var exercise_output = ex.output; var exercise_output = ex.output;
if (ex.timestamp) { if (ex.timestamp) {
@@ -477,28 +467,27 @@ fn resetLine() void {
} }
// Removes trailing whitespace per line and any trailing LF at the end. // Removes trailing whitespace per line and any trailing LF at the end.
fn trimLines(gpa: std.mem.Allocator, buf: []const u8) ![]const u8 { fn trimLines(arena: std.mem.Allocator, buf: []const u8) ![]const u8 {
var list = try std.ArrayList(u8).initCapacity(gpa, buf.len); var list = try std.ArrayList(u8).initCapacity(arena, buf.len);
errdefer list.deinit(gpa);
var iter = std.mem.splitSequence(u8, buf, " \n"); var iter = std.mem.splitSequence(u8, buf, " \n");
while (iter.next()) |line| { while (iter.next()) |line| {
const data = std.mem.trimEnd(u8, line, " \r"); const data = std.mem.trimEnd(u8, line, " \r");
try list.appendSlice(gpa, data); try list.appendSlice(arena, data);
try list.append(gpa, '\n'); try list.append(arena, '\n');
} }
const result = try list.toOwnedSlice(gpa); const result = try list.toOwnedSlice(arena);
return std.mem.trimEnd(u8, result, "\n"); return std.mem.trimEnd(u8, result, "\n");
} }
// Reads the last solved exercise number from .progress.txt; 0 if absent. // Reads the last solved exercise number from .progress.txt; 0 if absent.
fn readProgress(io: std.Io, gpa: std.mem.Allocator) u32 { fn readProgress(io: std.Io, arena: std.mem.Allocator) u32 {
const file = std.Io.Dir.cwd().openFile(io, progress_filename, .{}) catch return 0; const file = std.Io.Dir.cwd().openFile(io, progress_filename, .{}) catch return 0;
defer file.close(io); defer file.close(io);
const size = file.length(io) catch return 0; const size = file.length(io) catch return 0;
if (size == 0) return 0; if (size == 0) return 0;
const contents = gpa.alloc(u8, size) catch return 0; const contents = arena.alloc(u8, size) catch return 0;
var file_buffer: [1024]u8 = undefined; var file_buffer: [1024]u8 = undefined;
var reader = file.reader(io, &file_buffer); var reader = file.reader(io, &file_buffer);
const n = reader.interface.readSliceShort(contents) catch return 0; const n = reader.interface.readSliceShort(contents) catch return 0;
@@ -506,8 +495,8 @@ fn readProgress(io: std.Io, gpa: std.mem.Allocator) u32 {
return std.fmt.parseInt(u32, trimmed, 10) catch 0; return std.fmt.parseInt(u32, trimmed, 10) catch 0;
} }
fn writeProgress(io: std.Io, gpa: std.mem.Allocator, number: usize) !void { fn writeProgress(io: std.Io, arena: std.mem.Allocator, number: usize) !void {
const progress = try std.fmt.allocPrint(gpa, "{d}", .{number}); const progress = try std.fmt.allocPrint(arena, "{d}", .{number});
const file = try std.Io.Dir.cwd().createFile( const file = try std.Io.Dir.cwd().createFile(
io, io,
progress_filename, progress_filename,
@@ -693,7 +682,10 @@ const exercises = [_]Exercise{
}, },
.{ .{
.main_file = "028_defer2.zig", .main_file = "028_defer2.zig",
.output = "(Goat) (Cat) (Dog) (Dog) (Goat) (Unknown) done.", .output =
\\(Goat) (Cat) (Dog) (Dog) (Goat) (Unknown) done.
\\Answer to everything? 42
, // pay attention to the comma
}, },
.{ .{
.main_file = "029_errdefer.zig", .main_file = "029_errdefer.zig",
@@ -965,9 +957,6 @@ const exercises = [_]Exercise{
\\Grasshopper hopped 32 meters. \\Grasshopper hopped 32 meters.
, // pay attention to the comma , // pay attention to the comma
}, },
// Skipped because of https://github.com/ratfactor/ziglings/issues/163
// direct link: https://github.com/ziglang/zig/issues/6025
.{ .{
.main_file = "085_async.zig", .main_file = "085_async.zig",
.output = "Current time: <timestamp>s since epoch", .output = "Current time: <timestamp>s since epoch",