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
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 5 additions & 1 deletion check-markdown-links.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@ def check_link(markdown_file_path, link_target, offline_mode=False):
if "\\" in link_target:
return "use forward slashes instead of backslashes"

path = markdown_file_path.parent / link_target.split("#")[0]
if link_target.startswith('#'):
# Link to within same file
path = markdown_file_path
else:
path = markdown_file_path.parent / link_target.split("#")[0]

if PROJECT_ROOT not in path.resolve().parents:
return "link points outside of the Jou project folder"
Expand Down
131 changes: 72 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 to take enough memory to hold the two `int`s,
and we need to tell the compiler to treat that memory as a `Point` instance.
In other words, we 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,28 +56,25 @@ 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-syntax).

```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 `y` coordinate of a `Point`:

```python
import "stdlib/io.jou"
Expand All @@ -86,6 +93,27 @@ def main() -> int:
return 0
```

The problem is that when we do `increment_y(p)`,
we simply pass the 64 (or more) bytes of the instance `p` to the `increment_y()` function.
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 +138,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 @@ -179,10 +210,15 @@ This means that the type of `self` is `Point`, not `Point*` (the default),
so `self` is not a pointer.


## Instantiating
## Instantiating Syntax

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 +258,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 +285,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