Skip to content

Commit

Permalink
Merge pull request #65 from pedropark99/revision-16
Browse files Browse the repository at this point in the history
Add revision for chapter 16 and fix issue
  • Loading branch information
pedropark99 authored Oct 10, 2024
2 parents 417c906 + c883060 commit 3a02942
Show file tree
Hide file tree
Showing 50 changed files with 669 additions and 598 deletions.
12 changes: 6 additions & 6 deletions Chapters/01-base64.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ the base64 encoding system. We want to produce the sequence of base64 characters
original message in the base64 encoding system.

In contrast, "decode" represents the inverse process.
We want to decode, or, in other words, translate a base64 message back to it's original content.
We want to decode, or, in other words, translate a base64 message back to its original content.
So, in this process we get a sequence of base64 characters as input, and produce as output,
the binary data that is represented by this sequence of base64 characters.

Expand All @@ -251,13 +251,13 @@ that converts a sequence of base64 characters back into the original sequence of

One task that we need to do is to calculate how much space we need to reserve for the
output, both of the encoder and decoder. This is simple math, and can be done easily in Zig
because every array have it's length (it's number of elements) easily accesible by consulting
because every array have its length (its number of elements) easily accesible by consulting
the `.len` property of the array.

For the encoder, the logic is the following: for each 3 bytes that we find in the input,
4 new bytes are created in the output. So, we take the number of bytes in the input, divide it
by 3, use a ceiling function, then, we multiply the result by 4. That way, we get the total
number of bytes that will be produced by the encoder in it's output.
number of bytes that will be produced by the encoder in its output.

The `_calc_encode_length()` function below encapsulates this logic.
Inside this function, we take the length of the input array,
Expand Down Expand Up @@ -469,10 +469,10 @@ bits from the first and second bytes in the input string. But how
can we do that? The answer relies on the *bitwise and* (`&`) operator.

The @fig-encoder-bitshift already showed you what effect this `&` operator
produces in the bits of it's operands. But let's make a clear description of it.
produces in the bits of its operands. But let's make a clear description of it.

In summary, the `&` operator performs a logical conjunction operation
between the bits of it's operands. In more details, the operator `&`
between the bits of its operands. In more details, the operator `&`
compares each bit of the first operand to the corresponding bit of the second operand.
If both bits are 1, the corresponding result bit is set to 1.
Otherwise, the corresponding result bit is set to 0 [@microsoftbitwiseand].
Expand Down Expand Up @@ -559,7 +559,7 @@ just for brevity reasons. So, just remember that this function is a public funct
Furthermore, this `encode()` function have two other arguments:

1. `input` is the input sequence of characters that you want to encode in base64;
2. `allocator` is an allocator object to use in the necessary memory allocations.
1. `allocator` is an allocator object to use in the necessary memory allocations.

I described everything you need to know about allocator objects at @sec-allocators.
So, if you are not familiar with them, I highly recommend you to comeback to
Expand Down
35 changes: 15 additions & 20 deletions Chapters/01-memory.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Knowing the length (or the size) of each object is also important. So the length
in some cases, *known at compile time*.

The `zig` compiler cares more about knowing the length (or the size) of a particular object
, than to know it's actual value. But, if the `zig` compiler knows the value of the object, then, it
, than to know its actual value. But, if the `zig` compiler knows the value of the object, then, it
automatically knows the size of this object. Because it can simply calculate the
size of the object by looking at the size of the value.

Expand All @@ -84,7 +84,7 @@ but this array have a known fixed size, like `[60]u8` (which declares an array o
this type, or, this struct that you are declaring, becomes a type with a known fixed size at compile-time.
And because of that, in this case, the `zig` compiler do not need to known at compile-time the exact value of
any object of this type. Since the compiler can find the necessary size to store this object by
looking at the size of it's type.
looking at the size of its type.


Let's look at an example. In the source code below, we have two constant objects (`name` and `array`) declared.
Expand Down Expand Up @@ -118,14 +118,14 @@ For example, the function `input_length()` contains an argument named `input`, w
Is impossible to know at compile time the value of this particular argument. And it also is impossible to know the size/length
of this particular argument. Because it is an array that do not have a fixed size specified explicitly in the argument type annotation.

So, we know that this `input` argument will be an array of `u8` integers. But we do not know at compile-time, it's value, and neither his size.
So, we know that this `input` argument will be an array of `u8` integers. But we do not know at compile-time, its value, and neither his size.
This information is known only at runtime, which is the period of time when you program is executed.
As a consequence, the value of the expression `input.len` is also known only at runtime.
This is an intrinsic characteristic of any function. Just remember that the value of function arguments is usually not "compile-time known".

However, as I mentioned earlier, what really matters to the compiler is to know the size of the object
at compile-time, and not necessarily it's value. So, although we don't know the value of the object `n`, which is the result of the expression
`input.len`, at compile-time, we do know it's size. Because the expression `input.len` always return a value of type `usize`,
at compile-time, and not necessarily its value. So, although we don't know the value of the object `n`, which is the result of the expression
`input.len`, at compile-time, we do know its size. Because the expression `input.len` always return a value of type `usize`,
and the type `usize` have a known fixed size.


Expand Down Expand Up @@ -228,7 +228,7 @@ Because they will be automatically destroyed once the stack space is freed at th
So, once the function call returns (or ends, if you prefer to call it this way)
the space that was reserved in the stack is destroyed, and all of the objects that were in that space goes away with it.
This mechanism exists because this space, and the objects within it, are not necessary anymore,
since the function "finished it's business".
since the function "finished its business".
Using the `add()` function that we exposed above as an example, it means that the object `result` is automatically
destroyed once the function returns.

Expand All @@ -238,7 +238,7 @@ freed/destroyed at the end of the function scope.
:::


This same logic applies to any other special structure in Zig that have it's own scope by surrounding
This same logic applies to any other special structure in Zig that have its own scope by surrounding
it with curly braces (`{}`).
For loops, while loops, if else statements, etc. For example, if you declare any local
object in the scope of a for loop, this local object is accessible only within the scope
Expand Down Expand Up @@ -319,14 +319,14 @@ in the program.
If a local object in your function is stored in the stack, you should never
return a pointer to this local object from the function. Because
this pointer will always become undefined after the function returns, since the stack space of the function
is destroyed at the end of it's scope.
is destroyed at the end of its scope.
:::

But what if you really need to use this local object in some way after your function returns?
How can you do this? The answer is: "in the same you would do if this was a C or C++ program. By returning
an address to an object stored in the heap". The heap memory have a much more flexible lifecycle,
and allows you to get a valid pointer to a local object of a function that already returned
from it's scope.
from its scope.


### Heap {#sec-heap}
Expand All @@ -344,7 +344,7 @@ and that serves (or "deals with") any incoming request that reaches this particu
The heap is a good choice for this type of system, mainly because the server does not know upfront
how many requests it will receive from users, while it is active. It could be one single request,
or, 5 thousand requests, or, it could also be zero requests.
The server needs to have the ability to allocate and manage it's memory according to how many requests it receives.
The server needs to have the ability to allocate and manage its memory according to how many requests it receives.

Another key difference between the stack and the heap, is that the heap is a type
of memory that you, the programmer, have complete control over. This makes the heap a
Expand Down Expand Up @@ -379,13 +379,9 @@ In summary, the Zig compiler will use the following rules to decide where each
object you declare is stored:

1. every literal value (such as `"this is string"`, `10`, or `true`) is stored in the global data section.

1. every constant object (`const`) whose value **is known at compile-time** is also stored in the global data section.

1. every object (constant or not) whose length/size **is known at compile time** is stored in the stack space for the current scope.

1. if an object is created with the method `alloc()` or `create()` of an allocator object, this object is stored in the memory space used by this particular allocator object. Most of allocators available in Zig use the heap memory, so, this object is likely stored in the heap (`FixedBufferAllocator()` is an exception to that).

1. the heap can only be accessed through allocators. If your object was not created through the `alloc()` or `create()` methods of an allocator object, then, he is most certainly not an object stored in the heap.


Expand All @@ -400,13 +396,13 @@ source code of these operators, and find the memory allocation calls.
Many programmers find this behaviour annoying and hard to keep track of.

But, in Zig, if a function, an operator, or anything from the standard library
needs to allocate some memory during it's execution, then, this function/operator needs to receive (as input) an allocator
needs to allocate some memory during its execution, then, this function/operator needs to receive (as input) an allocator
provided by the user, to actually be able to allocate the memory it needs.

This creates a clear distinction between functions that "do not" from those that "actually do"
allocate memory. Just look at the arguments of this function.
If a function, or operator, have an allocator object as one of it's inputs/arguments, then, you know for
sure that this function/operator will allocate some memory during it's execution.
If a function, or operator, have an allocator object as one of its inputs/arguments, then, you know for
sure that this function/operator will allocate some memory during its execution.

An example is the `allocPrint()` function from the Zig standard library. With this function, you can
write a new string using format specifiers. So, this function is, for example, very similar to the function `sprintf()` in C.
Expand Down Expand Up @@ -464,8 +460,7 @@ known fixed length.
But in reality, there are two very common instances where this "fixed length limitation" of the stack is a deal braker:

1. the objects that you create inside your function might grow in size during the execution of the function.

2. sometimes, it is impossible to know upfront how many inputs you will receive, or how big this input will be.
1. sometimes, it is impossible to know upfront how many inputs you will receive, or how big this input will be.

Also, there is another instance where you might want to use an allocator, which is when you want to write a function that returns a pointer
to a local object. As I described at @sec-stack, you cannot do that if this local object is stored in the
Expand Down Expand Up @@ -499,7 +494,7 @@ allocators available in the standard library:
- `c_allocator()` (requires you to link to libc).


Each allocator have it's own perks and limitations. All allocators, except `FixedBufferAllocator()` and `ArenaAllocator()`,
Each allocator have its own perks and limitations. All allocators, except `FixedBufferAllocator()` and `ArenaAllocator()`,
are allocators that use the heap memory. So any memory that you allocate with
these allocators, will be placed in the heap.

Expand Down
29 changes: 17 additions & 12 deletions Chapters/01-zig-weird.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ knitr::opts_chunk$set(

In this chapter, I want to introduce you to the world of Zig.
Zig is a very young language that is being actively developed.
As a consequence, it's world is still very wild and to be explored.
As a consequence, its world is still very wild and to be explored.
This book is my attempt to help you on your personal journey for
understanding and exploring the exciting world of Zig.

Expand Down Expand Up @@ -203,12 +203,11 @@ In this `root.zig` module, we are declaring a function called `add()`, which has
The function returns an integer of the type `i32` as result.


Zig is not exactly a strongly-typed language. Because you can (if you want to) omit
the type of an object in your code, if this type can be derived from the assigned value.
But there are other situations where you do need to be explicit.
Zig is a strongly-typed language. There are some specific situations where you can (if you want to) omit
the type of an object in your code, if this type can be inferred by the `zig` compiler (we talk more
about that at @sec-type-inference). But there are other situations where you do need to be explicit.
For example, you do have to explicitly specify the type of each function argument, and also,
the return type of every function you create in Zig. So, at least in function declarations,
Zig is a strongly-typed language.
the return type of every function that you create in Zig.

We specify the type of an object or a function argument in Zig by
using a colon character (`:`) followed by the type after the name of this object/function argument.
Expand Down Expand Up @@ -633,7 +632,7 @@ t.zig:4:11: error: unused local constant
Everytime you declare a new object in Zig, you have two choices:

1. you either use the value of this object;
2. or you explicitly discard the value of the object;
1. or you explicitly discard the value of the object;

To explicitly discard the value of any object (constant or variable), all you need to do is to assign
this object to an special character in Zig, which is the underscore (`_`).
Expand Down Expand Up @@ -1001,13 +1000,13 @@ your source code. In less technical terms, blocks are used to specify where in y
you can access whatever object you have in your source code.

So, a block is just a group of expressions contained inside a pair of curly braces.
And every block have it's own scope separated from the others.
And every block have its own scope separated from the others.
The body of a function is a classic example of a block. If statements, for and while loops
(and any other structure in the language that uses the pair of curly braces)
are also examples of blocks.

This means that, every if statement, or for loop,
etc., that you create in your source code have it's own separate scope.
etc., that you create in your source code have its own separate scope.
That is why you can't access the objects that you defined inside
of your for loop (or if statement) in an outer scope, i.e. a scope outside of the for loop.
Because you are trying to access an object that belongs to a scope that is different
Expand Down Expand Up @@ -1131,7 +1130,8 @@ where exactly the array ends, or, in other words, to find how much elements the
To do that, you would need something like this in C. In this example, the C string stored in
the object `array` is 25 bytes long:

```c
```{c}
#| eval: false
#include <stdio.h>
int main() {
char* array = "An example of string in C";
Expand Down Expand Up @@ -1200,7 +1200,10 @@ const stdout = std.io.getStdOut().writer();
pub fn main() !void {
const string_object = "This is an example of string literal in Zig";
const simple_array = [_]i32{1, 2, 3, 4};
try stdout.print("Type of array object: {}", .{@TypeOf(simple_array)});
try stdout.print(
"Type of array object: {}",
.{@TypeOf(simple_array)}
);
try stdout.print(
"Type of string object: {}",
.{@TypeOf(string_object)}
Expand Down Expand Up @@ -1242,7 +1245,9 @@ const std = @import("std");
const stdout = std.io.getStdOut().writer();
pub fn main() !void {
const string_object = "Ⱥ";
try stdout.print("Bytes that represents the string object: ", .{});
_ = try stdout.write(
"Bytes that represents the string object: "
);
for (string_object) |char| {
try stdout.print("{X} ", .{char});
}
Expand Down
Loading

0 comments on commit 3a02942

Please sign in to comment.