broaden type inference from context (first pass)

This commit is contained in:
2026-06-25 23:57:12 +02:00
parent 70a6d69d29
commit ff69e1da83
3 changed files with 855 additions and 38 deletions
+202
View File
@@ -6427,3 +6427,205 @@ main :: func() void {
}
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
contextual_inference_resolves_signed_const_chain :: proc(t: ^testing.T) {
text := `X :: 1000
Y int :: X
Z i32 :: Y
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)
// The concrete i32 on Z flows backward through Y to the open constant X, so all
// three resolve to i32 instead of X/Y staying at the literal's smallest signed type.
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, types.equal(hir_module.globals[0].type, types.I32))
testing.expect(t, types.equal(hir_module.globals[1].type, types.I32))
testing.expect(t, types.equal(hir_module.globals[2].type, types.I32))
}
@(test)
contextual_inference_open_constants_adopt_unsigned_demand :: proc(t: ^testing.T) {
text := `A :: 10
B u16 :: A
P :: 10
R u32 :: P
N :: 42
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)
// An open constant is sign-agnostic until used: it adopts the unsigned family a use
// demands (the literal's signed default would block this). Unconstrained N defaults.
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, types.equal(hir_module.globals[0].type, types.U16)) // A
testing.expect(t, types.equal(hir_module.globals[1].type, types.U16)) // B
testing.expect(t, types.equal(hir_module.globals[2].type, types.U32)) // P
testing.expect(t, types.equal(hir_module.globals[3].type, types.U32)) // R
testing.expect(t, types.equal(hir_module.globals[4].type, types.I8)) // N
}
@(test)
contextual_inference_rejects_constant_that_does_not_fit_demand :: proc(t: ^testing.T) {
text := `BIG :: 100000
C u8 :: BIG
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)
// 100000 does not fit u8, so the demand is rejected, BIG defaults to i32, and the
// genuine mismatch surfaces at the use's boundary coercion.
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8")
}
testing.expect(t, found)
}
@(test)
contextual_inference_does_not_cross_call_boundaries :: proc(t: ^testing.T) {
text := `echo :: func(p int) int { return p }
A :: 10
R u32 :: echo(A)
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)
// The u32 demand on R must not flow through echo into A (that is L3, deferred). A
// stays at its default i8, so the call result fails to coerce to u32.
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i8 to u32")
}
testing.expect(t, found)
}
@(test)
contextual_inference_resolves_locals_like_globals :: proc(t: ^testing.T) {
text := `take_u16 :: func(v u16) void {}
get :: func() u16 {
c :: 10
return c
}
main :: func() void {
x :: 1000
y int :: x
z i32 :: y
a :: 10
b u16 :: a
n :: 5
take_u16(n)
_ = get()
}
`
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)
// The same backward propagation works for locals: a constant local adopts the
// unsigned/wider type a later use demands (declaration, call argument, or return),
// so none of these need an explicit annotation. Without it, i8->u16/u32 would error.
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test)
contextual_inference_demand_from_function_body_reaches_global :: proc(t: ^testing.T) {
text := `take_u16 :: func(v u16) void {}
G :: 10
main :: func() void {
take_u16(G)
}
`
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)
// A demand originating inside a function body (passing G to a u16 parameter) flows
// back to the open-constant global G, resolving it to u16.
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, types.equal(hir_module.globals[0].type, types.U16))
}
@(test)
contextual_inference_rejects_local_constant_that_does_not_fit :: proc(t: ^testing.T) {
text := `main :: func() void {
big :: 100000
c u8 :: big
}
`
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)
// 100000 does not fit u8, so big keeps its i32 default and the use errors.
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "cannot implicitly convert i32 to u8")
}
testing.expect(t, found)
}