diff --git a/doc/classes.md b/doc/classes.md index e5ae04a1..db6bc1a6 100644 --- a/doc/classes.md +++ b/doc/classes.md @@ -35,8 +35,18 @@ In reality, instances may be bigger than expected due to [padding](https://stackoverflow.com/questions/4306186/structure-padding-and-packing), but this can be almost always ignored. -You can use e.g. `Point{x=12, y=34}` to instantiate the class, -and the usual `.` syntax to access its fields: +To create an instance of `Point`, +we simply need enough memory to hold the two `int`s, +and we need to tell the compiler to treat it as a `Point` instance. +In other words, we simply create a variable whose type is `Point`: + +```python +p: Point +``` + +Because the memory is [uninitialized](tutorial.md#undefined-behavior-ub), +we still need to assign values to the fields, which we can access as `p.x` and `p.y`. +Like this: ```python import "stdlib/io.jou" @@ -46,30 +56,26 @@ class Point: y: int def main() -> int: - p = Point{x=12, y=34} + p: Point + p.x = 12 + p.y = 34 printf("%d, %d\n", p.x, p.y) # Output: 12, 34 p.y++ printf("%d, %d\n", p.x, p.y) # Output: 12, 35 return 0 ``` -This does not allocate any heap memory (TODO: document heap allocations). -In fact, it's basically same as creating two variables `x` and `y` in the `main()` function: +Alternatively, we could create the instance in one line with `p = Point{x = 12, y = 34}`. +This syntax is explained in detail [below](#instantiating). -```python -import "stdlib/io.jou" -def main() -> int: - x = 12 - y = 34 - printf("%d, %d\n", x, y) # Output: 12, 34 - return 0 -``` +## Pointers -This means that if you pass an instance of a class to a function, you get a copy, -as if you had just passed the two integers: +Instances of classes are often passed around as pointers. +To understand why, let's try to make a function +that increments the `x` coordinate of a `Point`: -```python + ```python import "stdlib/io.jou" class Point: @@ -86,6 +92,27 @@ def main() -> int: return 0 ``` +The problem is that when we do `increment_y()`, +we simply pass the 64 (or more) bytes of the struct to the `increment_y()` method. +This is very similar to creating two variables `x` and `y` in the `main()` function: + +```python +import "stdlib/io.jou" + +def increment_y(x: int, y: int) -> None: + y++ # Doesn't work as expected + +def main() -> int: + x = 12 + y = 34 + increment_y(x, y) + printf("%d, %d\n", x, y) # Output: 12, 34 + return 0 +``` + +In either case, the `increment_y()` function gets a copy of the coordinates, +so `instance.y++` or `y++` only increments the `y` coordinate of the copy. + For this reason, instances of classes are often passed around as [pointers](tutorial.md#pointers). This way the `increment_y()` function knows where the original instance is in the computer's memory, so that it can place the new value there instead of its own copy of the instance. @@ -110,6 +137,9 @@ def main() -> int: Here `ptr->y` does the same thing as `(*ptr).y`: it accesses the `y` member of the instance located wherever the pointer `ptr` is pointing. +This arrow syntax feels weird at first, +but it's convenient once you get used to it, +and the C programming language uses the same syntax. ## Methods @@ -181,8 +211,13 @@ so `self` is not a pointer. ## Instantiating -As we have seen, the instantiating syntax is `ClassName{field=value}`. -The curly braces are used to distinguish instantiating from function calls. +As we have seen, "instantiating" simply means taking a chunk of memory of the correct size, +but it's often done with the `ClassName{field=value}` syntax. +Let's look at this syntax in more detail. + +The curly braces are used to distinguish instantiating syntax from function calls. +This makes the Jou compiler simpler, but if you don't like this syntax, +feel free to [create an issue](https://github,com/Akuli/jou/issues/new) to discuss it. If you omit some class fields, they will be initialized to zero. Specifically, the memory used for the fields will be all zero bytes. @@ -222,38 +257,23 @@ def main() -> int: You can achieve the same thing by setting the memory used by the instance to zero bytes. This is often done with the `memset()` function from [stdlib/mem.jou](../stdlib/mem.jou). It takes in three parameters, so that `memset(ptr, 0, n)` sets `n` bytes starting at pointer `ptr` to zero. -To calculate the correct `n`, you can use the `sizeof` operator (TODO: document sizeof): -```python -import "stdlib/io.jou" -import "stdlib/mem.jou" - - -class Person: - name: byte* - country: byte[50] +To calculate the correct `n`, you can use `sizeof(instance)`, where `instance` is any instance of the class. +It doesn't matter which instance you use, because all instances of the class are of the same size. +In general, the value of `sizeof(x)` only depends on the type of `x`, +and it doesn't even evaluate `x` when the program runs. - def introduce(self) -> None: - if self->name == NULL: - printf("I'm an anonymous person from '%s'\n", self->country) - else: - printf("I'm %s from '%s'\n", self->name, self->country) - - -def main() -> int: - akuli = Person{name="Akuli", country="Finland"} - memset(&akuli, 0, sizeof(akuli)) - akuli.introduce() # Output: I'm an anonymous person from '' - return 0 -``` - -This works the same way if you have multiple instances next to each other in memory, -such as in an array, and you want to zero-initialize all of them: +For example, the following program creates an array of three uninitialized instances of `Person`, +and then zero-initializes all of them using `memset()`. +Array elements are simply next to each other in memory, +so it's enough to do one `memset()` that is big enough to set all of them to zero. +Like this: ```python import "stdlib/io.jou" import "stdlib/mem.jou" + class Person: name: byte* country: byte[50] @@ -264,29 +284,21 @@ class Person: else: printf("I'm %s from '%s'\n", self->name, self->country) -def main() -> int: - contributors = [ - Person{name="Akuli", country="Finland"}, - Person{name="littlewhitecloud", country="China"}, - Person{name="Moosems", country="USA"}, - ] - - # Output: I'm Akuli from 'Finland' - # Output: I'm littlewhitecloud from 'China' - # Output: I'm Moosems from 'USA' - for i = 0; i < 3; i++: - contributors[i].introduce() - memset(&contributors, 0, sizeof(contributors)) +def main() -> int: + people: Person[3] + memset(&people, 0, sizeof(people[0]) * 3) # Output: I'm an anonymous person from '' # Output: I'm an anonymous person from '' # Output: I'm an anonymous person from '' for i = 0; i < 3; i++: - contributors[i].introduce() + people[i].introduce() return 0 ``` -Here `sizeof(contributors)` is the size of the entire array in bytes, -which is 3 times the size of a `Person`. +Instead of `sizeof(people[0]) * 3`, you could just as well use `sizeof(people)`. +The size of an array of 3 elements is simply 3 times the size of one element. +You could also use `people = [Person{}, Person{}, Person{}]` to create and zero-initialize the array, +but this becomes annoying if the array contains many instances.