mirror of
https://codeberg.org/andyscott/ziglings.git
synced 2024-12-22 14:03:10 -05:00
6b17a18893
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.
471 lines
18 KiB
Zig
471 lines
18 KiB
Zig
//
|
|
// We've absorbed a lot of information about the variations of types
|
|
// we can use in Zig. Roughly, in order we have:
|
|
//
|
|
// u8 single item
|
|
// *u8 single-item pointer
|
|
// []u8 slice (size known at runtime)
|
|
// [5]u8 array of 5 u8s
|
|
// [*]u8 many-item pointer (zero or more)
|
|
// enum {a, b} set of unique values a and b
|
|
// error {e, f} set of unique error values e and f
|
|
// struct {y: u8, z: i32} group of values y and z
|
|
// union(enum) {a: u8, b: i32} single value either u8 or i32
|
|
//
|
|
// Values of any of the above types can be assigned as "var" or "const"
|
|
// to allow or disallow changes (mutability) via the assigned name:
|
|
//
|
|
// const a: u8 = 5; // immutable
|
|
// var b: u8 = 5; // mutable
|
|
//
|
|
// We can also make error unions or optional types from any of
|
|
// the above:
|
|
//
|
|
// var a: E!u8 = 5; // can be u8 or error from set E
|
|
// var b: ?u8 = 5; // can be u8 or null
|
|
//
|
|
// Knowing all of this, maybe we can help out a local hermit. He made
|
|
// a little Zig program to help him plan his trips through the woods,
|
|
// but it has some mistakes.
|
|
//
|
|
// *************************************************************
|
|
// * A NOTE ABOUT THIS EXERCISE *
|
|
// * *
|
|
// * You do NOT have to read and understand every bit of this *
|
|
// * program. This is a very big example. Feel free to skim *
|
|
// * through it and then just focus on the few parts that are *
|
|
// * actually broken! *
|
|
// * *
|
|
// *************************************************************
|
|
//
|
|
const print = @import("std").debug.print;
|
|
|
|
// The grue is a nod to Zork.
|
|
const TripError = error{ Unreachable, EatenByAGrue };
|
|
|
|
// Let's start with the Places on the map. Each has a name and a
|
|
// distance or difficulty of travel (as judged by the hermit).
|
|
//
|
|
// Note that we declare the places as mutable (var) because we need to
|
|
// assign the paths later. And why is that? Because paths contain
|
|
// pointers to places and assigning them now would create a dependency
|
|
// loop!
|
|
const Place = struct {
|
|
name: []const u8,
|
|
paths: []const Path = undefined,
|
|
};
|
|
|
|
var a = Place{ .name = "Archer's Point" };
|
|
var b = Place{ .name = "Bridge" };
|
|
var c = Place{ .name = "Cottage" };
|
|
var d = Place{ .name = "Dogwood Grove" };
|
|
var e = Place{ .name = "East Pond" };
|
|
var f = Place{ .name = "Fox Pond" };
|
|
|
|
// The hermit's hand-drawn ASCII map
|
|
// +---------------------------------------------------+
|
|
// | * Archer's Point ~~~~ |
|
|
// | ~~~ ~~~~~~~~ |
|
|
// | ~~~| |~~~~~~~~~~~~ ~~~~~~~ |
|
|
// | Bridge ~~~~~~~~ |
|
|
// | ^ ^ ^ |
|
|
// | ^ ^ / \ |
|
|
// | ^ ^ ^ ^ |_| Cottage |
|
|
// | Dogwood Grove |
|
|
// | ^ <boat> |
|
|
// | ^ ^ ^ ^ ~~~~~~~~~~~~~ ^ ^ |
|
|
// | ^ ~~ East Pond ~~~ |
|
|
// | ^ ^ ^ ~~~~~~~~~~~~~~ |
|
|
// | ~~ ^ |
|
|
// | ^ ~~~ <-- short waterfall |
|
|
// | ^ ~~~~~ |
|
|
// | ~~~~~~~~~~~~~~~~~ |
|
|
// | ~~~~ Fox Pond ~~~~~~~ ^ ^ |
|
|
// | ^ ~~~~~~~~~~~~~~~ ^ ^ |
|
|
// | ~~~~~ |
|
|
// +---------------------------------------------------+
|
|
//
|
|
// We'll be reserving memory in our program based on the number of
|
|
// places on the map. Note that we do not have to specify the type of
|
|
// this value because we don't actually use it in our program once
|
|
// it's compiled! (Don't worry if this doesn't make sense yet.)
|
|
const place_count = 6;
|
|
|
|
// Now let's create all of the paths between sites. A path goes from
|
|
// one place to another and has a distance.
|
|
const Path = struct {
|
|
from: *const Place,
|
|
to: *const Place,
|
|
dist: u8,
|
|
};
|
|
|
|
// By the way, if the following code seems like a lot of tedious
|
|
// manual labor, you're right! One of Zig's killer features is letting
|
|
// us write code that runs at compile time to "automate" repetitive
|
|
// code (much like macros in other languages), but we haven't learned
|
|
// how to do that yet!
|
|
const a_paths = [_]Path{
|
|
Path{
|
|
.from = &a, // from: Archer's Point
|
|
.to = &b, // to: Bridge
|
|
.dist = 2,
|
|
},
|
|
};
|
|
|
|
const b_paths = [_]Path{
|
|
Path{
|
|
.from = &b, // from: Bridge
|
|
.to = &a, // to: Archer's Point
|
|
.dist = 2,
|
|
},
|
|
Path{
|
|
.from = &b, // from: Bridge
|
|
.to = &d, // to: Dogwood Grove
|
|
.dist = 1,
|
|
},
|
|
};
|
|
|
|
const c_paths = [_]Path{
|
|
Path{
|
|
.from = &c, // from: Cottage
|
|
.to = &d, // to: Dogwood Grove
|
|
.dist = 3,
|
|
},
|
|
Path{
|
|
.from = &c, // from: Cottage
|
|
.to = &e, // to: East Pond
|
|
.dist = 2,
|
|
},
|
|
};
|
|
|
|
const d_paths = [_]Path{
|
|
Path{
|
|
.from = &d, // from: Dogwood Grove
|
|
.to = &b, // to: Bridge
|
|
.dist = 1,
|
|
},
|
|
Path{
|
|
.from = &d, // from: Dogwood Grove
|
|
.to = &c, // to: Cottage
|
|
.dist = 3,
|
|
},
|
|
Path{
|
|
.from = &d, // from: Dogwood Grove
|
|
.to = &f, // to: Fox Pond
|
|
.dist = 7,
|
|
},
|
|
};
|
|
|
|
const e_paths = [_]Path{
|
|
Path{
|
|
.from = &e, // from: East Pond
|
|
.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!)
|
|
},
|
|
};
|
|
|
|
const f_paths = [_]Path{
|
|
Path{
|
|
.from = &f, // from: Fox Pond
|
|
.to = &d, // to: Dogwood Grove
|
|
.dist = 7,
|
|
},
|
|
};
|
|
|
|
// Once we've plotted the best course through the woods, we'll make a
|
|
// "trip" out of it. A trip is a series of Places connected by Paths.
|
|
// We use a TripItem union to allow both Places and Paths to be in the
|
|
// same array.
|
|
const TripItem = union(enum) {
|
|
place: *const Place,
|
|
path: *const Path,
|
|
|
|
// This is a little helper function to print the two different
|
|
// types of item correctly.
|
|
fn printMe(self: TripItem) void {
|
|
switch (self) {
|
|
// Oops! The hermit forgot how to capture the union values
|
|
// in a switch statement. Please capture both values as
|
|
// 'p' so the print statements work!
|
|
.place => print("{s}", .{p.name}),
|
|
.path => print("--{}->", .{p.dist}),
|
|
}
|
|
}
|
|
};
|
|
|
|
// The Hermit's Notebook is where all the magic happens. A notebook
|
|
// entry is a Place discovered on the map along with the Path taken to
|
|
// get there and the distance to reach it from the start point. If we
|
|
// find a better Path to reach a Place (shorter distance), we update the
|
|
// entry. Entries also serve as a "todo" list which is how we keep
|
|
// track of which paths to explore next.
|
|
const NotebookEntry = struct {
|
|
place: *const Place,
|
|
coming_from: ?*const Place,
|
|
via_path: ?*const Path,
|
|
dist_to_reach: u16,
|
|
};
|
|
|
|
// +------------------------------------------------+
|
|
// | ~ Hermit's Notebook ~ |
|
|
// +---+----------------+----------------+----------+
|
|
// | | Place | From | Distance |
|
|
// +---+----------------+----------------+----------+
|
|
// | 0 | Archer's Point | null | 0 |
|
|
// | 1 | Bridge | Archer's Point | 2 | < next_entry
|
|
// | 2 | Dogwood Grove | Bridge | 1 |
|
|
// | 3 | | | | < end_of_entries
|
|
// | ... |
|
|
// +---+----------------+----------------+----------+
|
|
//
|
|
const HermitsNotebook = struct {
|
|
// Remember the array repetition operator `**`? It is no mere
|
|
// novelty, it's also a great way to assign multiple items in an
|
|
// array without having to list them one by one. Here we use it to
|
|
// initialize an array with null values.
|
|
entries: [place_count]?NotebookEntry = .{null} ** place_count,
|
|
|
|
// The next entry keeps track of where we are in our "todo" list.
|
|
next_entry: u8 = 0,
|
|
|
|
// Mark the start of empty space in the notebook.
|
|
end_of_entries: u8 = 0,
|
|
|
|
// We'll often want to find an entry by Place. If one is not
|
|
// found, we return null.
|
|
fn getEntry(self: *HermitsNotebook, place: *const Place) ?*NotebookEntry {
|
|
for (&self.entries, 0..) |*entry, i| {
|
|
if (i >= self.end_of_entries) break;
|
|
|
|
// Here's where the hermit got stuck. We need to return
|
|
// an optional pointer to a NotebookEntry.
|
|
//
|
|
// What we have with "entry" is the opposite: a pointer to
|
|
// an optional NotebookEntry!
|
|
//
|
|
// To get one from the other, we need to dereference
|
|
// "entry" (with .*) and get the non-null value from the
|
|
// optional (with .?) and return the address of that. The
|
|
// if statement provides some clues about how the
|
|
// dereference and optional value "unwrapping" look
|
|
// together. Remember that you return the address with the
|
|
// "&" operator.
|
|
if (place == entry.*.?.place) return entry;
|
|
// Try to make your answer this long:__________;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// The checkNote() method is the beating heart of the magical
|
|
// notebook. Given a new note in the form of a NotebookEntry
|
|
// struct, we check to see if we already have an entry for the
|
|
// note's Place.
|
|
//
|
|
// If we DON'T, we'll add the entry to the end of the notebook
|
|
// along with the Path taken and distance.
|
|
//
|
|
// If we DO, we check to see if the path is "better" (shorter
|
|
// distance) than the one we'd noted before. If it is, we
|
|
// overwrite the old entry with the new one.
|
|
fn checkNote(self: *HermitsNotebook, note: NotebookEntry) void {
|
|
var existing_entry = self.getEntry(note.place);
|
|
|
|
if (existing_entry == null) {
|
|
self.entries[self.end_of_entries] = note;
|
|
self.end_of_entries += 1;
|
|
} else if (note.dist_to_reach < existing_entry.?.dist_to_reach) {
|
|
existing_entry.?.* = note;
|
|
}
|
|
}
|
|
|
|
// The next two methods allow us to use the notebook as a "todo"
|
|
// list.
|
|
fn hasNextEntry(self: *HermitsNotebook) bool {
|
|
return self.next_entry < self.end_of_entries;
|
|
}
|
|
|
|
fn getNextEntry(self: *HermitsNotebook) *const NotebookEntry {
|
|
defer self.next_entry += 1; // Increment after getting entry
|
|
return &self.entries[self.next_entry].?;
|
|
}
|
|
|
|
// After we've completed our search of the map, we'll have
|
|
// computed the shortest Path to every Place. To collect the
|
|
// complete trip from the start to the destination, we need to
|
|
// walk backwards from the destination's notebook entry, following
|
|
// the coming_from pointers back to the start. What we end up with
|
|
// is an array of TripItems with our trip in reverse order.
|
|
//
|
|
// We need to take the trip array as a parameter because we want
|
|
// the main() function to "own" the array memory. What do you
|
|
// suppose could happen if we allocated the array in this
|
|
// function's stack frame (the space allocated for a function's
|
|
// "local" data) and returned a pointer or slice to it?
|
|
//
|
|
// Looks like the hermit forgot something in the return value of
|
|
// this function. What could that be?
|
|
fn getTripTo(self: *HermitsNotebook, trip: []?TripItem, dest: *Place) void {
|
|
// We start at the destination entry.
|
|
const destination_entry = self.getEntry(dest);
|
|
|
|
// This function needs to return an error if the requested
|
|
// destination was never reached. (This can't actually happen
|
|
// in our map since every Place is reachable by every other
|
|
// Place.)
|
|
if (destination_entry == null) {
|
|
return TripError.Unreachable;
|
|
}
|
|
|
|
// Variables hold the entry we're currently examining and an
|
|
// index to keep track of where we're appending trip items.
|
|
var current_entry = destination_entry.?;
|
|
var i: u8 = 0;
|
|
|
|
// At the end of each looping, a continue expression increments
|
|
// our index. Can you see why we need to increment by two?
|
|
while (true) : (i += 2) {
|
|
trip[i] = TripItem{ .place = current_entry.place };
|
|
|
|
// An entry "coming from" nowhere means we've reached the
|
|
// start, so we're done.
|
|
if (current_entry.coming_from == null) break;
|
|
|
|
// Otherwise, entries have a path.
|
|
trip[i + 1] = TripItem{ .path = current_entry.via_path.? };
|
|
|
|
// Now we follow the entry we're "coming from". If we
|
|
// aren't able to find the entry we're "coming from" by
|
|
// Place, something has gone horribly wrong with our
|
|
// program! (This really shouldn't ever happen. Have you
|
|
// checked for grues?)
|
|
// Note: you do not need to fix anything here.
|
|
const previous_entry = self.getEntry(current_entry.coming_from.?);
|
|
if (previous_entry == null) return TripError.EatenByAGrue;
|
|
current_entry = previous_entry.?;
|
|
}
|
|
}
|
|
};
|
|
|
|
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
|
|
|
|
// Store each Path array as a slice in each Place. As mentioned
|
|
// above, we needed to delay making these references to avoid
|
|
// creating a dependency loop when the compiler is trying to
|
|
// figure out how to allocate space for each item.
|
|
a.paths = a_paths[0..];
|
|
b.paths = b_paths[0..];
|
|
c.paths = c_paths[0..];
|
|
d.paths = d_paths[0..];
|
|
e.paths = e_paths[0..];
|
|
f.paths = f_paths[0..];
|
|
|
|
// Now we create an instance of the notebook and add the first
|
|
// "start" entry. Note the null values. Read the comments for the
|
|
// checkNote() method above to see how this entry gets added to
|
|
// the notebook.
|
|
var notebook = HermitsNotebook{};
|
|
var working_note = NotebookEntry{
|
|
.place = start,
|
|
.coming_from = null,
|
|
.via_path = null,
|
|
.dist_to_reach = 0,
|
|
};
|
|
notebook.checkNote(working_note);
|
|
|
|
// Get the next entry from the notebook (the first being the
|
|
// "start" entry we just added) until we run out, at which point
|
|
// we'll have checked every reachable Place.
|
|
while (notebook.hasNextEntry()) {
|
|
var place_entry = notebook.getNextEntry();
|
|
|
|
// For every Path that leads FROM the current Place, create a
|
|
// new note (in the form of a NotebookEntry) with the
|
|
// destination Place and the total distance from the start to
|
|
// reach that place. Again, read the comments for the
|
|
// checkNote() method to see how this works.
|
|
for (place_entry.place.paths) |*path| {
|
|
working_note = NotebookEntry{
|
|
.place = path.to,
|
|
.coming_from = place_entry.place,
|
|
.via_path = path,
|
|
.dist_to_reach = place_entry.dist_to_reach + path.dist,
|
|
};
|
|
notebook.checkNote(working_note);
|
|
}
|
|
}
|
|
|
|
// Once the loop above is complete, we've calculated the shortest
|
|
// path to every reachable Place! What we need to do now is set
|
|
// aside memory for the trip and have the hermit's notebook fill
|
|
// in the trip from the destination back to the path. Note that
|
|
// this is the first time we've actually used the destination!
|
|
var trip = [_]?TripItem{null} ** (place_count * 2);
|
|
|
|
notebook.getTripTo(trip[0..], destination) catch |err| {
|
|
print("Oh no! {}\n", .{err});
|
|
return;
|
|
};
|
|
|
|
// Print the trip with a little helper function below.
|
|
printTrip(trip[0..]);
|
|
}
|
|
|
|
// Remember that trips will be a series of alternating TripItems
|
|
// containing a Place or Path from the destination back to the start.
|
|
// The remaining space in the trip array will contain null values, so
|
|
// we need to loop through the items in reverse, skipping nulls, until
|
|
// we reach the destination at the front of the array.
|
|
fn printTrip(trip: []?TripItem) void {
|
|
// We convert the usize length to a u8 with @intCast(), a
|
|
// builtin function just like @import(). We'll learn about
|
|
// these properly in a later exercise.
|
|
var i: u8 = @intCast(u8, trip.len);
|
|
|
|
while (i > 0) {
|
|
i -= 1;
|
|
if (trip[i] == null) continue;
|
|
trip[i].?.printMe();
|
|
}
|
|
|
|
print("\n", .{});
|
|
}
|
|
|
|
// Going deeper:
|
|
//
|
|
// In computer science terms, our map places are "nodes" or "vertices" and
|
|
// the paths are "edges". Together, they form a "weighted, directed
|
|
// graph". It is "weighted" because each path has a distance (also
|
|
// known as a "cost"). It is "directed" because each path goes FROM
|
|
// one place TO another place (undirected graphs allow you to travel
|
|
// on an edge in either direction).
|
|
//
|
|
// Since we append new notebook entries at the end of the list and
|
|
// then explore each sequentially from the beginning (like a "todo"
|
|
// list), we are treating the notebook as a "First In, First Out"
|
|
// (FIFO) queue.
|
|
//
|
|
// Since we examine all closest paths first before trying further ones
|
|
// (thanks to the "todo" queue), we are performing a "Breadth-First
|
|
// Search" (BFS).
|
|
//
|
|
// By tracking "lowest cost" paths, we can also say that we're
|
|
// performing a "least-cost search".
|
|
//
|
|
// Even more specifically, the Hermit's Notebook most closely
|
|
// resembles the Shortest Path Faster Algorithm (SPFA), attributed to
|
|
// Edward F. Moore. By replacing our simple FIFO queue with a
|
|
// "priority queue", we would basically have Dijkstra's algorithm. A
|
|
// priority queue retrieves items sorted by "weight" (in our case, it
|
|
// would keep the paths with the shortest distance at the front of the
|
|
// queue). Dijkstra's algorithm is more efficient because longer paths
|
|
// can be eliminated more quickly. (Work it out on paper to see why!)
|