braceless one-statement for-loops

This commit is contained in:
2026-07-15 23:27:18 +02:00
parent 7de0b7f268
commit 3cc750b3b2
4 changed files with 107 additions and 22 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ fields. `_` is not a keyword member name.
- checked integer `+ - *`, unary `-`, float-only `/`, IEEE float arithmetic, comparisons, `!`, `and`, and `or` - 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 - 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 - 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 - `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 - `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 - `break`, `continue`, labeled `break :label`, labeled `continue :label`, and labeled plain blocks; `break :label` can cross nested scopes to exit a labeled block
+7 -7
View File
@@ -252,15 +252,15 @@
c :: b + 3 # c is constrained to `i32` 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) 16. allow brace-less single-statement `if` and `for` bodies (implemented)
- if statements without a bracketed body must wrap the condition in parentheses UNLESS it's a function call - 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 - 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 brace-less branches mix freely
- the parenthesize-or-call rule constrains only the then-branch condition; `else` and the - the parenthesize-or-call rule constrains `if` then-branch conditions (including unwraps)
unwrap `|...|` already delimit, so they need no parentheses and `for` iterables; `else` bodies have no preceding expression to constrain
- the brace-less statement may sit on the line after the condition - the brace-less statement may sit on the following line
- parser-only change (`parse_branch_body` in `compiler/parser/parser.odin`): a brace-less - 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 body is just a 1-element statement slice, so the checker and codegen are unchanged
17. multi-line strings (implemented; see below) 17. multi-line strings (implemented; see below)
+19 -13
View File
@@ -1993,20 +1993,18 @@ parse_block :: proc(parser: ^Parser) -> []ast.Stmt_Id {
return body[:] return body[:]
} }
// parse_branch_body parses an `if` then/else body: a braced block (possibly on // parse_control_body parses a braced block (possibly on a following line) or a
// a following line) or a single brace-less statement (milestone 16). For the // single brace-less statement. A non-empty diagnostic enforces the shared
// then-branch (`validate_condition`), a brace-less body requires the condition // parenthesized-or-call rule for the preceding condition or iterable.
// to be parenthesized unless it is a function call; the `else` keyword and the parse_control_body :: proc(parser: ^Parser, header: ast.Expr_Id, diagnostic: string) -> []ast.Stmt_Id {
// 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 {
skip_newlines(parser) skip_newlines(parser)
if current(parser).kind == .Left_Brace { if current(parser).kind == .Left_Brace {
return parse_block(parser) return parse_block(parser)
} }
if validate_condition && !has_captures && condition != ast.INVALID_EXPR && int(condition) < len(parser.module.exprs) { if len(diagnostic) > 0 && header != ast.INVALID_EXPR && int(header) < len(parser.module.exprs) {
expr := parser.module.exprs[condition] expr := parser.module.exprs[header]
if !expr.parenthesized && expr.kind != .Call { 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) 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") 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 else_body: []ast.Stmt_Id = nil
saved_cursor := parser.cursor saved_cursor := parser.cursor
skip_newlines(parser) skip_newlines(parser)
@@ -2069,7 +2071,7 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id {
single[0] = nested single[0] = nested
else_body = single else_body = single
} else { } else {
else_body = parse_branch_body(parser, false, ast.INVALID_EXPR, false) else_body = parse_control_body(parser, ast.INVALID_EXPR, "")
} }
} else { } else {
parser.cursor = saved_cursor 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 // 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 // brace-less statement. For value-match a brace-less body is a single expression
// that the checker yields implicitly. // that the checker yields implicitly.
parse_arm_body :: proc(parser: ^Parser) -> []ast.Stmt_Id { parse_arm_body :: proc(parser: ^Parser) -> []ast.Stmt_Id {
@@ -2306,7 +2308,11 @@ parse_for :: proc(parser: ^Parser) -> ast.Stmt_Id {
} }
skip_newlines(parser) skip_newlines(parser)
label := parse_optional_loop_label(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)) id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{ append(&parser.module.statements, ast.Stmt{
+80 -1
View File
@@ -8798,7 +8798,7 @@ main func() void {
if (x == 2) x = 3 else x = 4 if (x == 2) x = 3 else x = 4
if (x > 0) { x = 10 } else x = 11 if (x > 0) { x = 10 } else x = 11
v ?i32 = 5 v ?i32 = 5
if v |u| _ = u if (v) |u| _ = u
} }
` `
source_file := source.Source{path="test.bro", text=text} 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")) 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) @(test)
braceless_if_compiles_and_runs :: proc(t: ^testing.T) { braceless_if_compiles_and_runs :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-braceless-if" 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)) 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) @(test)
range_bound_parenthesization_is_enforced :: proc(t: ^testing.T) { range_bound_parenthesization_is_enforced :: proc(t: ^testing.T) {
text := `main func() void { text := `main func() void {