diff --git a/TODO.md b/TODO.md index 2067508..9dd14df 100644 --- a/TODO.md +++ b/TODO.md @@ -81,10 +81,14 @@ - static inline functions (implemented) - 5. control flow - - boolean expressions + - boolean expressions (implemented) + - `bool` type with `true` / `false` literals + - comparison operators: `==`, `!=`, `<`, `<=`, `>`, `>=` (numeric operands widen; `bool` supports only `==` / `!=`) - operators: `and`, `or`, `!` - lazy evaluation / short-circuit evaluation - - if statements. example: `if condition { ... } else if { ... } else { ... }` + - if statements (implemented). example: `if condition { ... } else if { ... } else { ... }` + - conditions must be `bool`; block-scoped locals with shadowing across blocks + - lowered through new `Label` / `Br` / `Cond_Br` IR opcodes (alloca-backed locals, no phi nodes) - conditional unwrapping for optionals (`?T`): `if val |v| { ... } else { ... }` - unwrap `val` into `v` if it is not `none` - conditional unwrapping with guard clause: `if val |v : v >= 10| { ... } else { ... }` - unwrap `val` into `v` if it is not `none` - multi-unwrap (see section below) diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 729bb45..d309627 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -68,6 +68,7 @@ Expr_Kind :: enum u8 { Integer, Float, String, + Bool, Array, None, Name, @@ -81,7 +82,16 @@ Expr_Kind :: enum u8 { Struct_Literal, Keyed, Negate, + Not, Add, + Eq, + Ne, + Lt, + Le, + Gt, + Ge, + And, + Or, Call, } @@ -109,6 +119,7 @@ Stmt_Kind :: enum u8 { Assignment, Return, Expression, + If, } Stmt :: struct { @@ -119,6 +130,11 @@ Stmt :: struct { immutable: bool, target: Expr_Id, expr: Expr_Id, + // `If` statements use `expr` as the condition, `body` as the then-block, and + // `else_body` as the else-block. An `else if` chain is represented as an + // `else_body` holding a single nested `If` statement. + body: []Stmt_Id, + else_body: []Stmt_Id, diagnostic: source.Diagnostic_Id, } @@ -234,6 +250,10 @@ destroy_module :: proc(module: ^Module) { for expr in module.exprs { delete(expr.args, module.allocator) } + for statement in module.statements { + delete(statement.body, module.allocator) + delete(statement.else_body, module.allocator) + } for function in module.functions { delete(function.params, module.allocator) delete(function.body, module.allocator) diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 5472e0b..9c7a8b5 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -44,6 +44,23 @@ Build_Local :: struct { id: hir.Local_Id, } +// Build_Ctx threads the per-function accumulators through build_block so that +// nested control-flow blocks (if/else) can be built recursively. `locals` is a +// scope stack: each block records its entry length and truncates back to it on +// exit, while `hir_locals` keeps every allocated slot for the function. +Build_Ctx :: struct { + checker: ^Checker, + pkg: ast.Package_Id, + file: ast.File_Id, + result: types.Type, + locals: ^[dynamic]Build_Local, + hir_locals: ^[dynamic]hir.Local, + global_reads: ^[dynamic]hir.Global_Id, + calls: ^[dynamic]hir.Function_Id, + problematic: ^bool, + has_return: ^bool, +} + Constant_Kind :: enum { Unknown, Not_Constant, @@ -593,11 +610,29 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as if expr.left != ast.INVALID_EXPR { append(&stack, expr.left) } - case .Negate, .Address, .Deref, .Field, .Unwrap, .Keyed: + case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Keyed: append(&stack, expr.left) - case .Add, .Index, .Orelse: + case .Add, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or: append(&stack, expr.left, expr.right) - case .Invalid, .Integer, .Float, .String, .None, .Name: + case .Invalid, .Integer, .Float, .String, .Bool, .None, .Name: + } + } +} + +mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, file: ast.File_Id) { + for statement_id in statements { + statement := checker.ast_module.statements[statement_id] + switch statement.kind { + case .Declaration, .Assignment, .Return, .Expression: + mark_expr_imports_used(checker, statement.expr, file) + if statement.target != ast.INVALID_EXPR { + mark_expr_imports_used(checker, statement.target, file) + } + case .If: + mark_expr_imports_used(checker, statement.expr, file) + mark_block_imports_used(checker, statement.body, file) + mark_block_imports_used(checker, statement.else_body, file) + case .Invalid: } } } @@ -772,17 +807,7 @@ validate_declarations :: proc(checker: ^Checker) { ) } } - for statement_id in function.body { - statement := checker.ast_module.statements[statement_id] - switch statement.kind { - case .Declaration, .Assignment, .Return, .Expression: - mark_expr_imports_used(checker, statement.expr, function.file) - if statement.target != ast.INVALID_EXPR { - mark_expr_imports_used(checker, statement.target, function.file) - } - case .Invalid: - } - } + mark_block_imports_used(checker, function.body, function.file) delete(locals) } for function, function_id in checker.ast_module.functions { @@ -988,6 +1013,15 @@ infer_compound_expr :: proc( ) -> types.Type { store := &checker.module.types #partial switch expr.kind { + case .Bool: + return types.BOOL + case .Not: + _ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded) + return types.BOOL + case .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or: + _ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded) + _ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded) + return types.BOOL case .String: return string_literal_type(checker, expr.integer) case .Array: @@ -1124,7 +1158,8 @@ infer_expr :: proc( last = types.F64 _ = pop(&stack) case .String, .Array, .None, .Address, .Deref, .Index, .Slice, - .Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed: + .Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, + .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or: last = infer_compound_expr(checker, expr, locals, pkg, file, demanded) _ = pop(&stack) case .Name: @@ -1342,6 +1377,49 @@ infer_expr :: proc( return last } +infer_statements :: proc( + checker: ^Checker, + statements: []ast.Stmt_Id, + locals: ^[dynamic]Infer_Local, + pkg: ast.Package_Id, + file: ast.File_Id, + demanded: ^[dynamic]Spec_Id, + result: ^types.Type, +) { + scope_start := len(locals^) + for statement_id in statements { + statement := checker.ast_module.statements[statement_id] + #partial switch statement.kind { + case .Declaration: + value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) + declared_local := type_from_syntax(statement.type) + if is_runtime_type(checker, declared_local) { + value_type = declared_local + } + append(locals, Infer_Local{name = statement.name, type = value_type}) + case .Assignment, .Expression: + if statement.target != ast.INVALID_EXPR { + _ = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded) + } + _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) + case .Return: + if statement.expr != ast.INVALID_EXPR { + returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) + if !types.is_valid(result^) { + result^ = returned + } else { + result^ = types.widest(result^, returned) + } + } + case .If: + _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) + infer_statements(checker, statement.body, locals, pkg, file, demanded, result) + infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result) + } + } + resize(locals, scope_start) +} + infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Spec_Id = nil) -> types.Type { spec := checker.specs[id] function := checker.ast_module.functions[spec.template] @@ -1362,32 +1440,7 @@ infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Sp } result := types.INVALID - for statement_id in function.body { - statement := checker.ast_module.statements[statement_id] - #partial switch statement.kind { - case .Declaration: - value_type := infer_expr(checker, statement.expr, locals[:], function.pkg, function.file, demanded) - declared_local := type_from_syntax(statement.type) - if is_runtime_type(checker, declared_local) { - value_type = declared_local - } - append(&locals, Infer_Local{name = statement.name, type = value_type}) - case .Assignment, .Expression: - if statement.target != ast.INVALID_EXPR { - _ = infer_expr(checker, statement.target, locals[:], function.pkg, function.file, demanded) - } - _ = infer_expr(checker, statement.expr, locals[:], function.pkg, function.file, demanded) - case .Return: - if statement.expr != ast.INVALID_EXPR { - returned := infer_expr(checker, statement.expr, locals[:], function.pkg, function.file, demanded) - if !types.is_valid(result) { - result = returned - } else { - result = types.widest(result, returned) - } - } - } - } + infer_statements(checker, function.body, &locals, function.pkg, function.file, demanded, &result) if types.is_constraint(declared) { return result } @@ -2116,6 +2169,90 @@ build_compound_expr :: proc( kind=.Orelse, span=expr.span, type=child, left=optional, right=fallback, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, }) + case .Bool: + return add_hir_expr(checker, hir.Expr{ + kind=.Bool, span=expr.span, type=types.BOOL, integer=i64(expr.integer), + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .Not: + operand := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.BOOL, pkg, file) + operand_type := checker.module.exprs[operand].type + if checker.module.exprs[operand].kind != .Invalid && !types.is_bool(operand_type) { + id := source.add(checker.diagnostics, expr.span, "'!' requires a bool operand") + return invalid_hir_expr(checker, expr.span, id, types.BOOL) + } + return add_hir_expr(checker, hir.Expr{ + kind=.Not, span=expr.span, type=types.BOOL, left=operand, + target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .And, .Or: + left := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.BOOL, pkg, file) + right := build_nested_expr(checker, expr.right, locals, global_reads, calls, types.BOOL, pkg, file) + left_type := checker.module.exprs[left].type + right_type := checker.module.exprs[right].type + left_ok := checker.module.exprs[left].kind == .Invalid || types.is_bool(left_type) + right_ok := checker.module.exprs[right].kind == .Invalid || types.is_bool(right_type) + if !left_ok || !right_ok { + id := source.add(checker.diagnostics, expr.span, "'and'/'or' require bool operands") + return invalid_hir_expr(checker, expr.span, id, types.BOOL) + } + return add_hir_expr(checker, hir.Expr{ + kind=.And if expr.kind == .And else .Or, span=expr.span, type=types.BOOL, + left=left, right=right, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .Eq, .Ne, .Lt, .Le, .Gt, .Ge: + // Contextualize a bare integer-literal operand to the other operand's type + // so comparisons like `count > 0` or `0 < count` type-check. + left_const := eval_constant(checker, expr.left) + right_const := eval_constant(checker, expr.right) + left, right: hir.Expr_Id + if right_const.kind == .Value && left_const.kind != .Value { + left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) + hint := checker.module.exprs[left].type + right = build_nested_expr(checker, expr.right, locals, global_reads, calls, hint, pkg, file) + } else if left_const.kind == .Value && right_const.kind != .Value { + right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) + hint := checker.module.exprs[right].type + left = build_nested_expr(checker, expr.left, locals, global_reads, calls, hint, pkg, file) + } else { + left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) + right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) + } + left_type := checker.module.exprs[left].type + right_type := checker.module.exprs[right].type + if checker.module.exprs[left].kind == .Invalid || checker.module.exprs[right].kind == .Invalid { + return invalid_hir_expr(checker, expr.span, expr.diagnostic, types.BOOL) + } + operand_type := types.INVALID + if types.is_bool(left_type) && types.is_bool(right_type) { + if expr.kind != .Eq && expr.kind != .Ne { + id := source.add(checker.diagnostics, expr.span, "bool values only support '==' and '!='") + return invalid_hir_expr(checker, expr.span, id, types.BOOL) + } + operand_type = types.BOOL + } else { + operand_type = types.widest(left_type, right_type) + if !types.is_concrete_scalar(operand_type) || types.is_bool(operand_type) { + id := source.add(checker.diagnostics, expr.span, "comparison requires compatible numeric operands") + return invalid_hir_expr(checker, expr.span, id, types.BOOL) + } + } + left = coerce_expr(checker, left, operand_type, checker.module.exprs[left].span) + right = coerce_expr(checker, right, operand_type, checker.module.exprs[right].span) + compare_kind := hir.Expr_Kind.Eq + #partial switch expr.kind { + case .Eq: compare_kind = .Eq + case .Ne: compare_kind = .Ne + case .Lt: compare_kind = .Lt + case .Le: compare_kind = .Le + case .Gt: compare_kind = .Gt + case .Ge: compare_kind = .Ge + } + return add_hir_expr(checker, hir.Expr{ + kind=compare_kind, span=expr.span, type=types.BOOL, left=left, right=right, + target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, + }) case .Struct_Literal: target_pkg, available := expr_package(checker, expr, pkg, file, true) struct_type := types.find_named(store, u32(target_pkg), u32(expr.name)) if available else types.INVALID @@ -2230,7 +2367,8 @@ build_expr :: proc( } switch expr.kind { case .String, .Array, .None, .Address, .Deref, .Index, .Slice, - .Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed: + .Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, + .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or: last = build_compound_expr( checker, expr, locals, global_reads, calls, frame.expected, pkg, file, ) @@ -2676,6 +2814,231 @@ make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string { return fmt.aprintf("%s", strings.to_string(builder), allocator = checker.allocator) } +build_block :: proc(ctx: ^Build_Ctx, statements: []ast.Stmt_Id) -> []hir.Stmt_Id { + checker := ctx.checker + body: [dynamic]hir.Stmt_Id + body.allocator = checker.allocator + scope_start := len(ctx.locals^) + for statement_id in statements { + statement := checker.ast_module.statements[statement_id] + switch statement.kind { + case .Declaration: + declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr) + expected := types.INVALID + if is_runtime_type(checker, declared) { + expected = declared + } + value := build_expr( + checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, + expected, ctx.pkg, ctx.file, + ) + value_type := checker.module.exprs[value].type + if is_runtime_type(checker, declared) { + value = coerce_expr(checker, value, declared, statement.span) + value_type = checker.module.exprs[value].type + } else if types.is_void(declared) { + id := source.add(checker.diagnostics, statement.span, "locals cannot have type void") + value = invalid_hir_expr(checker, statement.span, id) + value_type = types.INVALID + } + if _, found := find_build_local(ctx.locals^[scope_start:], statement.name); found { + id := source.addf( + checker.diagnostics, statement.span, + "duplicate local '%s'", symbol_text(checker, statement.name), + ) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = id, + }) + ctx.problematic^ = true + continue + } + local_id := hir.local_id(len(ctx.hir_locals^)) + append(ctx.hir_locals, hir.Local{ + name = statement.name, type = value_type, mutable = !statement.immutable, + }) + append(ctx.locals, Build_Local{ + name = statement.name, type = value_type, mutable = !statement.immutable, id = local_id, + }) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Declaration, span = statement.span, local = local_id, expr = value, + diagnostic = source.INVALID_DIAGNOSTIC, + }) + ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid + case .Assignment: + if statement.target != ast.INVALID_EXPR { + target_expr := build_expr( + checker, statement.target, ctx.locals^[:], ctx.global_reads, ctx.calls, + types.INVALID, ctx.pkg, ctx.file, + ) + target_type := checker.module.exprs[target_expr].type + if !hir_location_writable(checker, target_expr, ctx.locals^[:]) { + id := source.add(checker.diagnostics, statement.span, "assignment target is not writable") + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind=.Trap, span=statement.span, local=hir.INVALID_LOCAL, + target=hir.INVALID_EXPR, expr=hir.INVALID_EXPR, diagnostic=id, + }) + ctx.problematic^ = true + continue + } + value := build_expr( + checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, + target_type, ctx.pkg, ctx.file, + ) + value = coerce_expr(checker, value, target_type, statement.span) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind=.Assignment, span=statement.span, local=hir.INVALID_LOCAL, + target=target_expr, expr=value, diagnostic=source.INVALID_DIAGNOSTIC, + }) + ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid + continue + } + if statement.name == checker.sink_symbol { + value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) + if types.is_void(checker.module.exprs[value].type) { + id := source.add(checker.diagnostics, statement.span, "cannot assign a void expression to '_'") + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = id, + }) + ctx.problematic^ = true + } else { + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Sink, span = statement.span, expr = value, + local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, + }) + } + continue + } + local, found := find_build_local(ctx.locals^[:], statement.name) + if !found { + id := source.addf(checker.diagnostics, statement.span, "cannot assign unresolved local '%s'", symbol_text(checker, statement.name)) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = id, + }) + ctx.problematic^ = true + continue + } + if !local.mutable { + id := source.addf(checker.diagnostics, statement.span, "cannot assign immutable local '%s'", symbol_text(checker, statement.name)) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = id, + }) + ctx.problematic^ = true + continue + } + value := build_expr( + checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, + local.type, ctx.pkg, ctx.file, + ) + value = coerce_expr(checker, value, local.type, statement.span) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Assignment, span = statement.span, expr = value, local = local.id, + target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, + }) + ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid + case .Return: + ctx.has_return^ = true + if statement.expr == ast.INVALID_EXPR { + if !types.is_void(ctx.result) { + id := source.add(checker.diagnostics, statement.span, "'return _' is only valid in a void function") + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = id, + }) + ctx.problematic^ = true + } else { + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Return, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, + }) + } + continue + } + if types.is_void(ctx.result) { + id := source.add(checker.diagnostics, statement.span, "void function cannot return a value") + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = id, + }) + ctx.problematic^ = true + continue + } + value := build_expr( + checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, + ctx.result, ctx.pkg, ctx.file, + ) + value = coerce_expr(checker, value, ctx.result, statement.span) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Return, span = statement.span, expr = value, + local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, + }) + ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid + case .Expression: + value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) + if !types.is_void(checker.module.exprs[value].type) { + id := source.add(checker.diagnostics, statement.span, "non-void expression result must be consumed or assigned to '_'") + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = id, + }) + ctx.problematic^ = true + } else { + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Expression, span = statement.span, expr = value, + local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC, + }) + } + case .If: + condition := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file) + if checker.module.exprs[condition].kind != .Invalid && !types.is_bool(checker.module.exprs[condition].type) { + id := source.add(checker.diagnostics, statement.span, "'if' condition must be a bool") + condition = invalid_hir_expr(checker, statement.span, id, types.BOOL) + ctx.problematic^ = true + } + then_body := build_block(ctx, statement.body) + else_body: []hir.Stmt_Id = nil + if statement.else_body != nil { + else_body = build_block(ctx, statement.else_body) + } + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .If, span = statement.span, expr = condition, + then_body = then_body, else_body = else_body, + local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, + diagnostic = source.INVALID_DIAGNOSTIC, + }) + ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid + case .Invalid: + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = statement.diagnostic, + }) + ctx.problematic^ = true + } + } + resize(ctx.locals, scope_start) + return body[:] +} + build_function :: proc(checker: ^Checker, id: Spec_Id) { spec := checker.specs[id] function := checker.ast_module.functions[spec.template] @@ -2769,348 +3132,23 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { }, ) } - for statement_id in function.body { - statement := checker.ast_module.statements[statement_id] - switch statement.kind { - case .Declaration: - declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr) - expected := types.INVALID - if is_runtime_type(checker, declared) { - expected = declared - } - value := build_expr( - checker, - statement.expr, - locals[:], - &global_reads, - &calls, - expected, - function.pkg, - function.file, - ) - value_type := checker.module.exprs[value].type - if is_runtime_type(checker, declared) { - value = coerce_expr(checker, value, declared, statement.span) - value_type = checker.module.exprs[value].type - } else if types.is_void(declared) { - id := source.add( - checker.diagnostics, - statement.span, - "locals cannot have type void", - ) - value = invalid_hir_expr(checker, statement.span, id) - value_type = types.INVALID - } - if _, found := find_build_local(locals[:], statement.name); found { - id := source.addf( - checker.diagnostics, - statement.span, - "duplicate local '%s'", - symbol_text(checker, statement.name), - ) - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Trap, - span = statement.span, - expr = hir.INVALID_EXPR, - local = hir.INVALID_LOCAL, - diagnostic = id, - }, - ) - problematic = true - continue - } - local_id := hir.local_id(len(hir_locals)) - append( - &hir_locals, - hir.Local { - name = statement.name, - type = value_type, - mutable = !statement.immutable, - }, - ) - append( - &locals, - Build_Local { - name = statement.name, - type = value_type, - mutable = !statement.immutable, - id = local_id, - }, - ) - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Declaration, - span = statement.span, - local = local_id, - expr = value, - diagnostic = source.INVALID_DIAGNOSTIC, - }, - ) - problematic = problematic || checker.module.exprs[value].kind == .Invalid - case .Assignment: - if statement.target != ast.INVALID_EXPR { - target_expr := build_expr( - checker, statement.target, locals[:], &global_reads, &calls, - types.INVALID, function.pkg, function.file, - ) - target_type := checker.module.exprs[target_expr].type - if !hir_location_writable(checker, target_expr, locals[:]) { - id := source.add(checker.diagnostics, statement.span, "assignment target is not writable") - append(&body, hir.stmt_id(len(checker.module.statements))) - append(&checker.module.statements, hir.Stmt{ - kind=.Trap, span=statement.span, local=hir.INVALID_LOCAL, - target=hir.INVALID_EXPR, expr=hir.INVALID_EXPR, diagnostic=id, - }) - problematic = true - continue - } - value := build_expr( - checker, statement.expr, locals[:], &global_reads, &calls, - target_type, function.pkg, function.file, - ) - value = coerce_expr(checker, value, target_type, statement.span) - append(&body, hir.stmt_id(len(checker.module.statements))) - append(&checker.module.statements, hir.Stmt{ - kind=.Assignment, span=statement.span, local=hir.INVALID_LOCAL, - target=target_expr, expr=value, diagnostic=source.INVALID_DIAGNOSTIC, - }) - problematic = problematic || checker.module.exprs[value].kind == .Invalid - continue - } - if statement.name == checker.sink_symbol { - value := build_expr(checker, statement.expr, locals[:], &global_reads, &calls, types.INVALID, function.pkg, function.file) - if types.is_void(checker.module.exprs[value].type) { - id := source.add( - checker.diagnostics, - statement.span, - "cannot assign a void expression to '_'", - ) - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Trap, - span = statement.span, - expr = hir.INVALID_EXPR, - local = hir.INVALID_LOCAL, - diagnostic = id, - }, - ) - problematic = true - } else { - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Sink, - span = statement.span, - expr = value, - local = hir.INVALID_LOCAL, - diagnostic = source.INVALID_DIAGNOSTIC, - }, - ) - } - continue - } - local, found := find_build_local(locals[:], statement.name) - if !found { - id := source.addf( - checker.diagnostics, - statement.span, - "cannot assign unresolved local '%s'", - symbol_text(checker, statement.name), - ) - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Trap, - span = statement.span, - expr = hir.INVALID_EXPR, - local = hir.INVALID_LOCAL, - diagnostic = id, - }, - ) - problematic = true - continue - } - if !local.mutable { - id := source.addf( - checker.diagnostics, - statement.span, - "cannot assign immutable local '%s'", - symbol_text(checker, statement.name), - ) - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Trap, - span = statement.span, - expr = hir.INVALID_EXPR, - local = hir.INVALID_LOCAL, - diagnostic = id, - }, - ) - problematic = true - continue - } - value := build_expr( - checker, - statement.expr, - locals[:], - &global_reads, - &calls, - local.type, - function.pkg, - function.file, - ) - value = coerce_expr(checker, value, local.type, statement.span) - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Assignment, - span = statement.span, - expr = value, - local = local.id, - target = hir.INVALID_EXPR, - diagnostic = source.INVALID_DIAGNOSTIC, - }, - ) - problematic = problematic || checker.module.exprs[value].kind == .Invalid - case .Return: - has_return = true - if statement.expr == ast.INVALID_EXPR { - if !types.is_void(spec.result) { - id := source.add( - checker.diagnostics, - statement.span, - "'return _' is only valid in a void function", - ) - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Trap, - span = statement.span, - expr = hir.INVALID_EXPR, - local = hir.INVALID_LOCAL, - diagnostic = id, - }, - ) - problematic = true - } else { - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Return, - span = statement.span, - expr = hir.INVALID_EXPR, - local = hir.INVALID_LOCAL, - diagnostic = source.INVALID_DIAGNOSTIC, - }, - ) - } - continue - } - if types.is_void(spec.result) { - id := source.add( - checker.diagnostics, - statement.span, - "void function cannot return a value", - ) - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Trap, - span = statement.span, - expr = hir.INVALID_EXPR, - local = hir.INVALID_LOCAL, - diagnostic = id, - }, - ) - problematic = true - continue - } - value := build_expr( - checker, - statement.expr, - locals[:], - &global_reads, - &calls, - spec.result, - function.pkg, - function.file, - ) - value = coerce_expr(checker, value, spec.result, statement.span) - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Return, - span = statement.span, - expr = value, - local = hir.INVALID_LOCAL, - diagnostic = source.INVALID_DIAGNOSTIC, - }, - ) - problematic = problematic || checker.module.exprs[value].kind == .Invalid - case .Expression: - value := build_expr(checker, statement.expr, locals[:], &global_reads, &calls, types.INVALID, function.pkg, function.file) - if !types.is_void(checker.module.exprs[value].type) { - id := source.add( - checker.diagnostics, - statement.span, - "non-void expression result must be consumed or assigned to '_'", - ) - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Trap, - span = statement.span, - expr = hir.INVALID_EXPR, - local = hir.INVALID_LOCAL, - diagnostic = id, - }, - ) - problematic = true - } else { - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Expression, - span = statement.span, - expr = value, - local = hir.INVALID_LOCAL, - diagnostic = source.INVALID_DIAGNOSTIC, - }, - ) - } - case .Invalid: - append(&body, hir.stmt_id(len(checker.module.statements))) - append( - &checker.module.statements, - hir.Stmt { - kind = .Trap, - span = statement.span, - expr = hir.INVALID_EXPR, - local = hir.INVALID_LOCAL, - diagnostic = statement.diagnostic, - }, - ) - problematic = true - } + ctx := Build_Ctx{ + checker = checker, + pkg = function.pkg, + file = function.file, + result = spec.result, + locals = &locals, + hir_locals = &hir_locals, + global_reads = &global_reads, + calls = &calls, + problematic = &problematic, + has_return = &has_return, } + block := build_block(&ctx, function.body) + for block_stmt in block { + append(&body, block_stmt) + } + delete(block, checker.allocator) if !types.is_void(spec.result) && !has_return { id := source.addf( diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index a5ededa..8974f78 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -77,6 +77,7 @@ Expr_Kind :: enum u8 { Integer, Float, String, + Bool, Array, Struct, None, @@ -99,8 +100,17 @@ Expr_Kind :: enum u8 { Weaken_Slice, Decay_Array_Pointer, Negate, + Not, Add, Pointer_Add, + Eq, + Ne, + Lt, + Le, + Gt, + Ge, + And, + Or, Call, } @@ -130,6 +140,7 @@ Stmt_Kind :: enum u8 { Expression, Sink, Trap, + If, } Stmt :: struct { @@ -138,6 +149,10 @@ Stmt :: struct { local: Local_Id, target: Expr_Id, expr: Expr_Id, + // `If` statements use `expr` as the condition and `then_body`/`else_body` as + // the branch statement lists. + then_body: []Stmt_Id, + else_body: []Stmt_Id, diagnostic: source.Diagnostic_Id, } @@ -204,6 +219,10 @@ destroy_module :: proc(module: ^Module) { for expr in module.exprs { delete(expr.args, module.allocator) } + for statement in module.statements { + delete(statement.then_body, module.allocator) + delete(statement.else_body, module.allocator) + } for function in module.functions { delete(function.link_name, module.allocator) delete(function.params, module.allocator) diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index db600c5..d2bd86e 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -95,12 +95,27 @@ Opcode :: enum u8 { Neg_Checked, Add_Checked, Pointer_Add, + Not, + Compare, + Label, + Br, + Cond_Br, Call, Trap, Return, Return_Void, } +// Compare_Predicate is stored in Instruction.integer for the Compare opcode. +Compare_Predicate :: enum u8 { + Eq, + Ne, + Lt, + Le, + Gt, + Ge, +} + Instruction :: struct { span: source.Span, type: types.Type, diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index 591984f..9e0a720 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -23,7 +23,14 @@ keyword_kind :: proc(text: string) -> token.Kind { case "mut": return .Keyword_Mut case "none": return .Keyword_None case "orelse": return .Keyword_Orelse + case "and": return .Keyword_And + case "or": return .Keyword_Or + case "if": return .Keyword_If + case "else": return .Keyword_Else + case "true": return .Keyword_True + case "false": return .Keyword_False case "void": return .Keyword_Void + case "bool": return .Keyword_Bool case "int": return .Keyword_Int case "i8": return .Keyword_I8 case "i16": return .Keyword_I16 @@ -106,8 +113,41 @@ lex :: proc( append_token(&stream, source_file, .Invalid, start, cursor, diagnostic=id) } case '=': - append_token(&stream, source_file, .Equal, cursor, cursor+1) + start := cursor cursor += 1 + if cursor < len(bytes) && bytes[cursor] == '=' { + cursor += 1 + append_token(&stream, source_file, .Equal_Equal, start, cursor) + } else { + append_token(&stream, source_file, .Equal, start, cursor) + } + case '!': + start := cursor + cursor += 1 + if cursor < len(bytes) && bytes[cursor] == '=' { + cursor += 1 + append_token(&stream, source_file, .Bang_Equal, start, cursor) + } else { + append_token(&stream, source_file, .Bang, start, cursor) + } + case '<': + start := cursor + cursor += 1 + if cursor < len(bytes) && bytes[cursor] == '=' { + cursor += 1 + append_token(&stream, source_file, .Less_Equal, start, cursor) + } else { + append_token(&stream, source_file, .Less, start, cursor) + } + case '>': + start := cursor + cursor += 1 + if cursor < len(bytes) && bytes[cursor] == '=' { + cursor += 1 + append_token(&stream, source_file, .Greater_Equal, start, cursor) + } else { + append_token(&stream, source_file, .Greater, start, cursor) + } case '+': append_token(&stream, source_file, .Plus, cursor, cursor+1) cursor += 1 diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 7fbfa7b..fa06690 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -138,6 +138,9 @@ llvm_type :: proc(value: types.Type, store: ^types.Store = nil) -> string { if types.is_void(value) { return "void" } + if types.is_bool(value) { + return "i1" + } #partial switch types.kind(value, store) { case .Pointer: return "ptr" @@ -201,6 +204,9 @@ emit_function_result :: proc(builder: ^strings.Builder, function: ir.Function, s } sentinel :: proc(value_type: types.Type, selected := target.DEFAULT) -> i64 { + if types.is_bool(value_type) { + return 0 + } switch types.bits(value_type, selected) { case 8: return -86 case 16: return -21846 @@ -228,10 +234,10 @@ valid_value :: proc( case .Param, .Const, .String, .Aggregate, .None, .Optional_Some, .Load_Global, .Function_Address, .Address_Of, .Load, .Slice, .Length, .Slice_Ptr, .Unwrap, .Orelse, .Widen, .C_Vararg_Promote, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer, - .Neg_Checked, .Add_Checked, .Pointer_Add, .Call: + .Neg_Checked, .Add_Checked, .Pointer_Add, .Not, .Compare, .Call: return true case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, - .Store, .Trap, .Return, .Return_Void: + .Store, .Trap, .Label, .Br, .Cond_Br, .Return, .Return_Void: return false } return false @@ -262,6 +268,10 @@ write_constant :: proc(builder: ^strings.Builder, value: i64, value_type: types. strings.write_string(builder, "zeroinitializer") return } + if types.is_bool(value_type) { + strings.write_string(builder, "true" if value != 0 else "false") + return + } selected := store.selected if store != nil else target.DEFAULT if types.is_float(value_type, selected) { text := "" @@ -462,6 +472,30 @@ emit_unpack_c_record :: proc( fmt.sbprintf(&emitter.builder, " %s = load %s, ptr %%abi_unpack_slot%d\n", result_name, llvm_type(value_type, &emitter.module.types), tag) } +integer_predicate :: proc(predicate: ir.Compare_Predicate, signed: bool) -> string { + switch predicate { + case .Eq: return "eq" + case .Ne: return "ne" + case .Lt: return "slt" if signed else "ult" + case .Le: return "sle" if signed else "ule" + case .Gt: return "sgt" if signed else "ugt" + case .Ge: return "sge" if signed else "uge" + } + return "eq" +} + +float_predicate :: proc(predicate: ir.Compare_Predicate) -> string { + switch predicate { + case .Eq: return "oeq" + case .Ne: return "une" + case .Lt: return "olt" + case .Le: return "ole" + case .Gt: return "ogt" + case .Ge: return "oge" + } + return "oeq" +} + emit_instruction_stream :: proc( emitter: ^Emitter, instructions: []ir.Instruction, @@ -1335,6 +1369,44 @@ emit_instruction_stream :: proc( 100000+instruction_index, ) } + case .Not: + if !valid_value(instructions, instruction.a, types.BOOL, &emitter.module.types) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid '!' operand") + continue + } + fmt.sbprintf(&emitter.builder, " %%v%d = xor i1 ", instruction_index) + write_operand(&emitter.builder, instructions, instruction.a, types.BOOL, &emitter.module.types) + strings.write_string(&emitter.builder, ", true\n") + case .Compare: + operand_type := instructions[instruction.a].type if valid_instruction(instructions, instruction.a) else types.INVALID + if !valid_value(instructions, instruction.a, operand_type, &emitter.module.types) || + !valid_value(instructions, instruction.b, operand_type, &emitter.module.types) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid comparison operand") + continue + } + predicate := ir.Compare_Predicate(instruction.integer) + type_name := llvm_type(operand_type, &emitter.module.types) + if types.is_float(operand_type, emitter.module.target) { + fmt.sbprintf(&emitter.builder, " %%v%d = fcmp %s %s ", instruction_index, float_predicate(predicate), type_name) + } else { + fmt.sbprintf(&emitter.builder, " %%v%d = icmp %s %s ", instruction_index, integer_predicate(predicate, types.is_signed(operand_type, emitter.module.target)), type_name) + } + write_operand(&emitter.builder, instructions, instruction.a, operand_type, &emitter.module.types) + strings.write_string(&emitter.builder, ", ") + write_operand(&emitter.builder, instructions, instruction.b, operand_type, &emitter.module.types) + strings.write_string(&emitter.builder, "\n") + case .Label: + fmt.sbprintf(&emitter.builder, "bro_block_%d:\n", instruction.integer) + case .Br: + fmt.sbprintf(&emitter.builder, " br label %%bro_block_%d\n", instruction.integer) + case .Cond_Br: + if !valid_value(instructions, instruction.a, types.BOOL, &emitter.module.types) { + fmt.sbprintf(&emitter.builder, " br label %%bro_block_%d\n", u32(instruction.target)) + continue + } + strings.write_string(&emitter.builder, " br i1 ") + write_operand(&emitter.builder, instructions, instruction.a, types.BOOL, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", label %%bro_block_%d, label %%bro_block_%d\n", instruction.integer, u32(instruction.target)) case .Trap: message := diagnostic_message(emitter, instruction.diagnostic, instruction.span, "invalid recovered source") emit_trap_call(emitter, message) diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 9e96447..463a573 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -13,10 +13,19 @@ State :: struct { instructions: [dynamic]ir.Instruction, local_values: []ir.Instruction_Id, local_slots: []ir.Instruction_Id, + func_locals: []hir.Local, + func_result: types.Type, expr_stack: [dynamic]Lower_Expr_Frame, + next_label: i64, allocator: mem.Allocator, } +fresh_label :: proc(state: ^State) -> i64 { + id := state.next_label + state.next_label += 1 + return id +} + append_instruction :: proc(state: ^State, instruction: ir.Instruction) -> ir.Instruction_Id { id := ir.instruction_id(len(state.instructions)) append(&state.instructions, instruction) @@ -265,6 +274,77 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi target=ir.INVALID_REF, a=begin, b=fallback, diagnostic=source.INVALID_DIAGNOSTIC, }) + case .Not: + value := lower_nested_expr(state, expr.left) + return append_instruction(state, ir.Instruction{ + op=.Not, span=expr.span, type=types.BOOL, + target=ir.INVALID_REF, a=value, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .Eq, .Ne, .Lt, .Le, .Gt, .Ge: + left := lower_nested_expr(state, expr.left) + right := lower_nested_expr(state, expr.right) + predicate := ir.Compare_Predicate.Eq + #partial switch expr.kind { + case .Eq: predicate = .Eq + case .Ne: predicate = .Ne + case .Lt: predicate = .Lt + case .Le: predicate = .Le + case .Gt: predicate = .Gt + case .Ge: predicate = .Ge + } + return append_instruction(state, ir.Instruction{ + op=.Compare, span=expr.span, type=types.BOOL, integer=i64(predicate), + target=ir.INVALID_REF, a=left, b=right, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .And, .Or: + // Short-circuit via a bool slot: store the left operand, branch on it, and + // only evaluate/store the right operand when needed. Avoids phi nodes. + slot := append_instruction(state, ir.Instruction{ + op=.Alloca, span=expr.span, type=types.BOOL, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + left := lower_nested_expr(state, expr.left) + append_instruction(state, ir.Instruction{ + op=.Store, span=expr.span, type=types.BOOL, + target=ir.INVALID_REF, a=slot, b=left, diagnostic=source.INVALID_DIAGNOSTIC, + }) + rhs_lbl := fresh_label(state) + done_lbl := fresh_label(state) + true_target := rhs_lbl if expr.kind == .And else done_lbl + false_target := done_lbl if expr.kind == .And else rhs_lbl + append_instruction(state, ir.Instruction{ + op=.Cond_Br, span=expr.span, type=types.VOID, + a=left, integer=true_target, target=ir.Ref(u32(false_target)), + b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, + }) + append_instruction(state, ir.Instruction{ + op=.Label, span=expr.span, type=types.VOID, integer=rhs_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + right := lower_nested_expr(state, expr.right) + append_instruction(state, ir.Instruction{ + op=.Store, span=expr.span, type=types.BOOL, + target=ir.INVALID_REF, a=slot, b=right, diagnostic=source.INVALID_DIAGNOSTIC, + }) + append_instruction(state, ir.Instruction{ + op=.Br, span=expr.span, type=types.VOID, integer=done_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + append_instruction(state, ir.Instruction{ + op=.Label, span=expr.span, type=types.VOID, integer=done_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return append_instruction(state, ir.Instruction{ + op=.Load, span=expr.span, type=types.BOOL, + target=ir.INVALID_REF, a=slot, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) } return append_recovery_value(state, expr.span, expr.type, expr.diagnostic) } @@ -295,14 +375,15 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { case .Invalid: last = append_recovery_value(state, expr.span, expr.type, expr.diagnostic) _ = pop(&stack) - case .Integer, .Float: + case .Integer, .Float, .Bool: last = append_instruction(state, ir.Instruction{ op=.Const, span=expr.span, type=expr.type, integer=expr.integer, target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, }) _ = pop(&stack) case .String, .Array, .Struct, .None, .Optional_Some, .Address, .Deref, - .Index, .Slice, .Field, .Length, .Slice_Ptr, .Unwrap, .Orelse: + .Index, .Slice, .Field, .Length, .Slice_Ptr, .Unwrap, .Orelse, + .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or: last = lower_compound_expr(state, frame.expr) _ = pop(&stack) case .Local: @@ -453,10 +534,155 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { return last } +lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) { + hir_module := state.hir_module + for statement_id in statements { + statement := hir_module.statements[statement_id] + switch statement.kind { + case .Declaration: + value := lower_expr(state, statement.expr) + if statement.local == hir.INVALID_LOCAL || int(statement.local) >= len(state.func_locals) { + append_instruction(state, ir.Instruction{ + op=.Trap, span=statement.span, type=types.VOID, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=statement.diagnostic, + }) + continue + } + local := state.func_locals[statement.local] + slot := append_instruction(state, ir.Instruction{ + op=.Alloca, + span=statement.span, + type=local.type, + target=ir.local_ref(ir.Local_Id(statement.local)), + a=ir.INVALID_INSTRUCTION, + b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + state.local_slots[statement.local] = slot + append_instruction(state, ir.Instruction{ + op=.Store, + span=statement.span, + type=local.type, + target=ir.INVALID_REF, + a=slot, + b=value, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .Assignment: + value := lower_expr(state, statement.expr) + slot := ir.INVALID_INSTRUCTION + value_type := types.INVALID + if statement.target != hir.INVALID_EXPR { + slot = lower_location(state, statement.target, true) + if int(statement.target) < len(hir_module.exprs) { + value_type = hir_module.exprs[statement.target].type + } + } else if statement.local != hir.INVALID_LOCAL && int(statement.local) < len(state.local_slots) { + slot = state.local_slots[statement.local] + value_type = state.func_locals[statement.local].type + } + if slot == ir.INVALID_INSTRUCTION || !types.is_valid(value_type) { + append_instruction(state, ir.Instruction{ + op=.Trap, span=statement.span, type=types.VOID, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=statement.diagnostic, + }) + continue + } + append_instruction(state, ir.Instruction{ + op=.Store, + span=statement.span, + type=value_type, + target=ir.INVALID_REF, + a=slot, + b=value, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + case .Return: + if statement.expr == hir.INVALID_EXPR { + append_instruction(state, ir.Instruction{ + op=.Return_Void, + span=statement.span, + type=types.VOID, + target=ir.INVALID_REF, + a=ir.INVALID_INSTRUCTION, + b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } else { + value := lower_expr(state, statement.expr) + append_instruction(state, ir.Instruction{ + op=.Return, + span=statement.span, + type=state.func_result, + target=ir.INVALID_REF, + a=value, + b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + case .Expression, .Sink: + _ = lower_expr(state, statement.expr) + case .Trap: + append_instruction(state, ir.Instruction{ + op=.Trap, + span=statement.span, + type=types.VOID, + target=ir.INVALID_REF, + a=ir.INVALID_INSTRUCTION, + b=ir.INVALID_INSTRUCTION, + diagnostic=statement.diagnostic, + }) + case .If: + cond := lower_expr(state, statement.expr) + has_else := statement.else_body != nil + then_lbl := fresh_label(state) + else_lbl := fresh_label(state) if has_else else then_lbl + merge_lbl := fresh_label(state) + false_target := else_lbl if has_else else merge_lbl + append_instruction(state, ir.Instruction{ + op=.Cond_Br, span=statement.span, type=types.VOID, + a=cond, integer=then_lbl, target=ir.Ref(u32(false_target)), + b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, + }) + append_instruction(state, ir.Instruction{ + op=.Label, span=statement.span, type=types.VOID, integer=then_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + lower_statements(state, statement.then_body) + append_instruction(state, ir.Instruction{ + op=.Br, span=statement.span, type=types.VOID, integer=merge_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + if has_else { + append_instruction(state, ir.Instruction{ + op=.Label, span=statement.span, type=types.VOID, integer=else_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + lower_statements(state, statement.else_body) + append_instruction(state, ir.Instruction{ + op=.Br, span=statement.span, type=types.VOID, integer=merge_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + append_instruction(state, ir.Instruction{ + op=.Label, span=statement.span, type=types.VOID, integer=merge_lbl, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + } +} + lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: mem.Allocator) -> []ir.Instruction { state := State{ hir_module=hir_module, allocator=allocator, + func_locals=function.locals, + func_result=function.result, local_values=make([]ir.Instruction_Id, len(function.locals), allocator), local_slots=make([]ir.Instruction_Id, len(function.locals), allocator), } @@ -497,104 +723,7 @@ lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: m state.local_slots[local_id] = slot } - for statement_id in function.body { - statement := hir_module.statements[statement_id] - switch statement.kind { - case .Declaration: - value := lower_expr(&state, statement.expr) - if statement.local == hir.INVALID_LOCAL || int(statement.local) >= len(function.locals) { - append_instruction(&state, ir.Instruction{ - op=.Trap, span=statement.span, type=types.VOID, - target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=statement.diagnostic, - }) - continue - } - local := function.locals[statement.local] - slot := append_instruction(&state, ir.Instruction{ - op=.Alloca, - span=statement.span, - type=local.type, - target=ir.local_ref(ir.Local_Id(statement.local)), - a=ir.INVALID_INSTRUCTION, - b=ir.INVALID_INSTRUCTION, - diagnostic=source.INVALID_DIAGNOSTIC, - }) - state.local_slots[statement.local] = slot - append_instruction(&state, ir.Instruction{ - op=.Store, - span=statement.span, - type=local.type, - target=ir.INVALID_REF, - a=slot, - b=value, - diagnostic=source.INVALID_DIAGNOSTIC, - }) - case .Assignment: - value := lower_expr(&state, statement.expr) - slot := ir.INVALID_INSTRUCTION - value_type := types.INVALID - if statement.target != hir.INVALID_EXPR { - slot = lower_location(&state, statement.target, true) - if int(statement.target) < len(hir_module.exprs) { - value_type = hir_module.exprs[statement.target].type - } - } else if statement.local != hir.INVALID_LOCAL && int(statement.local) < len(state.local_slots) { - slot = state.local_slots[statement.local] - value_type = function.locals[statement.local].type - } - if slot == ir.INVALID_INSTRUCTION || !types.is_valid(value_type) { - append_instruction(&state, ir.Instruction{ - op=.Trap, span=statement.span, type=types.VOID, - target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=statement.diagnostic, - }) - continue - } - append_instruction(&state, ir.Instruction{ - op=.Store, - span=statement.span, - type=value_type, - target=ir.INVALID_REF, - a=slot, - b=value, - diagnostic=source.INVALID_DIAGNOSTIC, - }) - case .Return: - if statement.expr == hir.INVALID_EXPR { - append_instruction(&state, ir.Instruction{ - op=.Return_Void, - span=statement.span, - type=types.VOID, - target=ir.INVALID_REF, - a=ir.INVALID_INSTRUCTION, - b=ir.INVALID_INSTRUCTION, - diagnostic=source.INVALID_DIAGNOSTIC, - }) - } else { - value := lower_expr(&state, statement.expr) - append_instruction(&state, ir.Instruction{ - op=.Return, - span=statement.span, - type=function.result, - target=ir.INVALID_REF, - a=value, - b=ir.INVALID_INSTRUCTION, - diagnostic=source.INVALID_DIAGNOSTIC, - }) - } - case .Expression, .Sink: - _ = lower_expr(&state, statement.expr) - case .Trap: - append_instruction(&state, ir.Instruction{ - op=.Trap, - span=statement.span, - type=types.VOID, - target=ir.INVALID_REF, - a=ir.INVALID_INSTRUCTION, - b=ir.INVALID_INSTRUCTION, - diagnostic=statement.diagnostic, - }) - } - } + lower_statements(&state, function.body) if len(state.instructions) == 0 || (state.instructions[len(state.instructions)-1].op != .Return && state.instructions[len(state.instructions)-1].op != .Return_Void) { diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index cc3bea9..a2d234c 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -20,6 +20,11 @@ Parser :: struct { file: ast.File_Id, cursor: int, delimiter_depth: int, + // Suppresses `Name { ... }` struct-literal parsing at delimiter depth 0 so a + // control-flow condition like `if foo { ... }` does not swallow the block as a + // struct literal. Nested `(`/`[`/call-arg contexts (delimiter_depth > 0) still + // allow struct literals. + no_struct_literal: bool, } MAX_EXPRESSION_NESTING :: 256 @@ -90,7 +95,7 @@ is_type_token :: proc(kind: token.Kind) -> bool { .Keyword_C_Short, .Keyword_C_Ushort, .Keyword_C_Int, .Keyword_C_Uint, .Keyword_C_Long, .Keyword_C_Ulong, .Keyword_C_Longlong, .Keyword_C_Ulonglong, .Keyword_C_Float, .Keyword_C_Double, .Keyword_C_Longdouble, - .Keyword_Void, .Keyword_C_Func, .Identifier, .Question, .At, .Star, .Left_Bracket: + .Keyword_Void, .Keyword_Bool, .Keyword_C_Func, .Identifier, .Question, .At, .Star, .Left_Bracket: return true } return false @@ -291,6 +296,9 @@ parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax { case .Keyword_Void: advance(parser) return types.VOID + case .Keyword_Bool: + advance(parser) + return types.BOOL case .Keyword_C_Func: advance(parser) if _, ok := allow(parser, .Left_Paren); !ok { @@ -567,6 +575,16 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id { right=ast.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) + case .Keyword_True, .Keyword_False: + advance(parser) + return add_expr(parser, ast.Expr{ + kind=.Bool, + span=tok.span, + integer=1 if tok.kind == .Keyword_True else 0, + left=ast.INVALID_EXPR, + right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) case .Left_Bracket: return parse_array_literal(parser, nesting) case .Identifier: @@ -583,7 +601,7 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id { if current(parser).kind == .Left_Paren { return parse_call(parser, qualifier, first, name, nesting) } - if current(parser).kind == .Left_Brace { + if current(parser).kind == .Left_Brace && !(parser.no_struct_literal && parser.delimiter_depth == 0) { return parse_struct_literal(parser, qualifier, first, name, nesting) } return add_expr(parser, ast.Expr{ @@ -633,15 +651,36 @@ infix_binding_power :: proc(kind: token.Kind) -> (left, right: int, ok: bool) { #partial switch kind { case .Keyword_Orelse: return 2, 3, true + case .Keyword_Or: + return 4, 5, true + case .Keyword_And: + return 6, 7, true + case .Equal_Equal, .Bang_Equal, .Less, .Less_Equal, .Greater, .Greater_Equal: + return 8, 9, true case .Plus: return 10, 11, true } return 0, 0, false } +infix_expr_kind :: proc(kind: token.Kind) -> ast.Expr_Kind { + #partial switch kind { + case .Keyword_Orelse: return .Orelse + case .Keyword_Or: return .Or + case .Keyword_And: return .And + case .Equal_Equal: return .Eq + case .Bang_Equal: return .Ne + case .Less: return .Lt + case .Less_Equal: return .Le + case .Greater: return .Gt + case .Greater_Equal: return .Ge + case: return .Add + } +} + prefix_binding_power :: proc(kind: token.Kind) -> (right: int, ok: bool) { #partial switch kind { - case .Minus, .Ampersand: + case .Minus, .Ampersand, .Bang: return 20, true } return 0, false @@ -663,8 +702,13 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int } operand := parse_expression_bp(parser, right_power, nesting+1) operand_expr := parser.module.exprs[operand] + prefix_kind := ast.Expr_Kind.Negate + #partial switch operator.kind { + case .Ampersand: prefix_kind = .Address + case .Bang: prefix_kind = .Not + } left = add_expr(parser, ast.Expr{ - kind=.Address if operator.kind == .Ampersand else .Negate, + kind=prefix_kind, span=span_from(operator.span, operand_expr.span), left=operand, right=ast.INVALID_EXPR, @@ -787,7 +831,7 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int left_expr := parser.module.exprs[left] right_expr := parser.module.exprs[right] left = add_expr(parser, ast.Expr{ - kind=.Orelse if operator.kind == .Keyword_Orelse else .Add, + kind=infix_expr_kind(operator.kind), span=span_from(left_expr.span, right_expr.span), left=left, right=right, @@ -881,6 +925,9 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id { if current(parser).kind == .Keyword_Return { return parse_return(parser) } + if current(parser).kind == .Keyword_If { + return parse_if(parser) + } if current(parser).kind == .Identifier || current(parser).kind == .Underscore { start_cursor := parser.cursor @@ -995,8 +1042,76 @@ parse_params :: proc(parser: ^Parser) -> ([]ast.Param, bool) { return params[:], variadic } +// parse_block parses a brace-delimited statement sequence `{ ... }`, consuming +// both braces, and returns the contained statement ids. Shared by function +// bodies and control-flow blocks. +parse_block :: proc(parser: ^Parser) -> []ast.Stmt_Id { + body: [dynamic]ast.Stmt_Id + body.allocator = parser.module.allocator + if _, ok := allow(parser, .Left_Brace); !ok { + source.add(parser.diagnostics, current(parser).span, "expected '{' to open a block") + return body[:] + } + skip_newlines(parser) + for current(parser).kind != .Right_Brace && current(parser).kind != .Eof { + append(&body, parse_statement(parser)) + if diagnostic := finish_statement(parser, true); diagnostic != source.INVALID_DIAGNOSTIC { + statement_id := ast.stmt_id(len(parser.module.statements)) + append(&parser.module.statements, ast.Stmt{ + kind=.Invalid, + span=current(parser).span, + expr=ast.INVALID_EXPR, + diagnostic=diagnostic, + }) + append(&body, statement_id) + } + } + if _, ok := allow(parser, .Right_Brace); !ok { + source.add(parser.diagnostics, current(parser).span, "expected '}' to close a block") + } + return body[:] +} + +parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id { + start := advance(parser) // consume 'if' + skip_newlines(parser) + saved := parser.no_struct_literal + parser.no_struct_literal = true + condition := parse_expression(parser) + parser.no_struct_literal = saved + skip_newlines(parser) + then_body := parse_block(parser) + else_body: []ast.Stmt_Id = nil + saved_cursor := parser.cursor + skip_newlines(parser) + if current(parser).kind == .Keyword_Else { + advance(parser) + skip_newlines(parser) + if current(parser).kind == .Keyword_If { + nested := parse_if(parser) + single := make([]ast.Stmt_Id, 1, parser.module.allocator) + single[0] = nested + else_body = single + } else { + else_body = parse_block(parser) + } + } else { + parser.cursor = saved_cursor + } + id := ast.stmt_id(len(parser.module.statements)) + append(&parser.module.statements, ast.Stmt{ + kind=.If, + span=span_from(start.span, previous(parser).span), + expr=condition, + body=then_body, + else_body=else_body, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return id +} + parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { - func_token := advance(parser) + advance(parser) if _, ok := allow(parser, .Left_Paren); !ok { source.add(parser.diagnostics, current(parser).span, "expected '(' after 'func'") } @@ -1030,29 +1145,8 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { }) return } - advance(parser) - - body: [dynamic]ast.Stmt_Id - body.allocator = parser.module.allocator - skip_newlines(parser) - for current(parser).kind != .Right_Brace && current(parser).kind != .Eof { - append(&body, parse_statement(parser)) - if diagnostic := finish_statement(parser, true); diagnostic != source.INVALID_DIAGNOSTIC { - statement_id := ast.stmt_id(len(parser.module.statements)) - append(&parser.module.statements, ast.Stmt{ - kind=.Invalid, - span=current(parser).span, - expr=ast.INVALID_EXPR, - diagnostic=diagnostic, - }) - append(&body, statement_id) - } - } - end = current(parser) - if _, ok := allow(parser, .Right_Brace); !ok { - source.add(parser.diagnostics, current(parser).span, "expected '}' after function body") - end = func_token - } + body := parse_block(parser) + end = previous(parser) _ = ast.function_id(len(parser.module.functions)) append(&parser.module.functions, ast.Function{ span=span_from(name.span, end.span), @@ -1064,7 +1158,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { variadic=variadic, params=params, result=result, - body=body[:], + body=body, diagnostic=source.INVALID_DIAGNOSTIC, }) } diff --git a/compiler/token/token.odin b/compiler/token/token.odin index c25b34a..63a13f6 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -15,6 +15,13 @@ Kind :: enum u8 { Underscore, Colon_Colon, Equal, + Equal_Equal, + Bang, + Bang_Equal, + Less, + Less_Equal, + Greater, + Greater_Equal, Plus, Minus, Dot, @@ -42,7 +49,14 @@ Kind :: enum u8 { Keyword_Mut, Keyword_None, Keyword_Orelse, + Keyword_And, + Keyword_Or, + Keyword_If, + Keyword_Else, + Keyword_True, + Keyword_False, Keyword_Void, + Keyword_Bool, Keyword_Int, Keyword_I8, Keyword_I16, diff --git a/compiler/types/types.odin b/compiler/types/types.odin index ccbede9..7624cf6 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -40,6 +40,8 @@ C_FLOAT :: Type(26) C_DOUBLE :: Type(27) C_LONGDOUBLE :: Type(28) +BOOL :: Type(29) + DYNAMIC_START :: Type(64) Numeric_Category :: enum u8 { @@ -236,6 +238,8 @@ kind :: proc(value: Type, store: ^Store = nil) -> Kind { return .Void case INT: return .Int_Constraint + case BOOL: + return .Scalar } if value >= I8 && value <= C_LONGDOUBLE { return .Scalar @@ -268,6 +272,10 @@ is_void :: proc(value: Type) -> bool { return value == VOID } +is_bool :: proc(value: Type) -> bool { + return value == BOOL +} + is_constraint :: proc(value: Type) -> bool { return value == INT } @@ -320,6 +328,8 @@ category :: proc(value: Type, selected := target.DEFAULT) -> Numeric_Category { bits :: proc(value: Type, selected := target.DEFAULT) -> int { switch value { + case BOOL: + return 1 case I8, U8: return 8 case I16, U16: @@ -945,6 +955,7 @@ name :: proc(value: Type) -> string { switch value { case INVALID: return "" case VOID: return "void" + case BOOL: return "bool" case INT: return "int" case I8: return "i8" case I16: return "i16" diff --git a/compiler_tests.odin b/compiler_tests.odin index b46161e..f79ec1f 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -1793,6 +1793,27 @@ sentinel_pointer_views_compile_and_run :: proc(t: ^testing.T) { testing.expect_value(t, state.exit_code, 303) } +@(test) +control_flow_compiles_and_runs :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-control-flow" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/programs/control_flow", output) + testing.expect_value(t, status, 0) + state, stdout, stderr, _ := os2.process_exec( + os2.Process_Desc{command=[]string{output}}, + context.allocator, + ) + defer delete(stdout) + defer delete(stderr) + // if / else if / else, comparisons, logical and/or/not, bool locals, and + // block scoping together produce 42. + testing.expect_value(t, state.exit_code, 42) + // Short-circuit: `noisy()` is never reached, so its output must be absent, + // while the taken or-branch must print. + testing.expect(t, !strings.contains(string(stdout), "rhs-evaluated")) + testing.expect(t, strings.contains(string(stdout), "or-taken")) +} + @(test) foreign_function_links_from_c_source :: proc(t: ^testing.T) { output := "/tmp/brolang-test-foreign-source" diff --git a/examples/programs/control_flow/main.bro b/examples/programs/control_flow/main.bro new file mode 100644 index 0000000..6fba07a --- /dev/null +++ b/examples/programs/control_flow/main.bro @@ -0,0 +1,69 @@ +# Milestone 5 foundation: booleans, comparisons, logical ops, if/else if/else. + +printf :: c_func(format *c_char, ...) c_int + +# Returns a distinct code per range using if / else if / else and comparisons. +classify :: func(n i32) i32 { + if n < 0 { + return 1 + } else if n == 0 { + return 2 + } else if n >= 100 { + return 3 + } else { + return 4 + } +} + +# A bool-returning function with a visible side effect, used to prove +# short-circuit evaluation: it must only print when actually evaluated. +noisy :: func() bool { + _ = printf("rhs-evaluated\n") + return true +} + +main :: func() i32 { + total i32 = 0 + + # comparisons drive if / else if / else + total = total + classify(-5) # 1 + total = total + classify(0) # 2 + total = total + classify(250) # 3 + total = total + classify(42) # 4 -> 10 + + # bool variables and logical and / or / not + a :: true + b :: false + if a and !b { + total = total + 10 # 20 + } + if b or a { + total = total + 10 # 30 + } + if !(a and b) { + total = total + 5 # 35 + } + + # block scoping: inner x shadows outer x, outer is unchanged after the block + x i32 = 1 + if x == 1 { + x i32 = 100 + if x == 100 { + total = total + 5 # 40 + } + } + if x == 1 { + total = total + 2 # 42 + } + + # short-circuit: `false and noisy()` must NOT call noisy() + if false and noisy() { + _ = printf("unreachable-and\n") + } + # short-circuit: `true or noisy()` must NOT call noisy() + if true or noisy() { + _ = printf("or-taken\n") + } + + return total # expect exit code 42 +}