From b59bef29b9c74270539e2f775d2f0394ec435f2d Mon Sep 17 00:00:00 2001 From: Manlio Perillo Date: Wed, 12 Apr 2023 17:04:18 +0200 Subject: [PATCH 1/4] ci: add a CI workflow Add a new github workflow named CI. Add a job named compat, checking that and old Zig compiler will not fail with a compiler error, but instead will print an useful error message. --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ ci/compat.sh | 25 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100755 ci/compat.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ebeda5b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI +on: + pull_request: + branches: [ main ] + +defaults: + run: + shell: bash + +jobs: + compat: + runs-on: ubuntu-latest + strategy: + matrix: + zig: [ 0.6.0, 0.7.0, 0.8.0, 0.9.0, 0.10.0 ] + steps: + - uses: actions/checkout@v2 + + - name: Setup Zig + uses: goto-bus-stop/setup-zig@v2 + with: + version: ${{ matrix.zig }} + + - name: Check compatibility with old Zig compilers + run: ci/compat.sh diff --git a/ci/compat.sh b/ci/compat.sh new file mode 100755 index 0000000..f5895fd --- /dev/null +++ b/ci/compat.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# This script checks that `zig build` will return an useful error message when +# the Zig compiler is not compatible, instead of failing due to a syntax error. +# +# This script should be run on an UNIX system. + +zig_version=$(zig version) + +zig build -Dn=1 -Dhealed &> /dev/null 2>&1 +zig_ret=$? + +if [ "$zig_ret" -eq 0 ]; then + printf "zig %s unexpectedly succeeded\n" "$zig_version" + exit 1 +fi + +zig_error=$(zig build -Dn=1 -Dhealed 2>&1) + +echo "$zig_error" | grep -q "it looks like your version of zig is too old" +zig_ret=$? + +if [ "$zig_ret" -ne 0 ]; then + printf "zig %s is not compatible\n" "$zig_version" + exit 1 +fi From 6b17a188936d489e2777e541c84112b2609532d5 Mon Sep 17 00:00:00 2001 From: Manlio Perillo Date: Sun, 16 Apr 2023 12:23:10 +0200 Subject: [PATCH 2/4] Ensure the exercises use the canonical format Add the check-exercises.py tool in the new tools directory. It is used to check that the exercises are correctly formatted, printing on stderr the invalid ones and the diff in the unified format. Update the exercises that don't use the canonical zig fmt format. Update some patches that cause the generated zig file to be incorrectly formatted. --- exercises/058_quiz7.zig | 28 +++---- exercises/059_integers.zig | 6 +- exercises/069_comptime4.zig | 2 +- exercises/075_quiz8.zig | 4 +- exercises/083_anonymous_lists.zig | 4 +- exercises/092_interfaces.zig | 2 +- patches/patches/024_errors4.patch | 2 +- patches/patches/059_integers.patch | 8 +- patches/patches/083_anonymous_lists.patch | 4 +- tools/check-exercises.py | 97 +++++++++++++++++++++++ 10 files changed, 127 insertions(+), 30 deletions(-) create mode 100755 tools/check-exercises.py diff --git a/exercises/058_quiz7.zig b/exercises/058_quiz7.zig index 3069710..1ceac5a 100644 --- a/exercises/058_quiz7.zig +++ b/exercises/058_quiz7.zig @@ -107,7 +107,7 @@ const Path = struct { const a_paths = [_]Path{ Path{ .from = &a, // from: Archer's Point - .to = &b, // to: Bridge + .to = &b, // to: Bridge .dist = 2, }, }; @@ -115,12 +115,12 @@ const a_paths = [_]Path{ const b_paths = [_]Path{ Path{ .from = &b, // from: Bridge - .to = &a, // to: Archer's Point + .to = &a, // to: Archer's Point .dist = 2, }, Path{ .from = &b, // from: Bridge - .to = &d, // to: Dogwood Grove + .to = &d, // to: Dogwood Grove .dist = 1, }, }; @@ -128,12 +128,12 @@ const b_paths = [_]Path{ const c_paths = [_]Path{ Path{ .from = &c, // from: Cottage - .to = &d, // to: Dogwood Grove + .to = &d, // to: Dogwood Grove .dist = 3, }, Path{ .from = &c, // from: Cottage - .to = &e, // to: East Pond + .to = &e, // to: East Pond .dist = 2, }, }; @@ -141,17 +141,17 @@ const c_paths = [_]Path{ const d_paths = [_]Path{ Path{ .from = &d, // from: Dogwood Grove - .to = &b, // to: Bridge + .to = &b, // to: Bridge .dist = 1, }, Path{ .from = &d, // from: Dogwood Grove - .to = &c, // to: Cottage + .to = &c, // to: Cottage .dist = 3, }, Path{ .from = &d, // from: Dogwood Grove - .to = &f, // to: Fox Pond + .to = &f, // to: Fox Pond .dist = 7, }, }; @@ -159,20 +159,20 @@ const d_paths = [_]Path{ const e_paths = [_]Path{ Path{ .from = &e, // from: East Pond - .to = &c, // to: Cottage + .to = &c, // to: Cottage .dist = 2, }, Path{ .from = &e, // from: East Pond - .to = &f, // to: Fox Pond - .dist = 1, // (one-way down a short waterfall!) + .to = &f, // to: Fox Pond + .dist = 1, // (one-way down a short waterfall!) }, }; const f_paths = [_]Path{ Path{ .from = &f, // from: Fox Pond - .to = &d, // to: Dogwood Grove + .to = &d, // to: Dogwood Grove .dist = 7, }, }; @@ -355,8 +355,8 @@ pub fn main() void { // Here's where the hermit decides where he would like to go. Once // you get the program working, try some different Places on the // map! - const start = &a; // Archer's Point - const destination = &f; // Fox Pond + const start = &a; // Archer's Point + const destination = &f; // Fox Pond // Store each Path array as a slice in each Place. As mentioned // above, we needed to delay making these references to avoid diff --git a/exercises/059_integers.zig b/exercises/059_integers.zig index 5a6295d..a497efa 100644 --- a/exercises/059_integers.zig +++ b/exercises/059_integers.zig @@ -18,10 +18,10 @@ const print = @import("std").debug.print; pub fn main() void { - var zig = [_]u8 { - 0o131, // octal + var zig = [_]u8{ + 0o131, // octal 0b1101000, // binary - 0x66, // hex + 0x66, // hex }; print("{s} is cool.\n", .{zig}); diff --git a/exercises/069_comptime4.zig b/exercises/069_comptime4.zig index 004a42c..f4c8bbb 100644 --- a/exercises/069_comptime4.zig +++ b/exercises/069_comptime4.zig @@ -16,7 +16,7 @@ const print = @import("std").debug.print; pub fn main() void { // Here we declare arrays of three different types and sizes // at compile time from a function call. Neat! - const s1 = makeSequence(u8, 3); // creates a [3]u8 + const s1 = makeSequence(u8, 3); // creates a [3]u8 const s2 = makeSequence(u32, 5); // creates a [5]u32 const s3 = makeSequence(i64, 7); // creates a [7]i64 diff --git a/exercises/075_quiz8.zig b/exercises/075_quiz8.zig index c2dbc1a..d54864c 100644 --- a/exercises/075_quiz8.zig +++ b/exercises/075_quiz8.zig @@ -151,8 +151,8 @@ const HermitsNotebook = struct { }; pub fn main() void { - const start = &a; // Archer's Point - const destination = &f; // Fox Pond + const start = &a; // Archer's Point + const destination = &f; // Fox Pond // We could either have this: // diff --git a/exercises/083_anonymous_lists.zig b/exercises/083_anonymous_lists.zig index 838d40e..daaeaff 100644 --- a/exercises/083_anonymous_lists.zig +++ b/exercises/083_anonymous_lists.zig @@ -18,8 +18,8 @@ pub fn main() void { // // Don't change this part: // - // = .{'h', 'e', 'l', 'l', 'o'}; + // = .{ 'h', 'e', 'l', 'l', 'o' }; // - const hello = .{'h', 'e', 'l', 'l', 'o'}; + const hello = .{ 'h', 'e', 'l', 'l', 'o' }; print("I say {s}!\n", .{hello}); } diff --git a/exercises/092_interfaces.zig b/exercises/092_interfaces.zig index 43f1119..5ac5768 100644 --- a/exercises/092_interfaces.zig +++ b/exercises/092_interfaces.zig @@ -99,7 +99,7 @@ pub fn main() !void { var my_insects = [_]Insect{ Insect{ .ant = Ant{ .still_alive = true } }, Insect{ .bee = Bee{ .flowers_visited = 17 } }, - Insect{ .grasshopper = Grasshopper{ .distance_hopped = 32 }, }, + Insect{ .grasshopper = Grasshopper{ .distance_hopped = 32 } }, }; std.debug.print("Daily Insect Report:\n", .{}); diff --git a/patches/patches/024_errors4.patch b/patches/patches/024_errors4.patch index 5996a99..48e0821 100644 --- a/patches/patches/024_errors4.patch +++ b/patches/patches/024_errors4.patch @@ -5,6 +5,6 @@ > if (err == MyNumberError.TooSmall) { > return 10; > } -> +> > return err; > }; diff --git a/patches/patches/059_integers.patch b/patches/patches/059_integers.patch index 50a89a0..7075ebe 100644 --- a/patches/patches/059_integers.patch +++ b/patches/patches/059_integers.patch @@ -1,8 +1,8 @@ 22,24c22,24 -< 0o131, // octal +< 0o131, // octal < 0b1101000, // binary -< 0x66, // hex +< 0x66, // hex --- -> 0o132, // octal +> 0o132, // octal > 0b1101001, // binary -> 0x67, // hex +> 0x67, // hex diff --git a/patches/patches/083_anonymous_lists.patch b/patches/patches/083_anonymous_lists.patch index b981909..94b594b 100644 --- a/patches/patches/083_anonymous_lists.patch +++ b/patches/patches/083_anonymous_lists.patch @@ -1,4 +1,4 @@ 23c23 -< const hello = .{'h', 'e', 'l', 'l', 'o'}; +< const hello = .{ 'h', 'e', 'l', 'l', 'o' }; --- -> const hello: [5]u8 = .{'h', 'e', 'l', 'l', 'o'}; +> const hello: [5]u8 = .{ 'h', 'e', 'l', 'l', 'o' }; diff --git a/tools/check-exercises.py b/tools/check-exercises.py new file mode 100755 index 0000000..fa0b5cb --- /dev/null +++ b/tools/check-exercises.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python + +import difflib +import io +import os +import os.path +import subprocess +import sys + + +IGNORE = subprocess.DEVNULL +PIPE = subprocess.PIPE + +EXERCISES_PATH = "exercises" +HEALED_PATH = "patches/healed" +PATCHES_PATH = "patches/patches" + + +# Heals all the exercises. +def heal(): + maketree(HEALED_PATH) + + with os.scandir(EXERCISES_PATH) as it: + for entry in it: + name = entry.name + + original_path = entry.path + patch_path = os.path.join(PATCHES_PATH, patch_name(name)) + output_path = os.path.join(HEALED_PATH, name) + + patch(original_path, patch_path, output_path) + + +# Yields all the healed exercises that are not correctly formatted. +def check_healed(): + term = subprocess.run( + ["zig", "fmt", "--check", HEALED_PATH], stdout=PIPE, text=True + ) + if term.stdout == "" and term.returncode != 0: + term.check_returncode() + + stream = io.StringIO(term.stdout) + for line in stream: + yield line.strip() + + +def main(): + heal() + + # Show the unified diff between the original example and the correctly + # formatted one. + for i, original in enumerate(check_healed()): + if i > 0: + print() + + name = os.path.basename(original) + print(f"checking exercise {name}...\n") + + from_file = open(original) + to_file = zig_fmt_file(original) + + diff = difflib.unified_diff( + from_file.readlines(), to_file.readlines(), name, name + "-fmt" + ) + sys.stderr.writelines(diff) + + +def maketree(path): + return os.makedirs(path, exist_ok=True) + + +# Returns path with the patch extension. +def patch_name(path): + name, _ = os.path.splitext(path) + + return name + ".patch" + + +# Applies patch to original, and write the file to output. +def patch(original, patch, output): + subprocess.run( + ["patch", "-i", patch, "-o", output, original], stdout=IGNORE, check=True + ) + + +# Formats the Zig file at path, and returns the possibly reformatted file as a +# file object. +def zig_fmt_file(path): + with open(path) as stdin: + term = subprocess.run( + ["zig", "fmt", "--stdin"], stdin=stdin, stdout=PIPE, check=True, text=True + ) + + return io.StringIO(term.stdout) + + +main() From 7d7be0482c3a2cafc91c743c6d139b5c140457f3 Mon Sep 17 00:00:00 2001 From: Manlio Perillo Date: Sun, 16 Apr 2023 18:07:51 +0200 Subject: [PATCH 3/4] eowyn: run `zig fmt --check` before `zig build` Update the eowyn.sh script to check the healed exercises formatting, before running `zig build`. --- patches/eowyn.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/patches/eowyn.sh b/patches/eowyn.sh index a30a67a..a036acf 100755 --- a/patches/eowyn.sh +++ b/patches/eowyn.sh @@ -42,5 +42,8 @@ do fi done +# Check the healed exercises formatting. +zig fmt --check patches/healed + # Test the healed exercises. May the compiler have mercy upon us. zig build -Dhealed From c7697a428206a46326a61b2c851aab2a7dadb8b8 Mon Sep 17 00:00:00 2001 From: Manlio Perillo Date: Mon, 17 Apr 2023 09:19:49 +0200 Subject: [PATCH 4/4] Ensure the patches are up-to-date and consistent Add the update-patches.py tool. Update all the patches, so that the files are up-to-date and use the same patch file format. --- patches/patches/026_hello2.patch | 2 +- patches/patches/029_errdefer.patch | 2 +- patches/patches/040_pointers2.patch | 2 +- patches/patches/046_optionals2.patch | 4 +- patches/patches/050_no_value.patch | 2 +- patches/patches/064_builtins.patch | 4 +- patches/patches/065_builtins2.patch | 59 ++++++------------ patches/patches/066_comptime.patch | 2 +- patches/patches/076_sentinels.patch | 4 +- patches/patches/080_anonymous_structs.patch | 26 +++----- patches/patches/096_memory_allocation.patch | 2 +- patches/patches/097_bit_manipulation.patch | 2 +- patches/patches/098_bit_manipulation2.patch | 2 +- tools/update-patches.py | 68 +++++++++++++++++++++ 14 files changed, 110 insertions(+), 71 deletions(-) create mode 100755 tools/update-patches.py diff --git a/patches/patches/026_hello2.patch b/patches/patches/026_hello2.patch index 0065da5..e97a7b5 100644 --- a/patches/patches/026_hello2.patch +++ b/patches/patches/026_hello2.patch @@ -1,4 +1,4 @@ -22c22 +26c26 < stdout.print("Hello world!\n", .{}); --- > try stdout.print("Hello world!\n", .{}); diff --git a/patches/patches/029_errdefer.patch b/patches/patches/029_errdefer.patch index f93c56f..aa42279 100644 --- a/patches/patches/029_errdefer.patch +++ b/patches/patches/029_errdefer.patch @@ -1,4 +1,4 @@ -35c34 +35c35 < std.debug.print("failed!\n", .{}); --- > errdefer std.debug.print("failed!\n", .{}); diff --git a/patches/patches/040_pointers2.patch b/patches/patches/040_pointers2.patch index a69cb20..87291c3 100644 --- a/patches/patches/040_pointers2.patch +++ b/patches/patches/040_pointers2.patch @@ -1,4 +1,4 @@ -24c24 +26c26 < const b: *u8 = &a; // fix this! --- > const b: *const u8 = &a; // fix this! diff --git a/patches/patches/046_optionals2.patch b/patches/patches/046_optionals2.patch index 8437cdc..89fa604 100644 --- a/patches/patches/046_optionals2.patch +++ b/patches/patches/046_optionals2.patch @@ -1,8 +1,8 @@ -24c12 +24c24 < tail: *Elephant = null, // Hmm... tail needs something... --- > tail: ?*Elephant = null, // <---- make this optional! -54c42 +54c54 < if (e.tail == null) ???; --- > if (e.tail == null) break; diff --git a/patches/patches/050_no_value.patch b/patches/patches/050_no_value.patch index 79db0a3..7ea4e9e 100644 --- a/patches/patches/050_no_value.patch +++ b/patches/patches/050_no_value.patch @@ -6,7 +6,7 @@ < var first_line2: Err!*const [21]u8 = ???; --- > var first_line2: Err!*const [21]u8 = Err.Cthulhu; -79,80c79,80 +80,81c80,81 < fn printSecondLine() ??? { < var second_line2: ?*const [18]u8 = ???; --- diff --git a/patches/patches/064_builtins.patch b/patches/patches/064_builtins.patch index ebe313d..8a154a4 100644 --- a/patches/patches/064_builtins.patch +++ b/patches/patches/064_builtins.patch @@ -1,8 +1,8 @@ -72c72 +67c67 < const expected_result: u8 = ???; --- > const expected_result: u8 = 0b00010010; -88c88 +82c82 < const tupni: u8 = @bitReverse(input, tupni); --- > const tupni: u8 = @bitReverse(input); diff --git a/patches/patches/065_builtins2.patch b/patches/patches/065_builtins2.patch index 89fb55f..1b085ef 100644 --- a/patches/patches/065_builtins2.patch +++ b/patches/patches/065_builtins2.patch @@ -1,39 +1,20 @@ ---- exercises/065_builtins2.zig -+++ answers/065_builtins2.zig -@@ -58,7 +58,7 @@ - // Oops! We cannot leave the 'me' and 'myself' fields - // undefined. Please set them here: - narcissus.me = &narcissus; -- narcissus.??? = ???; -+ narcissus.myself = &narcissus; - - // This determines a "peer type" from three separate - // references (they just happen to all be the same object). -@@ -70,7 +70,7 @@ - // - // The fix for this is very subtle, but it makes a big - // difference! -- const Type2 = narcissus.fetchTheMostBeautifulType(); -+ const Type2 = Narcissus.fetchTheMostBeautifulType(); - - // Now we print a pithy statement about Narcissus. - print("A {s} loves all {s}es. ", .{ -@@ -109,15 +109,15 @@ - // Please complete these 'if' statements so that the field - // name will not be printed if the field is of type 'void' - // (which is a zero-bit type that takes up no space at all!): -- if (fields[0].??? != void) { -+ if (fields[0].type != void) { - print(" {s}", .{@typeInfo(Narcissus).Struct.fields[0].name}); - } - -- if (fields[1].??? != void) { -+ if (fields[1].type != void) { - print(" {s}", .{@typeInfo(Narcissus).Struct.fields[1].name}); - } - -- if (fields[2].??? != void) { -+ if (fields[2].type != void) { - print(" {s}", .{@typeInfo(Narcissus).Struct.fields[2].name}); - } - +61c61 +< narcissus.??? = ???; +--- +> narcissus.myself = &narcissus; +73c73 +< const Type2 = narcissus.fetchTheMostBeautifulType(); +--- +> const Type2 = Narcissus.fetchTheMostBeautifulType(); +112c112 +< if (fields[0].??? != void) { +--- +> if (fields[0].type != void) { +116c116 +< if (fields[1].??? != void) { +--- +> if (fields[1].type != void) { +120c120 +< if (fields[2].??? != void) { +--- +> if (fields[2].type != void) { diff --git a/patches/patches/066_comptime.patch b/patches/patches/066_comptime.patch index 4828ad6..e7130c2 100644 --- a/patches/patches/066_comptime.patch +++ b/patches/patches/066_comptime.patch @@ -1,4 +1,4 @@ -64,65c64,65 +65,66c65,66 < var var_int = 12345; < var var_float = 987.654; --- diff --git a/patches/patches/076_sentinels.patch b/patches/patches/076_sentinels.patch index 3cf4877..55c4c96 100644 --- a/patches/patches/076_sentinels.patch +++ b/patches/patches/076_sentinels.patch @@ -1,8 +1,8 @@ -85c84 +85c85 < for (???) |s| { --- > for (my_seq) |s| { -97c96 +97c97 < while (??? != my_sentinel) { --- > while (my_seq[i] != my_sentinel) { diff --git a/patches/patches/080_anonymous_structs.patch b/patches/patches/080_anonymous_structs.patch index 6df1890..a94e7b8 100644 --- a/patches/patches/080_anonymous_structs.patch +++ b/patches/patches/080_anonymous_structs.patch @@ -1,18 +1,8 @@ ---- exercises/080_anonymous_structs.zig -+++ answers/080_anonymous_structs.zig -@@ -48,13 +48,13 @@ - // * circle1 should hold i32 integers - // * circle2 should hold f32 floats - // -- var circle1 = ??? { -+ var circle1 = Circle(i32){ - .center_x = 25, - .center_y = 70, - .radius = 15, - }; - -- var circle2 = ??? { -+ var circle2 = Circle(f32){ - .center_x = 25.234, - .center_y = 70.999, - .radius = 15.714, +51c51 +< var circle1 = ??? { +--- +> var circle1 = Circle(i32){ +57c57 +< var circle2 = ??? { +--- +> var circle2 = Circle(f32){ diff --git a/patches/patches/096_memory_allocation.patch b/patches/patches/096_memory_allocation.patch index 5398ce5..fd990b0 100644 --- a/patches/patches/096_memory_allocation.patch +++ b/patches/patches/096_memory_allocation.patch @@ -1,4 +1,4 @@ -65c65 +66c66 < var avg: []f64 = ???; --- > var avg: []f64 = try allocator.alloc(f64, arr.len); diff --git a/patches/patches/097_bit_manipulation.patch b/patches/patches/097_bit_manipulation.patch index e216309..2afe3da 100644 --- a/patches/patches/097_bit_manipulation.patch +++ b/patches/patches/097_bit_manipulation.patch @@ -1,4 +1,4 @@ -82c82 +83c83 < ???; --- > x ^= y; diff --git a/patches/patches/098_bit_manipulation2.patch b/patches/patches/098_bit_manipulation2.patch index 1b5df35..8becd34 100644 --- a/patches/patches/098_bit_manipulation2.patch +++ b/patches/patches/098_bit_manipulation2.patch @@ -1,4 +1,4 @@ -62c62 +63c63 < return bits == 0x..???; --- > return bits == 0x3ffffff; diff --git a/tools/update-patches.py b/tools/update-patches.py new file mode 100755 index 0000000..76a1c46 --- /dev/null +++ b/tools/update-patches.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +import os +import os.path +import subprocess + + +IGNORE = subprocess.DEVNULL + +EXERCISES_PATH = "exercises" +ANSWERS_PATH = "answers" +PATCHES_PATH = "patches/patches" + + +# Heals all the exercises. +def heal(): + maketree(ANSWERS_PATH) + + with os.scandir(EXERCISES_PATH) as it: + for entry in it: + name = entry.name + + original_path = entry.path + patch_path = os.path.join(PATCHES_PATH, patch_name(name)) + output_path = os.path.join(ANSWERS_PATH, name) + + patch(original_path, patch_path, output_path) + + +def main(): + heal() + + with os.scandir(EXERCISES_PATH) as it: + for entry in it: + name = entry.name + + broken_path = entry.path + healed_path = os.path.join(ANSWERS_PATH, name) + patch_path = os.path.join(PATCHES_PATH, patch_name(name)) + + with open(patch_path, "w") as file: + term = subprocess.run( + ["diff", broken_path, healed_path], + stdout=file, + text=True, + ) + assert term.returncode == 1 + + +def maketree(path): + return os.makedirs(path, exist_ok=True) + + +# Returns path with the patch extension. +def patch_name(path): + name, _ = os.path.splitext(path) + + return name + ".patch" + + +# Applies patch to original, and write the file to output. +def patch(original, patch, output): + subprocess.run( + ["patch", "-i", patch, "-o", output, original], stdout=IGNORE, check=True + ) + + +main()