Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc/classes.md improvements #610

Merged
merged 5 commits into from
Jan 14, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`:
Akuli marked this conversation as resolved.
Show resolved Hide resolved

```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++
Akuli marked this conversation as resolved.
Show resolved Hide resolved
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`:
Akuli marked this conversation as resolved.
Show resolved Hide resolved

```python
```python
Akuli marked this conversation as resolved.
Show resolved Hide resolved
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.
Akuli marked this conversation as resolved.
Show resolved Hide resolved
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
Akuli marked this conversation as resolved.
Show resolved Hide resolved

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.
Loading