diff --git a/doc/tutorial.md b/doc/tutorial.md index 20328032..4b991b8d 100644 --- a/doc/tutorial.md +++ b/doc/tutorial.md @@ -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: @@ -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: diff --git a/examples/aoc2023/day03/part2.jou b/examples/aoc2023/day03/part2.jou index c4853a2d..5fca6581 100644 --- a/examples/aoc2023/day03/part2.jou +++ b/examples/aoc2023/day03/part2.jou @@ -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 diff --git a/examples/aoc2023/day05/part1.jou b/examples/aoc2023/day05/part1.jou index 89e5a592..b94fac06 100644 --- a/examples/aoc2023/day05/part1.jou +++ b/examples/aoc2023/day05/part1.jou @@ -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]) diff --git a/examples/aoc2023/day05/part2.jou b/examples/aoc2023/day05/part2.jou index 746e30b8..ddde242c 100644 --- a/examples/aoc2023/day05/part2.jou +++ b/examples/aoc2023/day05/part2.jou @@ -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]) diff --git a/examples/aoc2023/day24/part1.jou b/examples/aoc2023/day24/part1.jou index 2ad745b9..2db4f62b 100644 --- a/examples/aoc2023/day24/part1.jou +++ b/examples/aoc2023/day24/part1.jou @@ -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]} diff --git a/examples/aoc2023/day24/part2.jou b/examples/aoc2023/day24/part2.jou index bbd41d4c..8fb596cc 100644 --- a/examples/aoc2023/day24/part2.jou +++ b/examples/aoc2023/day24/part2.jou @@ -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]} diff --git a/examples/aoc2024/day01/part1.jou b/examples/aoc2024/day01/part1.jou index cd6ff781..c2c1725f 100644 --- a/examples/aoc2024/day01/part1.jou +++ b/examples/aoc2024/day01/part1.jou @@ -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++ diff --git a/examples/aoc2024/day13/part1.jou b/examples/aoc2024/day13/part1.jou index 2f0c7806..896fe8ba 100644 --- a/examples/aoc2024/day13/part1.jou +++ b/examples/aoc2024/day13/part1.jou @@ -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) diff --git a/examples/aoc2024/day13/part2.jou b/examples/aoc2024/day13/part2.jou index 3c226524..bf8c67ed 100644 --- a/examples/aoc2024/day13/part2.jou +++ b/examples/aoc2024/day13/part2.jou @@ -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) diff --git a/examples/aoc2024/day14/part1.jou b/examples/aoc2024/day14/part1.jou index 3371edea..2eb21087 100644 --- a/examples/aoc2024/day14/part1.jou +++ b/examples/aoc2024/day14/part1.jou @@ -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 diff --git a/examples/aoc2024/day14/part2.jou b/examples/aoc2024/day14/part2.jou index 787bbc28..318bdccb 100644 --- a/examples/aoc2024/day14/part2.jou +++ b/examples/aoc2024/day14/part2.jou @@ -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{ diff --git a/examples/aoc2024/day18/part2.jou b/examples/aoc2024/day18/part2.jou index cfcf88f6..b8b6539d 100644 --- a/examples/aoc2024/day18/part2.jou +++ b/examples/aoc2024/day18/part2.jou @@ -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 diff --git a/self_hosted/parser.jou b/self_hosted/parser.jou index f9eadf5c..e164cf79 100644 --- a/self_hosted/parser.jou +++ b/self_hosted/parser.jou @@ -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") @@ -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 diff --git a/src/parse.c b/src/parse.c index a821f33a..08e15a6c 100644 --- a/src/parse.c +++ b/src/parse.c @@ -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 }; @@ -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); diff --git a/tests/should_succeed/sscanf.jou b/tests/should_succeed/sscanf.jou index ab8f4dc4..1bdb91c5 100644 --- a/tests/should_succeed/sscanf.jou +++ b/tests/should_succeed/sscanf.jou @@ -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 diff --git a/tests/syntax_error/double_assignment_comma_typed.jou b/tests/syntax_error/double_assignment_comma_typed.jou new file mode 100644 index 00000000..58d7c017 --- /dev/null +++ b/tests/syntax_error/double_assignment_comma_typed.jou @@ -0,0 +1,2 @@ +def foo() -> None: + x, y: int = 7 # Error: only one variable can be assigned at a time diff --git a/tests/syntax_error/double_assignment_comma_untyped.jou b/tests/syntax_error/double_assignment_comma_untyped.jou new file mode 100644 index 00000000..eddb11cb --- /dev/null +++ b/tests/syntax_error/double_assignment_comma_untyped.jou @@ -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 diff --git a/tests/syntax_error/local_var_comma.jou b/tests/syntax_error/local_var_comma.jou new file mode 100644 index 00000000..f0ca76a4 --- /dev/null +++ b/tests/syntax_error/local_var_comma.jou @@ -0,0 +1,2 @@ +def main() -> int: + x, = 0 # Error: not a valid statement diff --git a/tests/syntax_error/local_vars_comma.jou b/tests/syntax_error/local_vars_comma.jou new file mode 100644 index 00000000..54bbf485 --- /dev/null +++ b/tests/syntax_error/local_vars_comma.jou @@ -0,0 +1,2 @@ +def main() -> int: + x, y, = 0 # Error: expected ':' and a type after it (example: "foo, bar: int"), got ',' diff --git a/tests/syntax_error/local_vars_comma_assign.jou b/tests/syntax_error/local_vars_comma_assign.jou new file mode 100644 index 00000000..77d976de --- /dev/null +++ b/tests/syntax_error/local_vars_comma_assign.jou @@ -0,0 +1,2 @@ +def main() -> int: + x, y, # Error: expected ':' and a type after it (example: "foo, bar: int"), got ',' diff --git a/tests/syntax_error/python_style_unpack.jou b/tests/syntax_error/python_style_unpack.jou new file mode 100644 index 00000000..f92b77d8 --- /dev/null +++ b/tests/syntax_error/python_style_unpack.jou @@ -0,0 +1,2 @@ +def main() -> int: + x, y = "hi" # Error: only one variable can be assigned at a time