undefined assignment

This commit is contained in:
2026-06-25 20:37:20 +02:00
parent e0b6f6049c
commit ca6cc89da0
10 changed files with 489 additions and 47 deletions
+207
View File
@@ -375,6 +375,32 @@ main :: func() void {}
testing.expect(t, strings.contains(diagnostics.items[0].message, "followed by a newline"))
}
@(test)
parser_accepts_undefined_expression :: proc(t: ^testing.T) {
text := `main :: func() void {
value i32 = undefined
}
`
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)
found_keyword := false
for tok in stream.items {
found_keyword = found_keyword || tok.kind == .Keyword_Undefined
}
statement := module.statements[module.functions[0].body[0]]
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_keyword)
testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.Undefined)
}
@(test)
pratt_parser_preserves_left_associative_addition_shape :: proc(t: ^testing.T) {
source_file := source.Source{path="test.bro", text="value :: 1 + 2 + 3\nmain :: func() void {}\n"}
@@ -5013,6 +5039,187 @@ compound_assignment_preserves_operation_and_rhs :: proc(t: ^testing.T) {
testing.expect_value(t, module.exprs[statement.expr].integer, u64(5))
}
@(test)
undefined_inferred_local_lowers_to_fill :: proc(t: ^testing.T) {
text := `choose :: func(flag bool) i32 {
value int = undefined
if flag {
value = 42
} else {
value = -2
}
return value
}
main :: func() i32 {
return choose(true)
}
`
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)
choose_symbol := symbol.intern(&symbols, "choose")
value_symbol := symbol.intern(&symbols, "value")
found_value_i32 := false
fill_count := 0
for function, function_index in hir_module.functions {
if function.name != choose_symbol {
continue
}
for local in function.locals {
found_value_i32 = found_value_i32 || local.name == value_symbol && local.type == types.I32
}
for instruction in ir_module.functions[function_index].instructions {
fill_count += 1 if instruction.op == .Fill else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_value_i32)
testing.expect_value(t, fill_count, 1)
testing.expect(t, strings.contains(llvm_text, "declare void @llvm.memset.p0.i64"))
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memset.p0.i64"))
testing.expect(t, strings.contains(llvm_text, "i8 -86"))
}
@(test)
local_int_inference_widens_from_assignments :: proc(t: ^testing.T) {
text := `wide :: func() int {
value int = 1
value = 1000
return value
}
main :: func() void {
_ = wide()
}
`
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)
wide_symbol := symbol.intern(&symbols, "wide")
value_symbol := symbol.intern(&symbols, "value")
found_value_i16 := false
found_result_i16 := false
for function in hir_module.functions {
if function.name != wide_symbol {
continue
}
found_result_i16 = function.result == types.I16
for local in function.locals {
found_value_i16 = found_value_i16 || local.name == value_symbol && local.type == types.I16
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_result_i16)
testing.expect(t, found_value_i16)
}
@(test)
undefined_accepts_concrete_runtime_annotations :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
y i32
}
main :: func() void {
point Point = undefined
pointer @i32 = undefined
maybe ?i32 = undefined
}
`
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)
main_symbol := symbol.intern(&symbols, "main")
fill_count := 0
for function, function_index in hir_module.functions {
if function.name != main_symbol {
continue
}
for instruction in ir_module.functions[function_index].instructions {
fill_count += 1 if instruction.op == .Fill else 0
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, fill_count, 3)
}
@(test)
undefined_rejects_non_declaration_uses_and_unresolved_inference :: proc(t: ^testing.T) {
text := `global :: undefined
main :: func() void {
immutable :: undefined
typed_immutable int :: undefined
unresolved int = undefined
existing i32 = 1
existing = undefined
mismatch int = undefined
mismatch = 1
mismatch = 1.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)
immutable_count := 0
found_unresolved := false
found_assignment := false
found_incompatible := false
for diagnostic in diagnostics.items {
immutable_count += 1 if strings.contains(diagnostic.message, "'undefined' requires a mutable local declaration") else 0
found_unresolved = found_unresolved || strings.contains(diagnostic.message, "could not infer a concrete type for local 'unresolved'")
found_assignment = found_assignment || strings.contains(diagnostic.message, "'undefined' is only valid as a mutable local declaration initializer")
found_incompatible = found_incompatible || strings.contains(diagnostic.message, "cannot implicitly convert f64 to i8")
}
testing.expect(t, immutable_count >= 2)
testing.expect(t, found_unresolved)
testing.expect(t, found_assignment)
testing.expect(t, found_incompatible)
}
@(test)
compound_assignment_evaluates_lvalue_once :: proc(t: ^testing.T) {
// A compound assignment to an indexed lvalue must compute the element address