diff --git a/LANGUAGE.md b/LANGUAGE.md index 7ba21c7..a261fff 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -87,7 +87,7 @@ fields. `_` is not a keyword member name. - checked integer `+ - *`, unary `-`, float-only `/`, IEEE float arithmetic, comparisons, `!`, `and`, and `or` - assignments and compound assignments `+= -= *= /=` with single evaluation of complex lvalues; `/=` is float-only - field access through struct values and pointers, index/slice bounds contextually coerced to `usize`, and unsigned narrower index support -- boolean `if` / `else if` / `else`, braceless single-statement branches, and optional parenthesized conditions +- boolean `if` / `else if` / `else` and `for` loops with braceless single-statement bodies when the preceding expression is parenthesized or a function call - `while` loops with optional post-iteration update clauses - `for` loops over ranges, arrays, slices, and pointers-to-arrays with copy captures, pointer captures `|@item|`, and optional `usize` index captures - `break`, `continue`, labeled `break :label`, labeled `continue :label`, and labeled plain blocks; `break :label` can cross nested scopes to exit a labeled block diff --git a/TODO.md b/TODO.md index 94f36ee..62f460c 100644 --- a/TODO.md +++ b/TODO.md @@ -252,15 +252,15 @@ c :: b + 3 # c is constrained to `i32` ``` -16. for if statements, allow `if (cond) one-line statement` or `if some_func(some_arg) one-line statement` (instead of forcing either `if (cond) { block }` or `if cond { block }`) (implemented) - - if statements without a bracketed body must wrap the condition in parentheses UNLESS it's a function call +16. allow brace-less single-statement `if` and `for` bodies (implemented) + - brace-less bodies must wrap the condition or iterable in parentheses UNLESS it's a function call - brace-less single-statement bodies apply to the then-body, the `else`-body, and the - unwrap/guard forms (`if v |x| stmt`); each branch is independent, so braced and + unwrap/guard forms (`if (v) |x| stmt`); each branch is independent, so braced and brace-less branches mix freely - - the parenthesize-or-call rule constrains only the then-branch condition; `else` and the - unwrap `|...|` already delimit, so they need no parentheses - - the brace-less statement may sit on the line after the condition - - parser-only change (`parse_branch_body` in `compiler/parser/parser.odin`): a brace-less + - the parenthesize-or-call rule constrains `if` then-branch conditions (including unwraps) + and `for` iterables; `else` bodies have no preceding expression to constrain + - the brace-less statement may sit on the following line + - parser-only change (`parse_control_body` in `compiler/parser/parser.odin`): a brace-less body is just a 1-element statement slice, so the checker and codegen are unchanged 17. multi-line strings (implemented; see below) diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index d880fc9..32d4f26 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -1993,20 +1993,18 @@ parse_block :: proc(parser: ^Parser) -> []ast.Stmt_Id { return body[:] } -// parse_branch_body parses an `if` then/else body: a braced block (possibly on -// a following line) or a single brace-less statement (milestone 16). For the -// then-branch (`validate_condition`), a brace-less body requires the condition -// to be parenthesized unless it is a function call; the `else` keyword and the -// unwrap `|...|` already delimit, so those positions skip the check. -parse_branch_body :: proc(parser: ^Parser, validate_condition: bool, condition: ast.Expr_Id, has_captures: bool) -> []ast.Stmt_Id { +// parse_control_body parses a braced block (possibly on a following line) or a +// single brace-less statement. A non-empty diagnostic enforces the shared +// parenthesized-or-call rule for the preceding condition or iterable. +parse_control_body :: proc(parser: ^Parser, header: ast.Expr_Id, diagnostic: string) -> []ast.Stmt_Id { skip_newlines(parser) if current(parser).kind == .Left_Brace { return parse_block(parser) } - if validate_condition && !has_captures && condition != ast.INVALID_EXPR && int(condition) < len(parser.module.exprs) { - expr := parser.module.exprs[condition] + if len(diagnostic) > 0 && header != ast.INVALID_EXPR && int(header) < len(parser.module.exprs) { + expr := parser.module.exprs[header] if !expr.parenthesized && expr.kind != .Call { - source.add(parser.diagnostics, expr.span, "a brace-less 'if' body requires the condition to be parenthesized unless it is a function call") + source.add(parser.diagnostics, expr.span, diagnostic) } } single := make([]ast.Stmt_Id, 1, parser.module.allocator) @@ -2056,7 +2054,11 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id { source.add(parser.diagnostics, current(parser).span, "expected '|' to close unwrap captures") } } - then_body := parse_branch_body(parser, true, condition, len(captures) > 0) + then_body := parse_control_body( + parser, + condition, + "a brace-less 'if' body requires the condition to be parenthesized unless it is a function call", + ) else_body: []ast.Stmt_Id = nil saved_cursor := parser.cursor skip_newlines(parser) @@ -2069,7 +2071,7 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id { single[0] = nested else_body = single } else { - else_body = parse_branch_body(parser, false, ast.INVALID_EXPR, false) + else_body = parse_control_body(parser, ast.INVALID_EXPR, "") } } else { parser.cursor = saved_cursor @@ -2089,7 +2091,7 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id { } // parse_arm_body parses a match arm's body after the `:`: a braced block (whose -// inner statements are returned unwrapped, like `parse_branch_body`) or a single +// inner statements are returned unwrapped, like `parse_control_body`) or a single // brace-less statement. For value-match a brace-less body is a single expression // that the checker yields implicitly. parse_arm_body :: proc(parser: ^Parser) -> []ast.Stmt_Id { @@ -2306,7 +2308,11 @@ parse_for :: proc(parser: ^Parser) -> ast.Stmt_Id { } skip_newlines(parser) label := parse_optional_loop_label(parser) - body := parse_block(parser) + body := parse_control_body( + parser, + iterable, + "a brace-less 'for' body requires the iterable to be parenthesized unless it is a function call", + ) id := ast.stmt_id(len(parser.module.statements)) append(&parser.module.statements, ast.Stmt{ diff --git a/compiler_tests.odin b/compiler_tests.odin index f146445..8a043a7 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -8798,7 +8798,7 @@ main func() void { if (x == 2) x = 3 else x = 4 if (x > 0) { x = 10 } else x = 11 v ?i32 = 5 - if v |u| _ = u + if (v) |u| _ = u } ` source_file := source.Source{path="test.bro", text=text} @@ -8859,6 +8859,29 @@ parser_diagnoses_braceless_if_without_parens_or_call :: proc(t: ^testing.T) { testing.expect(t, strings.contains(diagnostics.items[0].message, "parenthesized")) } +@(test) +parser_diagnoses_braceless_for_and_unwrap_without_parens_or_call :: proc(t: ^testing.T) { + text := `main func() void { + for tokens.items |item| _ = item + value ?i32 = 1 + if value |present| _ = present +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&module) + + testing.expect_value(t, len(diagnostics.items), 2) + testing.expect(t, strings.contains(diagnostics.items[0].message, "brace-less 'for'")) + testing.expect(t, strings.contains(diagnostics.items[1].message, "brace-less 'if'")) +} + @(test) braceless_if_compiles_and_runs :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-braceless-if" @@ -9345,6 +9368,62 @@ for_loop_tokens_and_parser_capture_range_shape :: proc(t: ^testing.T) { testing.expect_value(t, module.exprs[third.expr].integer, u64(1)) } +@(test) +parser_accepts_braceless_for_bodies :: proc(t: ^testing.T) { + text := `make_items func() [2]i32 { return [1, 2] } +main func() void { + for (tokens.items) |item| _ = item + for make_items() |item| + _ = item +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&module) + + testing.expect_value(t, len(diagnostics.items), 0) + main := module.functions[1] + testing.expect_value(t, len(main.body), 2) + parenthesized := module.statements[main.body[0]] + call := module.statements[main.body[1]] + testing.expect_value(t, len(parenthesized.body), 1) + testing.expect(t, module.exprs[parenthesized.expr].parenthesized) + testing.expect_value(t, len(call.body), 1) + testing.expect_value(t, module.exprs[call.expr].kind, ast.Expr_Kind.Call) +} + +@(test) +braceless_for_compiles_and_runs :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-braceless-for" + main_path := "/tmp/brolang-test-braceless-for/main.bro" + output := "/tmp/brolang-test-braceless-for-output" + text := `make_items func() [2]i32 { return [20, 2] } +main func() i32 { + total i32 = 0 + items :: [10, 11] + for (items) |item| total += item + for make_items() |item| + total += item + return total +} +` + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + defer _ = os.remove(output) + testing.expect(t, os.make_directory(directory) == nil) + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) + status := compiler_core.compile_package(directory, output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 43) +} + @(test) range_bound_parenthesization_is_enforced :: proc(t: ^testing.T) { text := `main func() void {