diff --git a/TODO.md b/TODO.md index 05257da..b24f287 100644 --- a/TODO.md +++ b/TODO.md @@ -242,8 +242,16 @@ 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 }`) +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 + - 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 + 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 + body is just a 1-element statement slice, so the checker and codegen are unchanged 17. add slice-by-range - allow the use of a range in slice expressions: diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 63d544a..d680fd1 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -1184,6 +1184,27 @@ 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 { + 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 !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") + } + } + single := make([]ast.Stmt_Id, 1, parser.module.allocator) + single[0] = parse_statement(parser) + return single +} + parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id { start := advance(parser) // consume 'if' skip_newlines(parser) @@ -1226,8 +1247,7 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id { source.add(parser.diagnostics, current(parser).span, "expected '|' to close unwrap captures") } } - skip_newlines(parser) - then_body := parse_block(parser) + then_body := parse_branch_body(parser, true, condition, len(captures) > 0) else_body: []ast.Stmt_Id = nil saved_cursor := parser.cursor skip_newlines(parser) @@ -1240,7 +1260,7 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id { single[0] = nested else_body = single } else { - else_body = parse_block(parser) + else_body = parse_branch_body(parser, false, ast.INVALID_EXPR, false) } } else { parser.cursor = saved_cursor diff --git a/compiler_tests.odin b/compiler_tests.odin index c71ed67..7ffee13 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -4084,6 +4084,104 @@ conditional_unwrap_parser_captures_guard_and_parenthesized_chain :: proc(t: ^tes testing.expect_value(t, module.exprs[statement.guard].kind, ast.Expr_Kind.And) } +@(test) +parser_accepts_braceless_if_bodies :: proc(t: ^testing.T) { + text := `ready :: func() bool { return true } +main :: func() void { + x i32 = 0 + if (x == 0) x = 1 + if ready() x = 2 + if (x == 2) x = 3 else x = 4 + if (x > 0) { x = 10 } else x = 11 + v ?i32 = 5 + if v |u| _ = u +} +` + 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), 7) + + paren_if := module.statements[main.body[1]] + testing.expect_value(t, paren_if.kind, ast.Stmt_Kind.If) + testing.expect_value(t, len(paren_if.body), 1) + testing.expect(t, module.exprs[paren_if.expr].parenthesized) + + call_if := module.statements[main.body[2]] + testing.expect_value(t, call_if.kind, ast.Stmt_Kind.If) + testing.expect_value(t, len(call_if.body), 1) + testing.expect_value(t, module.exprs[call_if.expr].kind, ast.Expr_Kind.Call) + + if_else := module.statements[main.body[3]] + testing.expect_value(t, len(if_else.body), 1) + testing.expect_value(t, len(if_else.else_body), 1) + + braced_then := module.statements[main.body[4]] + testing.expect_value(t, len(braced_then.body), 1) + testing.expect_value(t, len(braced_then.else_body), 1) + + unwrap_if := module.statements[main.body[6]] + testing.expect_value(t, len(unwrap_if.captures), 1) + testing.expect_value(t, len(unwrap_if.body), 1) +} + +@(test) +parser_diagnoses_braceless_if_without_parens_or_call :: proc(t: ^testing.T) { + text := `main :: func() void { + x i32 = 0 + if x == 0 x = 1 +} +` + 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), 1) + testing.expect(t, strings.contains(diagnostics.items[0].message, "parenthesized")) +} + +@(test) +braceless_if_compiles_and_runs :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-braceless-if" + main_path := "/tmp/brolang-test-braceless-if/main.bro" + output := "/tmp/brolang-test-braceless-if-output" + text := `ready :: func() bool { return true } +main :: func() i32 { + x i32 = 0 + if (x == 0) x = 1 else x = 2 + if ready() x = x + 10 + y i32 = 5 + if (y == 0) y = 1 else y = 30 + x = x + y + return x +} +` + _ = 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, 41) +} + @(test) conditional_unwrap_allows_sink_captures :: proc(t: ^testing.T) { text := `main :: func() void {