Skip to content

Commit

Permalink
doc/classes.md: explain instantiation better
Browse files Browse the repository at this point in the history
  • Loading branch information
Akuli committed Jan 13, 2025
1 parent 0206836 commit 2aaad8a
Showing 1 changed file with 71 additions and 59 deletions.
130 changes: 71 additions & 59 deletions doc/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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]
Expand All @@ -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.

0 comments on commit 2aaad8a

Please sign in to comment.