From 6512ccd543a0e50994b49460d82d55c43cdcbecf Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Mon, 22 Jun 2026 21:20:15 +0200 Subject: [PATCH] compound assignment --- LANGUAGE.md | 6 +- TODO.md | 15 +- compiler/ast/ast.odin | 15 + compiler/checker/checker.odin | 173 ++++-- compiler/hir/hir.odin | 16 + compiler/ir/ir.odin | 3 + compiler/lexer/lexer.odin | 33 +- compiler/llvm/llvm.odin | 186 +++++-- compiler/lower/lower.odin | 56 +- compiler/parser/parser.odin | 34 +- compiler/token/token.odin | 5 + compiler_tests.odin | 503 ++++++++++++++++++ examples/interop/header/app/main.bro | 4 +- .../programs/compound_assignment/main.bro | 45 ++ 14 files changed, 995 insertions(+), 99 deletions(-) create mode 100644 examples/programs/compound_assignment/main.bro diff --git a/LANGUAGE.md b/LANGUAGE.md index 3ece332..748a17e 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -15,8 +15,8 @@ - exact-width `i8` through `i64`, `u8` through `u64`, `f32`, `f64`, `isize`, `usize`, `bool`, `void`, and inferred integer-constrained `int` - target-dependent atomic C primitives from `c_char` through `c_longdouble` - C primitives remain semantically distinct from exact-width Brolang primitives until target lowering -- contextual integer and character literals and constant folding of addition and negation trees -- strict numeric conversions, checked integer addition, and unary negation +- contextual integer and character literals and constant folding of arithmetic and negation trees +- strict numeric conversions, binary `+ - * /` with checked integer overflow and divide-by-zero traps (floats follow IEEE), and unary negation - boolean literals, comparisons, unary `!`, and short-circuiting `and` / `or` - arrays `[N]T`, sentinel arrays `[N;S]T`, single-item pointers `@T`, many-item pointers `*T`, sentinel many-item pointers `[*;S]T`, pointer offsets, slices, and explicit slicing - immutable UTF-8 string literals typed as pointers to static sentinel arrays: `@[N;0]u8` @@ -29,7 +29,7 @@ - source-order native structs, defined or opaque `c_struct`, and keyed record literals - complete plain imported C structs and unions as runtime values; incomplete or unsupported-layout records remain pointer-only - C function pointer types as pointer-sized runtime values, including manual `*c_func(...) T` spelling and nullable imported callback typedefs -- postfix pointer dereference, general writable locations, function calls, assignments, and returns +- postfix pointer dereference, general writable locations, function calls, assignments, compound assignment (`+= -= *= /=`) with single-evaluation lvalues, and returns - first-class exclusive and inclusive integer ranges: `start..end` and `start..=end` - boolean `if` statements and `while` loops with optional post-iteration assignment/expression clauses - `for` loops over ranges, arrays, slices, and pointers-to-arrays, with copy, pointer, and optional `usize` index captures diff --git a/TODO.md b/TODO.md index e3f1fbe..27311c1 100644 --- a/TODO.md +++ b/TODO.md @@ -100,7 +100,7 @@ - `while condition : i = i + 1 { ... }` - execute the update after each completed iteration - the condition and update may be parenthesized independently for visual clarity - update targets must already be declared and mutable; loops do not introduce implicit induction variables - - compound assignment (`+=`) remains deferred + - update clauses support ordinary and compound assignment - ranges (implemented; see section below) - for loops (implemented; operates on ranges, arrays, slices, and pointers-to-arrays). examples: - `for items |item| { ... }` - capture just the `item` value in the array/slice (uses copy semantics, i.e. gets a `T`) @@ -112,7 +112,18 @@ - `for 0..(len) |i| { ... }` or equivalently `for 0..=(len - 1) |i| { ... }` - calculating range bounds, expressions must be parenthesized - for all conditionals/guards, parentheses are optional but allowed for visual clarity -6. compound assignment: `+=`, `-=`, `*=`, `/=` +6. compound assignment: `+=`, `-=`, `*=`, `/=` (implemented) + - added the binary arithmetic operators `-`, `*`, `/` (previously only `+` existed); `*`/`/` + bind tighter than `+`/`-`, and prefix `-` (negation) is unchanged + - compound assignments preserve the target, operator, and right-hand side explicitly through + parsing and checking; lowering computes the target address once, then loads, applies the + operation, and stores through that address + - side-effecting index, field-base, and dereference expressions are evaluated once in + left-to-right order + - integer arithmetic traps on overflow (`Sub_Checked`/`Mul_Checked` via the LLVM + `.with.overflow` intrinsics) and integer `/` traps on divide-by-zero and `INT_MIN / -1`; + floats follow IEEE (`fadd`/`fsub`/`fmul`/`fdiv`, no trap) + - constant folding (global initializers) covers `-`, `*`, `/` alongside `+` 7. enums (native and c interop) (see below) diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 0e2a260..1835556 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -84,6 +84,9 @@ Expr_Kind :: enum u8 { Negate, Not, Add, + Sub, + Mul, + Div, Eq, Ne, Lt, @@ -126,6 +129,14 @@ Stmt_Kind :: enum u8 { For, } +Assignment_Op :: enum u8 { + Set, + Add, + Sub, + Mul, + Div, +} + Stmt :: struct { kind: Stmt_Kind, span: source.Span, @@ -134,6 +145,10 @@ Stmt :: struct { type: Type_Syntax, immutable: bool, pointer_capture: bool, + // Assignments store the lvalue in `target`, the right-hand side in `expr`, + // and the source operator in `assignment_op`. `Set` is ordinary `=`; + // the arithmetic variants are `+=`, `-=`, `*=`, and `/=`. + assignment_op: Assignment_Op, target: Expr_Id, expr: Expr_Id, // `If` statements use `expr` as the condition, `captures` as optional diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 0dabe99..745c62b 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -65,6 +65,7 @@ Constant_Kind :: enum { Not_Constant, Value, Overflow, + Div_By_Zero, } Constant :: struct { @@ -145,7 +146,8 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant { continue } expr := checker.ast_module.exprs[frame.expr] - if expr.kind != .Add && expr.kind != .Negate { + if expr.kind != .Add && expr.kind != .Sub && expr.kind != .Mul && + expr.kind != .Div && expr.kind != .Negate { result := Constant{kind = .Not_Constant} if expr.kind == .Integer { result = Constant{kind = .Value, value = i128(expr.integer)} @@ -168,7 +170,9 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant { operand = checker.constants[expr.left] } result := Constant{kind = .Not_Constant} - if operand.kind == .Overflow { + if operand.kind == .Div_By_Zero { + result = Constant{kind = .Div_By_Zero} + } else if operand.kind == .Overflow { result = Constant{kind = .Overflow} } else if operand.kind == .Value { value, overflow := intrinsics.overflow_sub(i128(0), operand.value) @@ -195,11 +199,30 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant { right = checker.constants[expr.right] } result := Constant{kind = .Not_Constant} - if left.kind == .Overflow || right.kind == .Overflow { + if left.kind == .Div_By_Zero || right.kind == .Div_By_Zero { + result = Constant{kind = .Div_By_Zero} + } else if left.kind == .Overflow || right.kind == .Overflow { result = Constant{kind = .Overflow} } else if left.kind == .Value && right.kind == .Value { - value, overflow := intrinsics.overflow_add(left.value, right.value) - result = Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value} + value: i128 + overflow: bool + div_by_zero: bool + #partial switch expr.kind { + case .Sub: value, overflow = intrinsics.overflow_sub(left.value, right.value) + case .Mul: value, overflow = intrinsics.overflow_mul(left.value, right.value) + case .Div: + if right.value == 0 { + div_by_zero = true + } else { + value = left.value / right.value + } + case: value, overflow = intrinsics.overflow_add(left.value, right.value) + } + switch { + case div_by_zero: result = Constant{kind = .Div_By_Zero} + case overflow: result = Constant{kind = .Overflow} + case: result = Constant{kind = .Value, value = value} + } } checker.constants[frame.expr] = result _ = pop(&stack) @@ -611,7 +634,7 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as } case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Keyed: append(&stack, expr.left) - case .Add, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: + case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: append(&stack, expr.left, expr.right) case .Invalid, .Integer, .Float, .String, .Bool, .None, .Name: } @@ -1163,7 +1186,8 @@ infer_expr :: proc( expr := checker.ast_module.exprs[frame.expr] if frame.stage == 0 { constant := eval_constant(checker, frame.expr) - if constant.kind == .Overflow || (constant.kind == .Value && !fits_i64(constant.value)) { + if constant.kind == .Overflow || constant.kind == .Div_By_Zero || + (constant.kind == .Value && !fits_i64(constant.value)) { last = types.I64 _ = pop(&stack) continue @@ -1244,7 +1268,7 @@ infer_expr :: proc( case .Negate: stack[frame_index].stage = 5 append(&stack, Infer_Frame{expr=expr.left, template=ast.INVALID_FUNCTION}) - case .Add: + case .Add, .Sub, .Mul, .Div: stack[frame_index].stage = 1 append(&stack, Infer_Frame{expr=expr.left, template=ast.INVALID_FUNCTION}) case .Call: @@ -1338,7 +1362,7 @@ infer_expr :: proc( continue } if frame.stage == 2 { - if types.is_many_pointer(frame.left, &checker.module.types) && types.is_concrete_integer(last) { + if expr.kind == .Add && types.is_many_pointer(frame.left, &checker.module.types) && types.is_concrete_integer(last) { last = frame.left } else { last = types.widest(frame.left, last) @@ -1847,6 +1871,10 @@ build_constant_expr :: proc( if types.is_concrete_integer(expected) { recovery_type = expected } + if constant.kind == .Div_By_Zero { + id := source.add(checker.diagnostics, expr.span, "division by zero in constant expression") + return invalid_hir_expr(checker, expr.span, id, recovery_type) + } if constant.kind == .Overflow || (!types.is_concrete_integer(expected) && !fits_i64(constant.value)) { id := source.add( @@ -2485,6 +2513,45 @@ build_compound_expr :: proc( } } +// build_binary_arith constructs the HIR node for `left op right`, where `op` is +// an arithmetic AST kind (`Add`/`Sub`/`Mul`/`Div`). It models many-pointer `+` +// as `Pointer_Add`, coerces both operands to their common type, and emits the +// "arithmetic requires compatible numeric operands" diagnostic when they have no +// shared numeric type. +build_binary_arith :: proc( + checker: ^Checker, + op: ast.Expr_Kind, + left, right: hir.Expr_Id, + span: source.Span, +) -> hir.Expr_Id { + // Pointer arithmetic is only defined for `+` (many-pointer + usize). + if op == .Add && + types.is_many_pointer(checker.module.exprs[left].type, &checker.module.types) && + types.equal(checker.module.exprs[right].type, types.USIZE) { + return add_hir_expr(checker, hir.Expr{ + kind=.Pointer_Add, span=span, type=checker.module.exprs[left].type, + left=left, right=right, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + result := types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type) + if !types.is_concrete_scalar(result) { + id := source.add(checker.diagnostics, span, "arithmetic requires compatible numeric operands") + return invalid_hir_expr(checker, span, id) + } + result_kind := hir.Expr_Kind.Add + #partial switch op { + case .Sub: result_kind = .Sub + case .Mul: result_kind = .Mul + case .Div: result_kind = .Div + } + coerced_left := coerce_expr(checker, left, result, checker.module.exprs[left].span) + coerced_right := coerce_expr(checker, right, result, checker.module.exprs[right].span) + return add_hir_expr(checker, hir.Expr{ + kind=result_kind, span=span, type=result, left=coerced_left, right=coerced_right, + target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, + }) +} + build_expr :: proc( checker: ^Checker, expr_id: ast.Expr_Id, @@ -2520,7 +2587,7 @@ build_expr :: proc( expr := checker.ast_module.exprs[frame.expr] if frame.stage == 0 { constant := eval_constant(checker, frame.expr) - if constant.kind == .Value || constant.kind == .Overflow { + if constant.kind == .Value || constant.kind == .Overflow || constant.kind == .Div_By_Zero { last = build_constant_expr(checker, expr, constant, frame.expected) _ = pop(&stack) continue @@ -2609,7 +2676,7 @@ build_expr :: proc( case .Negate: stack[frame_index].stage = 5 append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION}) - case .Add: + case .Add, .Sub, .Mul, .Div: stack[frame_index].stage = 1 // Preserve assignment/return context for literal operands, e.g. // assigning `i + 1` back into a `u32` local. @@ -2768,29 +2835,7 @@ build_expr :: proc( continue } if frame.stage == 2 { - left := frame.left - right := last - if types.is_many_pointer(checker.module.exprs[left].type, &checker.module.types) && - types.equal(checker.module.exprs[right].type, types.USIZE) { - last = add_hir_expr(checker, hir.Expr{ - kind=.Pointer_Add, span=expr.span, type=checker.module.exprs[left].type, - left=left, right=right, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC, - }) - _ = pop(&stack) - continue - } - result := types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type) - if !types.is_concrete_scalar(result) { - id := source.add(checker.diagnostics, expr.span, "addition requires compatible numeric operands") - last = invalid_hir_expr(checker, expr.span, id) - } else { - left = coerce_expr(checker, left, result, checker.module.exprs[left].span) - right = coerce_expr(checker, right, result, checker.module.exprs[right].span) - last = add_hir_expr(checker, hir.Expr{ - kind=.Add, span=expr.span, type=result, left=left, right=right, - target = hir.INVALID_REF, diagnostic = source.INVALID_DIAGNOSTIC, - }) - } + last = build_binary_arith(checker, expr.kind, frame.left, last, expr.span) _ = pop(&stack) continue } @@ -3061,14 +3106,64 @@ build_block :: proc( 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) + value: hir.Expr_Id + assignment_op := hir.Assignment_Op.Set + if statement.assignment_op != .Set { + rhs_expected := target_type + if types.is_many_pointer(target_type, &checker.module.types) { + rhs_expected = types.USIZE if statement.assignment_op == .Add else types.INVALID + } + value = build_expr( + checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, + rhs_expected, ctx.pkg, ctx.file, + ) + if types.is_many_pointer(target_type, &checker.module.types) { + assignment_op = .Pointer_Add + if statement.assignment_op != .Add { + id := source.add( + checker.diagnostics, + statement.span, + "many-item pointers only support '+=' compound assignment", + ) + value = invalid_hir_expr(checker, statement.span, id, types.USIZE) + } else { + value = coerce_expr(checker, value, types.USIZE, statement.span) + } + } else { + #partial switch statement.assignment_op { + case .Add: assignment_op = .Add + case .Sub: assignment_op = .Sub + case .Mul: assignment_op = .Mul + case .Div: assignment_op = .Div + } + rhs_type := checker.module.exprs[value].type + result_type := types.widest(target_type, rhs_type) + if !types.is_concrete_scalar(result_type) || + types.is_bool(result_type) { + id := source.add( + checker.diagnostics, + statement.span, + "arithmetic requires compatible numeric operands", + ) + value = invalid_hir_expr(checker, statement.span, id, target_type) + } else { + // Compound assignment stores back into the original + // target type, so only an equal or widening RHS + // conversion is permitted. + value = coerce_expr(checker, value, target_type, statement.span) + } + } + } else { + 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, + assignment_op=assignment_op, target=target_expr, expr=value, diagnostic=source.INVALID_DIAGNOSTIC, }) ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 62ff37f..53b3429 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -102,6 +102,9 @@ Expr_Kind :: enum u8 { Negate, Not, Add, + Sub, + Mul, + Div, Pointer_Add, Eq, Ne, @@ -151,6 +154,15 @@ Stmt_Kind :: enum u8 { For, } +Assignment_Op :: enum u8 { + Set, + Add, + Sub, + Mul, + Div, + Pointer_Add, +} + Stmt :: struct { kind: Stmt_Kind, span: source.Span, @@ -160,6 +172,10 @@ Stmt :: struct { expr: Expr_Id, iterator_type: types.Type, pointer_capture: bool, + // Assignments carry their operation explicitly. Arithmetic operations lower + // by computing the target address once, loading its current value, applying + // the operation to `expr`, and storing through the original address. + assignment_op: Assignment_Op, // Boolean `If` statements use `expr` as the condition. Conditional unwraps // use `unwraps` for the ordered optional expressions and capture locals, and // `guard` for the optional boolean checked after every unwrap succeeds. diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index b9cae36..e47affc 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -98,6 +98,9 @@ Opcode :: enum u8 { Decay_Array_Pointer, Neg_Checked, Add_Checked, + Sub_Checked, + Mul_Checked, + Div_Checked, Pointer_Add, Not, Compare, diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index fd8331c..19a108f 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -150,11 +150,23 @@ lex :: proc( append_token(&stream, source_file, .Greater, start, cursor) } case '+': - append_token(&stream, source_file, .Plus, cursor, cursor+1) + start := cursor cursor += 1 + if cursor < len(bytes) && bytes[cursor] == '=' { + cursor += 1 + append_token(&stream, source_file, .Plus_Equal, start, cursor) + } else { + append_token(&stream, source_file, .Plus, start, cursor) + } case '-': - append_token(&stream, source_file, .Minus, cursor, cursor+1) + start := cursor cursor += 1 + if cursor < len(bytes) && bytes[cursor] == '=' { + cursor += 1 + append_token(&stream, source_file, .Minus_Equal, start, cursor) + } else { + append_token(&stream, source_file, .Minus, start, cursor) + } case '.': start := cursor cursor += 1 @@ -176,8 +188,23 @@ lex :: proc( append_token(&stream, source_file, .At, cursor, cursor+1) cursor += 1 case '*': - append_token(&stream, source_file, .Star, cursor, cursor+1) + start := cursor cursor += 1 + if cursor < len(bytes) && bytes[cursor] == '=' { + cursor += 1 + append_token(&stream, source_file, .Star_Equal, start, cursor) + } else { + append_token(&stream, source_file, .Star, start, cursor) + } + case '/': + start := cursor + cursor += 1 + if cursor < len(bytes) && bytes[cursor] == '=' { + cursor += 1 + append_token(&stream, source_file, .Slash_Equal, start, cursor) + } else { + append_token(&stream, source_file, .Slash, start, cursor) + } case '&': append_token(&stream, source_file, .Ampersand, cursor, cursor+1) cursor += 1 diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 359a0a0..f17ac6d 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -240,7 +240,7 @@ valid_value :: proc( .Extract, .Select, .Unwrap, .Optional_Is_Some, .Optional_Value, .Orelse, .Widen, .C_Vararg_Promote, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer, - .Neg_Checked, .Add_Checked, .Pointer_Add, .Not, .Compare, .Call: + .Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked, .Pointer_Add, .Not, .Compare, .Call: return true case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, .Store, .Trap, .Label, .Br, .Cond_Br, .Return, .Return_Void: @@ -516,6 +516,117 @@ emit_entry_allocas :: proc(emitter: ^Emitter, instructions: []ir.Instruction) { } } +// emit_checked_arithmetic emits a trapping integer add/sub/mul through the LLVM +// `.with.overflow` intrinsics, or a plain floating-point operation. `mnemonic` +// is the integer intrinsic stem ("add"/"sub"/"mul"); the signed/unsigned prefix +// is chosen from the operand type. `float_op` is the matching float instruction. +emit_checked_arithmetic :: proc( + emitter: ^Emitter, + instructions: []ir.Instruction, + instruction_index: int, + instruction: ir.Instruction, + mnemonic: string, + float_op: string, + overflow_message: string, +) { + type_name := llvm_type(instruction.type, &emitter.module.types) + if types.is_float(instruction.type, emitter.module.target) { + fmt.sbprintf(&emitter.builder, " %%v%d = %s %s ", instruction_index, float_op, type_name) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, ", ") + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, "\n") + return + } + prefix := "u" if types.is_unsigned(instruction.type, emitter.module.target) else "s" + fmt.sbprintf(&emitter.builder, " %%pair%d = call ", instruction_index) + strings.write_string(&emitter.builder, "{ ") + fmt.sbprintf(&emitter.builder, "%s, i1 } @llvm.%s%s.with.overflow.%s(%s ", type_name, prefix, mnemonic, type_name, type_name) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", %s ", type_name) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ")\n") + fmt.sbprintf(&emitter.builder, " %%v%d = extractvalue ", instruction_index) + strings.write_string(&emitter.builder, "{ ") + fmt.sbprintf(&emitter.builder, "%s, i1 } %%pair%d, 0\n", type_name, instruction_index) + fmt.sbprintf(&emitter.builder, " %%overflow%d = extractvalue ", instruction_index) + strings.write_string(&emitter.builder, "{ ") + fmt.sbprintf(&emitter.builder, "%s, i1 } %%pair%d, 1\n", type_name, instruction_index) + fmt.sbprintf( + &emitter.builder, + " br i1 %%overflow%d, label %%overflow_trap%d, label %%overflow_continue%d\n", + instruction_index, + instruction_index, + instruction_index, + ) + fmt.sbprintf(&emitter.builder, "overflow_trap%d:\n", instruction_index) + message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, overflow_message) + emit_trap_call(emitter, message) + fmt.sbprintf(&emitter.builder, " unreachable\noverflow_continue%d:\n", instruction_index) +} + +// emit_checked_division emits a trapping integer division guarding divide-by-zero +// and signed `INT_MIN / -1` overflow, or a plain floating-point division. +emit_checked_division :: proc( + emitter: ^Emitter, + instructions: []ir.Instruction, + instruction_index: int, + instruction: ir.Instruction, +) { + type_name := llvm_type(instruction.type, &emitter.module.types) + if types.is_float(instruction.type, emitter.module.target) { + fmt.sbprintf(&emitter.builder, " %%v%d = fdiv %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, ", ") + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, "\n") + return + } + signed := !types.is_unsigned(instruction.type, emitter.module.target) + fmt.sbprintf(&emitter.builder, " %%divzero%d = icmp eq %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, ", 0\n") + fmt.sbprintf( + &emitter.builder, + " br i1 %%divzero%d, label %%divzero_trap%d, label %%divzero_ok%d\n", + instruction_index, + instruction_index, + instruction_index, + ) + fmt.sbprintf(&emitter.builder, "divzero_trap%d:\n", instruction_index) + zero_message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "integer division by zero") + emit_trap_call(emitter, zero_message) + fmt.sbprintf(&emitter.builder, " unreachable\ndivzero_ok%d:\n", instruction_index) + if signed { + min_value := -(i128(1) << u32(types.bits(instruction.type, emitter.module.target) - 1)) + fmt.sbprintf(&emitter.builder, " %%divminlo%d = icmp eq %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", %d\n", min_value) + fmt.sbprintf(&emitter.builder, " %%divminhi%d = icmp eq %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, ", -1\n") + fmt.sbprintf(&emitter.builder, " %%divovf%d = and i1 %%divminlo%d, %%divminhi%d\n", instruction_index, instruction_index, instruction_index) + fmt.sbprintf( + &emitter.builder, + " br i1 %%divovf%d, label %%divovf_trap%d, label %%divovf_ok%d\n", + instruction_index, + instruction_index, + instruction_index, + ) + fmt.sbprintf(&emitter.builder, "divovf_trap%d:\n", instruction_index) + ovf_message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "signed integer division overflow") + emit_trap_call(emitter, ovf_message) + fmt.sbprintf(&emitter.builder, " unreachable\ndivovf_ok%d:\n", instruction_index) + fmt.sbprintf(&emitter.builder, " %%v%d = sdiv %s ", instruction_index, type_name) + } else { + fmt.sbprintf(&emitter.builder, " %%v%d = udiv %s ", instruction_index, type_name) + } + write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, ", ") + write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) + strings.write_string(&emitter.builder, "\n") +} + emit_instruction_stream :: proc( emitter: ^Emitter, instructions: []ir.Instruction, @@ -1166,40 +1277,28 @@ emit_instruction_stream :: proc( emit_recovery_value(emitter, instruction_index, instruction, "invalid addition operand") continue } - type_name := llvm_type(instruction.type, &emitter.module.types) - if types.is_float(instruction.type, emitter.module.target) { - fmt.sbprintf(&emitter.builder, " %%v%d = fadd %s ", instruction_index, type_name) - write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) - strings.write_string(&emitter.builder, ", ") - write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) - strings.write_string(&emitter.builder, "\n") + emit_checked_arithmetic(emitter, instructions, instruction_index, instruction, "add", "fadd", "integer addition overflow") + case .Sub_Checked: + if !valid_value(instructions, instruction.a, instruction.type, &emitter.module.types) || + !valid_value(instructions, instruction.b, instruction.type, &emitter.module.types) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid subtraction operand") continue } - intrinsic := "uadd" if types.is_unsigned(instruction.type, emitter.module.target) else "sadd" - fmt.sbprintf(&emitter.builder, " %%pair%d = call ", instruction_index) - strings.write_string(&emitter.builder, "{ ") - fmt.sbprintf(&emitter.builder, "%s, i1 } @llvm.%s.with.overflow.%s(%s ", type_name, intrinsic, type_name, type_name) - write_operand(&emitter.builder, instructions, instruction.a, instruction.type, &emitter.module.types) - fmt.sbprintf(&emitter.builder, ", %s ", type_name) - write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) - fmt.sbprintf(&emitter.builder, ")\n") - fmt.sbprintf(&emitter.builder, " %%v%d = extractvalue ", instruction_index) - strings.write_string(&emitter.builder, "{ ") - fmt.sbprintf(&emitter.builder, "%s, i1 } %%pair%d, 0\n", type_name, instruction_index) - fmt.sbprintf(&emitter.builder, " %%overflow%d = extractvalue ", instruction_index) - strings.write_string(&emitter.builder, "{ ") - fmt.sbprintf(&emitter.builder, "%s, i1 } %%pair%d, 1\n", type_name, instruction_index) - fmt.sbprintf( - &emitter.builder, - " br i1 %%overflow%d, label %%overflow_trap%d, label %%overflow_continue%d\n", - instruction_index, - instruction_index, - instruction_index, - ) - fmt.sbprintf(&emitter.builder, "overflow_trap%d:\n", instruction_index) - message := diagnostic_message(emitter, source.INVALID_DIAGNOSTIC, instruction.span, "integer addition overflow") - emit_trap_call(emitter, message) - fmt.sbprintf(&emitter.builder, " unreachable\noverflow_continue%d:\n", instruction_index) + emit_checked_arithmetic(emitter, instructions, instruction_index, instruction, "sub", "fsub", "integer subtraction overflow") + case .Mul_Checked: + if !valid_value(instructions, instruction.a, instruction.type, &emitter.module.types) || + !valid_value(instructions, instruction.b, instruction.type, &emitter.module.types) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid multiplication operand") + continue + } + emit_checked_arithmetic(emitter, instructions, instruction_index, instruction, "mul", "fmul", "integer multiplication overflow") + case .Div_Checked: + if !valid_value(instructions, instruction.a, instruction.type, &emitter.module.types) || + !valid_value(instructions, instruction.b, instruction.type, &emitter.module.types) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid division operand") + continue + } + emit_checked_division(emitter, instructions, instruction_index, instruction) case .Pointer_Add: result_item, result_ok := types.node(&emitter.module.types, instruction.type) base_type := instructions[instruction.a].type if valid_instruction(instructions, instruction.a) else types.INVALID @@ -1835,19 +1934,16 @@ emit_messages :: proc(emitter: ^Emitter) { emit_declarations :: proc(emitter: ^Emitter) { strings.write_string(&emitter.builder, "declare i64 @write(i32, ptr, i64)\ndeclare void @llvm.trap()\ndeclare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1 immarg)\n") widths := [?]int{8, 16, 32, 64} + overflow_intrinsics := [?]string{"sadd", "uadd", "ssub", "usub", "smul", "umul"} for bits in widths { - strings.write_string(&emitter.builder, "declare { i") - fmt.sbprintf(&emitter.builder, "%d", bits) - strings.write_string(&emitter.builder, ", i1 } @llvm.sadd.with.overflow.i") - fmt.sbprintf(&emitter.builder, "%d(i%d, i%d)\n", bits, bits, bits) - strings.write_string(&emitter.builder, "declare { i") - fmt.sbprintf(&emitter.builder, "%d", bits) - strings.write_string(&emitter.builder, ", i1 } @llvm.uadd.with.overflow.i") - fmt.sbprintf(&emitter.builder, "%d(i%d, i%d)\n", bits, bits, bits) - strings.write_string(&emitter.builder, "declare { i") - fmt.sbprintf(&emitter.builder, "%d", bits) - strings.write_string(&emitter.builder, ", i1 } @llvm.ssub.with.overflow.i") - fmt.sbprintf(&emitter.builder, "%d(i%d, i%d)\n", bits, bits, bits) + for name in overflow_intrinsics { + strings.write_string(&emitter.builder, "declare { i") + fmt.sbprintf(&emitter.builder, "%d", bits) + strings.write_string(&emitter.builder, ", i1 } @llvm.") + strings.write_string(&emitter.builder, name) + strings.write_string(&emitter.builder, ".with.overflow.i") + fmt.sbprintf(&emitter.builder, "%d(i%d, i%d)\n", bits, bits, bits) + } } strings.write_string( &emitter.builder, diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index d000bd5..110c922 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -124,9 +124,11 @@ lower_location :: proc(state: ^State, expr_id: hir.Expr_Id, for_write := false) return lower_nested_expr(state, expr.left) case .Index: container_type := state.hir_module.exprs[expr.left].type - container := lower_nested_expr(state, expr.left) + container := ir.INVALID_INSTRUCTION if types.is_array(container_type, &state.hir_module.types) { container = lower_location(state, expr.left, for_write) + } else { + container = lower_nested_expr(state, expr.left) } index := lower_nested_expr(state, expr.right) return append_instruction(state, ir.Instruction{ @@ -137,9 +139,11 @@ lower_location :: proc(state: ^State, expr_id: hir.Expr_Id, for_write := false) }) case .Field: base_type := state.hir_module.exprs[expr.left].type - base := lower_nested_expr(state, expr.left) + base := ir.INVALID_INSTRUCTION if !types.is_pointer(base_type, &state.hir_module.types) { base = lower_location(state, expr.left, for_write) + } else { + base = lower_nested_expr(state, expr.left) } return append_instruction(state, ir.Instruction{ op=.Field_Address, span=expr.span, type=expr.type, integer=expr.integer, @@ -468,7 +472,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { case .Negate: stack[frame_index].stage = 5 append(&stack, Lower_Expr_Frame{expr=expr.left}) - case .Add, .Pointer_Add: + case .Add, .Sub, .Mul, .Div, .Pointer_Add: stack[frame_index].stage = 2 append(&stack, Lower_Expr_Frame{expr=expr.left}) case .Call: @@ -537,8 +541,15 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { continue } if frame.stage == 3 { + op := ir.Opcode.Add_Checked + #partial switch expr.kind { + case .Sub: op = .Sub_Checked + case .Mul: op = .Mul_Checked + case .Div: op = .Div_Checked + case .Pointer_Add: op = .Pointer_Add + } last = append_instruction(state, ir.Instruction{ - op=.Pointer_Add if expr.kind == .Pointer_Add else .Add_Checked, + op=op, span=expr.span, type=expr.type, target=ir.INVALID_REF, a=frame.left, b=last, diagnostic=source.INVALID_DIAGNOSTIC, }) @@ -605,6 +616,43 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) { diagnostic=source.INVALID_DIAGNOSTIC, }) case .Assignment: + if statement.assignment_op != .Set && statement.target != hir.INVALID_EXPR { + address := lower_location(state, statement.target, true) + target_type := types.INVALID + if int(statement.target) < len(hir_module.exprs) { + target_type = hir_module.exprs[statement.target].type + } + if address == ir.INVALID_INSTRUCTION || !types.is_valid(target_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 + } + current := append_instruction(state, ir.Instruction{ + op=.Load, span=statement.span, type=target_type, + target=ir.INVALID_REF, a=address, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + rhs := lower_expr(state, statement.expr) + op := ir.Opcode.Add_Checked + #partial switch statement.assignment_op { + case .Sub: op = .Sub_Checked + case .Mul: op = .Mul_Checked + case .Div: op = .Div_Checked + case .Pointer_Add: op = .Pointer_Add + } + value := append_instruction(state, ir.Instruction{ + op=op, span=statement.span, type=target_type, + target=ir.INVALID_REF, a=current, b=rhs, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + append_instruction(state, ir.Instruction{ + op=.Store, span=statement.span, type=target_type, + target=ir.INVALID_REF, a=address, b=value, diagnostic=source.INVALID_DIAGNOSTIC, + }) + continue + } value := lower_expr(state, statement.expr) slot := ir.INVALID_INSTRUCTION value_type := types.INVALID diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index a40b24d..92200c1 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -663,8 +663,10 @@ infix_binding_power :: proc(kind: token.Kind) -> (left, right: int, ok: bool) { return 6, 7, true case .Equal_Equal, .Bang_Equal, .Less, .Less_Equal, .Greater, .Greater_Equal: return 8, 9, true - case .Plus: + case .Plus, .Minus: return 10, 11, true + case .Star, .Slash: + return 12, 13, true } return 0, 0, false } @@ -681,10 +683,24 @@ infix_expr_kind :: proc(kind: token.Kind) -> ast.Expr_Kind { case .Less_Equal: return .Le case .Greater: return .Gt case .Greater_Equal: return .Ge + case .Plus: return .Add + case .Minus: return .Sub + case .Star: return .Mul + case .Slash: return .Div case: return .Add } } +compound_assignment_op :: proc(kind: token.Kind) -> (ast.Assignment_Op, bool) { + #partial switch kind { + case .Plus_Equal: return .Add, true + case .Minus_Equal: return .Sub, true + case .Star_Equal: return .Mul, true + case .Slash_Equal: return .Div, true + } + return .Set, false +} + is_simple_range_bound :: proc(expr: ast.Expr) -> bool { if expr.parenthesized { return true @@ -1029,6 +1045,22 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id { }) return id } + if assignment_op, is_compound := compound_assignment_op(current(parser).kind); is_compound { + advance(parser) + skip_newlines(parser) + value := parse_expression(parser) + span := span_from(parser.module.exprs[expr].span, parser.module.exprs[value].span) + id := ast.stmt_id(len(parser.module.statements)) + append(&parser.module.statements, ast.Stmt{ + kind=.Assignment, + span=span, + assignment_op=assignment_op, + target=expr, + expr=value, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return id + } id := ast.stmt_id(len(parser.module.statements)) append(&parser.module.statements, ast.Stmt{ kind=.Expression, diff --git a/compiler/token/token.odin b/compiler/token/token.odin index 4060360..dce2368 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -25,6 +25,11 @@ Kind :: enum u8 { Greater_Equal, Plus, Minus, + Slash, + Plus_Equal, + Minus_Equal, + Star_Equal, + Slash_Equal, Dot, Range, Range_Inclusive, diff --git a/compiler_tests.odin b/compiler_tests.odin index 79bc58e..f8b20dd 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -3047,6 +3047,35 @@ main :: func() void {} testing.expect_value(t, hir_module.globals[2].static_value, i64(-3)) } +@(test) +constant_division_by_zero_has_a_precise_diagnostic :: proc(t: ^testing.T) { + text := `value :: 5 / 0 +main :: func() void {} +` + 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) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + found_division_by_zero := false + found_overflow := false + for diagnostic in diagnostics.items { + found_division_by_zero = found_division_by_zero || + strings.contains(diagnostic.message, "division by zero in constant expression") + found_overflow = found_overflow || + strings.contains(diagnostic.message, "integer constant expression exceeds signed i64 range") + } + testing.expect(t, found_division_by_zero) + testing.expect(t, !found_overflow) +} + @(test) out_of_range_negative_constants_are_diagnosed :: proc(t: ^testing.T) { text := `positive :: 9223372036854775808 @@ -4728,3 +4757,477 @@ main :: func() i32 { testing.expect_value(t, index_address_count, 0) testing.expect(t, !strings.contains(llvm_text, "index_ok")) } + +@(test) +lexer_emits_compound_assignment_and_slash_tokens :: proc(t: ^testing.T) { + source_file := source.Source{path="test.bro", 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) + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, stream.items[0].kind, token.Kind.Plus_Equal) + testing.expect_value(t, stream.items[1].kind, token.Kind.Minus_Equal) + testing.expect_value(t, stream.items[2].kind, token.Kind.Star_Equal) + testing.expect_value(t, stream.items[3].kind, token.Kind.Slash_Equal) + testing.expect_value(t, stream.items[4].kind, token.Kind.Slash) + testing.expect_value(t, stream.items[5].kind, token.Kind.Star) +} + +@(test) +binary_operators_respect_multiplicative_precedence :: proc(t: ^testing.T) { + source_file := source.Source{path="test.bro", text="value :: 1 + 2 * 3\nmain :: func() void {}\n"} + 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) + + root := module.exprs[module.globals[0].expr] + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, root.kind, ast.Expr_Kind.Add) + testing.expect_value(t, module.exprs[root.left].integer, u64(1)) + testing.expect_value(t, module.exprs[root.right].kind, ast.Expr_Kind.Mul) +} + +@(test) +division_parses_left_associatively :: proc(t: ^testing.T) { + source_file := source.Source{path="test.bro", text="value :: 8 / 4 / 2\nmain :: func() void {}\n"} + 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) + + root := module.exprs[module.globals[0].expr] + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, root.kind, ast.Expr_Kind.Div) + testing.expect_value(t, module.exprs[root.left].kind, ast.Expr_Kind.Div) + testing.expect_value(t, module.exprs[root.right].integer, u64(2)) +} + +@(test) +compound_assignment_preserves_operation_and_rhs :: proc(t: ^testing.T) { + text := `main :: func() void { + x i32 = 0 + x += 5 +} +` + 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) + body := module.functions[0].body + statement := module.statements[body[1]] + testing.expect_value(t, statement.kind, ast.Stmt_Kind.Assignment) + testing.expect_value(t, statement.assignment_op, ast.Assignment_Op.Add) + testing.expect(t, statement.target != ast.INVALID_EXPR) + testing.expect_value(t, module.exprs[statement.target].kind, ast.Expr_Kind.Name) + testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.Integer) + testing.expect_value(t, module.exprs[statement.expr].integer, u64(5)) +} + +@(test) +compound_assignment_evaluates_lvalue_once :: proc(t: ^testing.T) { + // A compound assignment to an indexed lvalue must compute the element address + // once and reuse it for the load and the store, rather than re-lowering the + // lvalue (which would re-evaluate any side-effecting index subexpression). + text := `bump :: func() usize { + return 1 +} +main :: func() i32 { + values [3]mut i32 = [10, 20, 30] + values[bump()] += 5 + return 0 +} +` + 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) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + + testing.expect_value(t, len(diagnostics.items), 0) + call_count := 0 + index_address_count := 0 + for function in ir_module.functions { + if !function.is_main { + continue + } + for instruction in function.instructions { + #partial switch instruction.op { + case .Call: call_count += 1 + case .Index_Address: index_address_count += 1 + case: + } + } + } + // `bump()` is the lvalue's index. The fix shares one address between the load + // and the store, so the side-effecting index runs exactly once and a single + // Index_Address is emitted; the buggy double-lowering produced two of each. + testing.expect_value(t, call_count, 1) + testing.expect_value(t, index_address_count, 1) +} + +@(test) +compound_assignment_evaluates_nested_locations_once :: proc(t: ^testing.T) { + text := `Box :: struct { + value i32 +} +row :: func() usize { + return 0 +} +column :: func() usize { + return 1 +} +pointer_for :: func(value @mut i32) @mut i32 { + return value +} +main :: func() i32 { + matrix [2]mut [2]mut i32 = [[1, 2], [3, 4]] + (matrix[row()])[column()] += 1 + boxes [2]mut Box = [Box { value = 5 }, Box { value = 6 }] + boxes[row()].value += 1 + value i32 = 7 + pointer_for(&value)^ += 1 + return 0 +} +` + 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) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + + testing.expect_value(t, len(diagnostics.items), 0) + call_count := 0 + call_names: [4]string + index_address_count := 0 + field_address_count := 0 + for function in ir_module.functions { + if !function.is_main { + continue + } + for instruction in function.instructions { + #partial switch instruction.op { + case .Call: + function_id := ir.as_function(instruction.target) + if call_count < len(call_names) && + function_id != ir.INVALID_FUNCTION && + int(function_id) < len(hir_module.functions) { + call_names[call_count] = symbol.resolve( + &symbols, + hir_module.functions[function_id].name, + ) + } + call_count += 1 + case .Index_Address: index_address_count += 1 + case .Field_Address: field_address_count += 1 + case: + } + } + } + // row(), column(), the second row(), and pointer_for() each run once. The + // nested matrix target needs two index addresses; the indexed field needs + // one index address and one field address. + testing.expect_value(t, call_count, 4) + testing.expect_value(t, call_names, [4]string{"row", "column", "row", "pointer_for"}) + testing.expect_value(t, index_address_count, 3) + testing.expect_value(t, field_address_count, 1) +} + +@(test) +compound_assignment_supports_pointer_add_only :: proc(t: ^testing.T) { + valid_text := `main :: func() i32 { + values [3]mut i32 = [10, 20, 30] + pointer *mut i32 = (&values).ptr + pointer += 1 + offset usize = 1 + pointer += offset + return pointer^ +} +` + source_file := source.Source{path="test.bro", text=valid_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) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + + pointer_add_count := 0 + for function in ir_module.functions { + if !function.is_main { + continue + } + for instruction in function.instructions { + pointer_add_count += 1 if instruction.op == .Pointer_Add else 0 + } + } + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, pointer_add_count, 2) + + invalid_text := `main :: func() void { + values [1]mut i32 = [10] + pointer *mut i32 = (&values).ptr + pointer -= 1 +} +` + invalid_source := source.Source{path="invalid.bro", text=invalid_text} + invalid_diagnostics := source.init_diagnostics(&invalid_source) + defer source.destroy_diagnostics(&invalid_diagnostics) + invalid_symbols := symbol.init_table() + defer symbol.destroy_table(&invalid_symbols) + invalid_stream := lexer.lex(&invalid_source, &invalid_diagnostics, &invalid_symbols) + defer delete(invalid_stream.items) + invalid_ast := parser.parse(&invalid_stream, &invalid_source, &invalid_diagnostics) + defer ast.destroy_module(&invalid_ast) + invalid_hir := checker.check(&invalid_ast, &invalid_diagnostics, &invalid_symbols) + defer hir.destroy_module(&invalid_hir) + + found := false + for diagnostic in invalid_diagnostics.items { + found = found || strings.contains( + diagnostic.message, + "many-item pointers only support '+=' compound assignment", + ) + } + testing.expect(t, found) +} + +@(test) +compound_assignment_preserves_checked_numeric_operations :: proc(t: ^testing.T) { + text := `main :: func() i32 { + signed i32 = 24 + signed += 6 + signed -= 2 + signed *= 3 + signed /= 4 + unsigned u32 = 24 + unsigned += 6 + unsigned -= 2 + unsigned *= 3 + unsigned /= 4 + float f64 = 24.0 + float += 6.0 + float -= 2.0 + float *= 3.0 + float /= 4.0 + return signed +} +` + 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) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + + operation_counts: [hir.Assignment_Op]int + for statement_id in hir_module.functions[0].body { + statement := hir_module.statements[statement_id] + if statement.kind == .Assignment { + operation_counts[statement.assignment_op] += 1 + } + } + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, operation_counts[.Add], 3) + testing.expect_value(t, operation_counts[.Sub], 3) + testing.expect_value(t, operation_counts[.Mul], 3) + testing.expect_value(t, operation_counts[.Div], 3) + + add_count := 0 + sub_count := 0 + mul_count := 0 + div_count := 0 + for instruction in ir_module.functions[0].instructions { + #partial switch instruction.op { + case .Add_Checked: add_count += 1 + case .Sub_Checked: sub_count += 1 + case .Mul_Checked: mul_count += 1 + case .Div_Checked: div_count += 1 + case: + } + } + testing.expect_value(t, add_count, 3) + testing.expect_value(t, sub_count, 3) + testing.expect_value(t, mul_count, 3) + testing.expect_value(t, div_count, 3) +} + +@(test) +compound_assignment_rejects_narrowing_and_mixed_numeric_families :: proc(t: ^testing.T) { + text := `main :: func() void { + narrow i8 = 1 + wide i32 = 2 + narrow += wide + signed i32 = 3 + unsigned u32 = 4 + signed += unsigned +} +` + 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) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + found_narrowing := false + found_mixed_family := false + for diagnostic in diagnostics.items { + found_narrowing = found_narrowing || + strings.contains(diagnostic.message, "cannot implicitly convert i32 to i8") + found_mixed_family = found_mixed_family || + strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands") + } + testing.expect(t, found_narrowing) + testing.expect(t, found_mixed_family) +} + +@(test) +compound_assignment_compiles_and_runs :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-compound" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/programs/compound_assignment", output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 23) +} + +@(test) +binary_arithmetic_rejects_non_numeric_operands :: proc(t: ^testing.T) { + text := `main :: func() i32 { + a i32 = 1 + b u32 = 2 + _ = a / b + return 0 +} +` + 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) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + found := false + for diagnostic in diagnostics.items { + found = found || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands") + } + testing.expect(t, found) +} + +@(test) +compound_assignment_requires_writable_target :: proc(t: ^testing.T) { + text := `main :: func() i32 { + x :: 5 + x += 1 + return x +} +` + 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) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + found := false + for diagnostic in diagnostics.items { + found = found || strings.contains(diagnostic.message, "assignment target is not writable") + } + testing.expect(t, found) +} + +@(test) +checked_division_and_subtraction_emit_guarded_llvm :: proc(t: ^testing.T) { + text := `main :: func() i32 { + a i32 = 10 + b i32 = 3 + c i32 = a - b + return c / b +} +` + 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) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) + defer delete(llvm_text) + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, strings.contains(llvm_text, "@llvm.ssub.with.overflow.i32")) + testing.expect(t, strings.contains(llvm_text, "sdiv i32")) + testing.expect(t, strings.contains(llvm_text, "divzero_trap")) + testing.expect(t, strings.contains(llvm_text, "divovf_trap")) +} diff --git a/examples/interop/header/app/main.bro b/examples/interop/header/app/main.bro index 586607f..edd1df6 100644 --- a/examples/interop/header/app/main.bro +++ b/examples/interop/header/app/main.bro @@ -23,8 +23,8 @@ main :: func() void { _ = native.configured_value(9) _ = native.IMPORTED_SHADOW_OBJECT _ = native.IMPORTED_SHADOW_FUNCTION - native.imported_global = native.IMPORTED_MAGIC - native.imported_record_global.value = native.IMPORTED_MAGIC + native.imported_global += native.IMPORTED_MAGIC + native.imported_record_global.value += 2 color native.Imported_Color :: native.IMPORTED_COLOR _ = native.imported_check_state( color, diff --git a/examples/programs/compound_assignment/main.bro b/examples/programs/compound_assignment/main.bro new file mode 100644 index 0000000..6e050e6 --- /dev/null +++ b/examples/programs/compound_assignment/main.bro @@ -0,0 +1,45 @@ +# Milestone 6: compound assignment (`+=`, `-=`, `*=`, `/=`) and the binary +# arithmetic operators `-`, `*`, `/` with multiplicative precedence. + +check_float :: func() i32 { + x f64 = 10.0 + x /= 4.0 # 2.5 + x *= 2.0 # 5.0 + x -= 1.0 # 4.0 + x += 0.5 # 4.5 + if x > 4.0 { + return 1 + } + return 0 +} + +check_unsigned :: func() i32 { + n u32 = 100 + n /= 7 # 14 (truncating integer division) + n -= 4 # 10 + if n == 10 { + return 1 + } + return 0 +} + +main :: func() i32 { + total i32 = 0 + total += 10 # 10 + total -= 3 # 7 + total *= 4 # 28 + total /= 2 # 14 + + # binary operators honour precedence: 14 + (2 * 3) - 4 == 16 + total = total + 2 * 3 - 4 + + # compound assignment as a while-loop update + i i32 = 0 + while i < 5 : i += 1 { + total += 1 # +5 => 21 + } + + total += check_float() # +1 => 22 + total += check_unsigned() # +1 => 23 + return total +}