compound assignment

This commit is contained in:
2026-06-22 21:20:15 +02:00
parent 663f4dc658
commit 6512ccd543
14 changed files with 995 additions and 99 deletions
+503
View File
@@ -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"))
}