Skip to content

Commit

Permalink
Allow x, y: int syntax for declaring local variables (#540)
Browse files Browse the repository at this point in the history
  • Loading branch information
Akuli authored Dec 31, 2024
1 parent 703ad2e commit e638fcd
Show file tree
Hide file tree
Showing 21 changed files with 121 additions and 57 deletions.
16 changes: 7 additions & 9 deletions doc/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,17 +308,16 @@ def get_point(x: int*, y: int*) -> None:
*y = 456

def main() -> int:
x: int
y: int
x, y: int
get_point(&x, &y)
printf("The point is (%d,%d)\n", x, y) # Output: The point is (123,456)
return 0
```

Here `x: int` creates an integer variable `x` without assigning a value to it.
This means that we leave 4 bytes of the computer's memory unused for now.
Here `x, y: int` creates two integer variables without assigning values to them.
This means that we leave 8 bytes (4 bytes for both) of the computer's memory unused for now.
We then pass the location of that memory to `get_point()`,
so that it can write to that memory, i.e. set the value of the `x` variable.
so that it can write to that memory, i.e. set the values of the `x` and `y` variables.

Instead of pointers, you could also use an `int[2]` array to return the two values:

Expand Down Expand Up @@ -366,15 +365,14 @@ def get_point(x: int*, y: int*) -> None:
*y = 456

def main() -> int:
x: int
y: int
x, y: int
get_point(&x, &y)
printf("The point is (%d,%d)\n", x, y) # Output: The point is (123,456)
return 0
```

Here `x: int` creates a variable of type `int` without assigning a value to it.
If you try to use the value of `x` before it is set,
Here `x, y: int` creates two variables of type `int` without assigning values to them.
If you try to use the value of `x` or `y` before they are set,
you will most likely get a compiler warning together with a random garbage value when the program runs.
For example, if I delete the `get_point(&x, &y)` line, I get:

Expand Down
3 changes: 1 addition & 2 deletions examples/aoc2023/day03/part2.jou
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ def find_adjacent_numbers(input: byte*, p: byte*) -> int[10]:
for dx = -1; dx <= 1; dx++:
neighbor = &p[dx + line_size*dy]
if input <= neighbor and neighbor < &input[strlen(input)] and is_ascii_digit(*neighbor):
start: byte*
end: byte*
start, end: byte*
find_whole_number(input, neighbor, &start, &end)
if start != last_start or end != last_end:
last_start = start
Expand Down
4 changes: 1 addition & 3 deletions examples/aoc2023/day05/part1.jou
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ class Map:
ntriples: int

def add_triple(self, s: byte*) -> None:
a: long
b: long
c: long
a, b, c: long
assert sscanf(s, "%lld %lld %lld", &a, &b, &c) == 3

assert self->ntriples < sizeof(self->triples)/sizeof(self->triples[0])
Expand Down
4 changes: 1 addition & 3 deletions examples/aoc2023/day05/part2.jou
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ class Map:
ntriples: int

def add_triple(self, s: byte*) -> None:
a: long
b: long
c: long
a, b, c: long
assert sscanf(s, "%lld %lld %lld", &a, &b, &c) == 3

assert self->ntriples < sizeof(self->triples)/sizeof(self->triples[0])
Expand Down
7 changes: 1 addition & 6 deletions examples/aoc2023/day24/part1.jou
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,7 @@ def main() -> int:

r: Ray

x: long
y: long
z: long
dx: long
dy: long
dz: long
x, y, z, dx, dy, dz: long
while fscanf(f, "%lld, %lld, %lld @ %lld, %lld, %lld\n", &x, &y, &z, &dx, &dy, &dz) == 6:
assert nrays < sizeof(rays)/sizeof(rays[0])
rays[nrays++] = Ray{start = [x,y], dir = [dx,dy]}
Expand Down
7 changes: 1 addition & 6 deletions examples/aoc2023/day24/part2.jou
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,7 @@ def read_input(N: int) -> MovingPoint*:

result: MovingPoint* = malloc(N * sizeof(result[0]))
for i = 0; i < N; i++:
x: long
y: long
z: long
dx: long
dy: long
dz: long
x, y, z, dx, dy, dz: long
assert fscanf(f, "%lld, %lld, %lld @ %lld, %lld, %lld\n", &x, &y, &z, &dx, &dy, &dz) == 6
result[i] = MovingPoint{start = [x,y,z], speed = [dx,dy,dz]}

Expand Down
3 changes: 1 addition & 2 deletions examples/aoc2024/day01/part1.jou
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ def main() -> int:
f = fopen("sampleinput.txt", "r")
assert f != NULL

left_column: int[2000]
right_column: int[2000]
left_column, right_column: int[2000]
n = 0
while fscanf(f, "%d %d", &left_column[n], &right_column[n]) == 2:
n++
Expand Down
8 changes: 1 addition & 7 deletions examples/aoc2024/day13/part1.jou
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,7 @@ def main() -> int:
assert f != NULL

total_price = 0

ax: int
ay: int
bx: int
by: int
px: int
py: int
ax, ay, bx, by, px, py: int

while True:
ret = fscanf(f, "Button A: X+%d, Y+%d\n", &ax, &ay)
Expand Down
8 changes: 1 addition & 7 deletions examples/aoc2024/day13/part2.jou
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,7 @@ def main() -> int:
assert f != NULL

total_price = 0L

ax: long
ay: long
bx: long
by: long
px: long
py: long
ax, ay, bx, by, px, py: long

while True:
ret = fscanf(f, "Button A: X+%lld, Y+%lld\n", &ax, &ay)
Expand Down
5 changes: 1 addition & 4 deletions examples/aoc2024/day14/part1.jou
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ def main() -> int:

qcounts: int[5] = [0, 0, 0, 0, 0]

px: int
py: int
vx: int
vy: int
px, py, vx, vy: int
while fscanf(f, "p=%d,%d v=%d,%d\n", &px, &py, &vx, &vy) == 4:
x = (px + 100*vx) % width
y = (py + 100*vy) % height
Expand Down
5 changes: 1 addition & 4 deletions examples/aoc2024/day14/part2.jou
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ def main() -> int:
assert robots != NULL
nrobots = 0

px: int
py: int
vx: int
vy: int
px, py, vx, vy: int
while fscanf(f, "p=%d,%d v=%d,%d\n", &px, &py, &vx, &vy) == 4:
assert nrobots < max_robots
robots[nrobots++] = Robot{
Expand Down
3 changes: 1 addition & 2 deletions examples/aoc2024/day18/part2.jou
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ def main() -> int:
assert f != NULL

memset(&grid, '.', sizeof(grid))
x_in: int
y_in: int
x_in, y_in: int
while fscanf(f, "%d,%d\n", &x_in, &y_in) == 2:
assert 0 <= x_in and x_in < size
assert 0 <= y_in and y_in < size
Expand Down
49 changes: 49 additions & 0 deletions self_hosted/parser.jou
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,44 @@ class Parser:
body = self->parse_body(),
}

# Parses the "x: int" part of "x, y, z: int", leaving "y, z: int" to be parsed later.
def parse_first_of_multiple_local_var_declares(self) -> AstNameTypeValue:
assert self->tokens->kind == TokenKind::Name

ntv = AstNameTypeValue{
name = self->tokens->short_string,
name_location = self->tokens->location,
}

# Take a backup of the parser where first variable name and its comma are consumed.
save_state = *self
save_state.tokens = &save_state.tokens[2]

# Skip variables and commas so we can parse the type that comes after it
self->tokens++
while self->tokens->is_operator(",") and self->tokens[1].kind == TokenKind::Name:
self->tokens = &self->tokens[2]

# Error for "x, y = 0"
if self->tokens->is_operator("="):
fail(self->tokens->location, "only one variable can be assigned at a time")

if self->tokens->is_operator("="):
fail(self->tokens->location, "only one variable can be assigned at a time")

if not self->tokens->is_operator(":"):
self->tokens->fail_expected_got("':' and a type after it (example: \"foo, bar: int\")")
self->tokens++

ntv.type = self->parse_type()

# Error for "x, y: int = 0"
if self->tokens->is_operator("="):
fail(self->tokens->location, "only one variable can be assigned at a time")

*self = save_state
return ntv

def parse_statement(self) -> AstStatement:
if self->tokens->is_keyword("import"):
fail(self->tokens->location, "imports must be in the beginning of the file")
Expand Down Expand Up @@ -924,6 +962,17 @@ class Parser:
while_loop = self->parse_while_loop(),
}

if (
self->tokens[0].kind == TokenKind::Name
and self->tokens[1].is_operator(",")
and self->tokens[2].kind == TokenKind::Name
):
return AstStatement{
location = self->tokens->location,
kind = AstStatementKind::DeclareLocalVar,
var_declaration = self->parse_first_of_multiple_local_var_declares(),
}

result = self->parse_oneline_statement()
self->eat_newline()
return result
Expand Down
39 changes: 39 additions & 0 deletions src/parse.c
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,42 @@ static AstEnumDef parse_enumdef(ParserState *ps)
return result;
}

// Parses the "x: int" part of "x, y, z: int", leaving "y, z: int" to be parsed later.
static AstNameTypeValue parse_first_of_multiple_local_var_declares(ParserState *ps)
{
assert(ps->tokens->type == TOKEN_NAME);
assert(is_operator(&ps->tokens[1], ","));

AstNameTypeValue ntv = { .name_location = ps->tokens->location, .value = NULL };
safe_strcpy(ntv.name, ps->tokens->data.name);

// Take a backup of the parser where first variable name and its comma are consumed.
ParserState savestate = *ps;
savestate.tokens += 2;

// Skip variables and commas so we can parse the type that comes after it
ps->tokens++;
while (is_operator(ps->tokens, ",") && ps->tokens[1].type == TOKEN_NAME)
ps->tokens += 2;

// Error for "x, y = 0"
if (is_operator(ps->tokens, "="))
fail(ps->tokens->location, "only one variable can be assigned at a time");

if (!is_operator(ps->tokens, ":"))
fail_with_parse_error(ps->tokens, "':' and a type after it (example: \"foo, bar: int\")");
ps->tokens++;

ntv.type = parse_type(ps);

// Error for "x, y: int = 0"
if (is_operator(ps->tokens, "="))
fail(ps->tokens->location, "only one variable can be assigned at a time");

*ps = savestate;
return ntv;
}

static AstStatement parse_statement(ParserState *ps)
{
AstStatement result = { .location = ps->tokens->location };
Expand Down Expand Up @@ -1031,6 +1067,9 @@ static AstStatement parse_statement(ParserState *ps)
ps->tokens++;
*result.data.forloop.incr = parse_oneline_statement(ps);
result.data.forloop.body = parse_body(ps);
} else if (ps->tokens->type == TOKEN_NAME && is_operator(&ps->tokens[1], ",") && ps->tokens[2].type == TOKEN_NAME) {
result.kind = AST_STMT_DECLARE_LOCAL_VAR;
result.data.vardecl = parse_first_of_multiple_local_var_declares(ps);
} else {
result = parse_oneline_statement(ps);
eat_newline(ps);
Expand Down
3 changes: 1 addition & 2 deletions tests/should_succeed/sscanf.jou
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import "stdlib/io.jou"
import "stdlib/str.jou"

def calculate_sum(string: byte*) -> int:
x: int
y: int
x, y: int
sscanf(string, "%d + %d", &x, &y)
return x + y

Expand Down
2 changes: 2 additions & 0 deletions tests/syntax_error/double_assignment_comma_typed.jou
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def foo() -> None:
x, y: int = 7 # Error: only one variable can be assigned at a time
4 changes: 4 additions & 0 deletions tests/syntax_error/double_assignment_comma_untyped.jou
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def foo() -> None:
# Python's unpack syntax is not supported in Jou.
# If you think it should be supported, note that it conflicts with "x, y: int" syntax (see #536).
x, y = 0 # Error: only one variable can be assigned at a time
2 changes: 2 additions & 0 deletions tests/syntax_error/local_var_comma.jou
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def main() -> int:
x, = 0 # Error: not a valid statement
2 changes: 2 additions & 0 deletions tests/syntax_error/local_vars_comma.jou
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def main() -> int:
x, y, = 0 # Error: expected ':' and a type after it (example: "foo, bar: int"), got ','
2 changes: 2 additions & 0 deletions tests/syntax_error/local_vars_comma_assign.jou
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def main() -> int:
x, y, # Error: expected ':' and a type after it (example: "foo, bar: int"), got ','
2 changes: 2 additions & 0 deletions tests/syntax_error/python_style_unpack.jou
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def main() -> int:
x, y = "hi" # Error: only one variable can be assigned at a time

0 comments on commit e638fcd

Please sign in to comment.