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

Allow x, y: int syntax for declaring local variables #540

Merged
merged 14 commits into from
Dec 31, 2024
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
Loading