From cfc1b2cb424af8d128ed40aac0109947d3bfc9b0 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Fri, 26 Jun 2026 16:10:23 +0200 Subject: [PATCH] broaden type inference from context (arithmetic expressions) --- TODO.md | 19 ++- compiler/checker/checker.odin | 291 ++++++++++++++++++++++++---------- compiler_tests.odin | 117 ++++++++++++++ 3 files changed, 340 insertions(+), 87 deletions(-) diff --git a/TODO.md b/TODO.md index 10c634f..a4b55e3 100644 --- a/TODO.md +++ b/TODO.md @@ -214,13 +214,14 @@ bare-name typed declarations, call arguments (a concrete parameter type demands its argument, e.g. `take_u16(a)`), and returns — including from inside a function body back onto a referenced global - - demands flow only through bare names; they do not cross arithmetic or other operators, - nor back across a call's result (the result-to-argument direction is milestone 14.5) + - at this milestone, demands flow only through bare names; they do not cross arithmetic + or other operators, nor back across a call's result (arithmetic is milestone 15; + result-to-argument direction is milestone 14.5) - a non-fitting or family-conflicting demand is not applied (first demand wins); the genuine mismatch then surfaces as the usual boundary coercion error at the use (e.g. `C u8 :: BIG` where `BIG :: 100000`) -14.5. backward type-demand propagation through call boundaries (deferred) +14.5. backward type-demand propagation through call boundaries (DEFERRED) - a callee's result/return demand flows back through the function body to constrain the caller's arguments, so `R u32 :: echo(A)` (with `echo :: func(p int) int`) resolves A to u32 instead of erroring at the call's result coercion @@ -229,7 +230,17 @@ through every call site and the specialization fixpoint - only meaningful on top of milestone 14's open constants -15. broaden type inference to infer type of declaration based on arithmetic expressions too +15. broaden type inference to infer type of declaration based on arithmetic expressions too (implemented) + - backward contextual demands now flow through numeric arithmetic (`+`, `-`, `*`, `/`, unary `-`) + for integer and float open constants + - integer literals can adopt integer or float arithmetic context; float literals can adopt `f32`/`f64` + - unannotated declarations initialized by arithmetic expressions adopt the concrete numeric operand type + - e.g. + ``` + a :: 1 + b i32 :: a + 2 # a is constrained to `i32` + c :: b + 3 # c is constrained to `i32` + ``` 16. add slice-by-range - allow the use of a range in slice expressions: diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index fde52ae..62cefeb 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -38,10 +38,10 @@ Infer_Local :: struct { declared: types.Type, statement: ast.Stmt_Id, mutable: bool, - // open_const marks a local whose initializer is a compile-time integer with no - // concrete annotation: like an open-constant global, it is sign-agnostic until a - // backward demand from a use picks its family/width (see merge_local_demand). + // open_const/open_float mark a local whose initializer is an unannotated numeric + // constant: like an open-constant global, it can adopt a backward demand from use. open_const: bool, + open_float: bool, const_value: i128, demanded: bool, } @@ -117,6 +117,7 @@ Checker :: struct { // inference fixpoint. global_demands: []types.Type, global_open_const: []bool, + global_open_float: []bool, global_const_value: []i128, global_demands_dirty: bool, external_global_canonical: []ast.Global_Id, @@ -287,6 +288,40 @@ is_undefined_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool { return checker.ast_module.exprs[expr_id].kind == .Undefined } +is_float_constant_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool { + if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + return false + } + expr := checker.ast_module.exprs[expr_id] + if expr.kind == .Float { + return true + } + return expr.kind == .Negate && is_float_constant_expr(checker, expr.left) +} + +is_numeric_arithmetic_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool { + if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + return false + } + #partial switch checker.ast_module.exprs[expr_id].kind { + case .Add, .Sub, .Mul, .Div, .Negate: + return true + } + return false +} + +is_numeric_constant_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool { + if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + return false + } + return eval_constant(checker, expr_id).kind == .Value || is_float_constant_expr(checker, expr_id) +} + +is_numeric_demand :: proc(value: types.Type, selected := target.DEFAULT) -> bool { + return types.is_concrete_scalar(value) && !types.is_bool(value) || + types.is_float(value, selected) +} + string_literal_type :: proc(checker: ^Checker, string_id: u64) -> types.Type { length: u64 if string_id < u64(len(checker.ast_module.strings)) { @@ -1463,10 +1498,27 @@ infer_expr :: proc( continue } if frame.stage == 2 { - if expr.kind == .Add && types.is_many_pointer(frame.left, &checker.module.types) && types.is_concrete_integer(last) { + right := last + if expr.kind == .Add && types.is_many_pointer(frame.left, &checker.module.types) && types.is_concrete_integer(right) { last = frame.left + } else if is_numeric_constant_expr(checker, expr.right) && + is_numeric_demand(frame.left, checker.target) && + expr_accepts_numeric_demand(checker, expr.right, frame.left, locals, pkg, file) { + last = frame.left + } else if is_numeric_constant_expr(checker, expr.left) && + is_numeric_demand(right, checker.target) && + expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) { + last = right + } else if is_numeric_demand(frame.left, checker.target) && + expr_accepts_numeric_demand(checker, expr.right, frame.left, locals, pkg, file) { + _ = record_demand(checker, expr.right, frame.left, locals, local_types, pkg, file) + last = frame.left + } else if is_numeric_demand(right, checker.target) && + expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) { + _ = record_demand(checker, expr.left, right, locals, local_types, pkg, file) + last = right } else { - last = types.widest(frame.left, last) + last = types.widest(frame.left, right) } _ = pop(&stack) continue @@ -1646,12 +1698,15 @@ infer_statements :: proc( value_type = types.constraint_target(declared_local, value_type, &checker.module.types) } open := false + open_float := false const_val := i128(0) if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) { constant := eval_constant(checker, statement.expr) if constant.kind == .Value && fits_i64(constant.value) { open = true const_val = constant.value + } else if is_float_constant_expr(checker, statement.expr) { + open_float = true } } local := Infer_Local{ @@ -1661,6 +1716,7 @@ infer_statements :: proc( statement=statement_id, mutable=!statement.immutable, open_const=open, + open_float=open_float, const_value=const_val, } append(locals, local) @@ -1691,6 +1747,7 @@ infer_statements :: proc( if statement.expr != ast.INVALID_EXPR { returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) if is_runtime_type(checker, result_hint) { + _ = record_demand(checker, statement.expr, result_hint, locals^[:], local_types, pkg, file) expr := checker.ast_module.exprs[statement.expr] if expr.kind == .Name && !symbol.is_valid(expr.qualifier) { if local_index, ok := find_infer_local_index(locals^[:], expr.name); ok { @@ -1842,33 +1899,30 @@ merge_inferred_type :: proc(store: ^types.Store, current: ^types.Type, inferred: return false } -// root_demand_target returns the global that an initializer pushes a backward type -// demand onto: when the initializer's root expression is a bare name referencing a -// global (e.g. `Z i32 :: Y`). Returns INVALID_GLOBAL for any other shape — demands -// deliberately do not flow through arithmetic, calls, or other operators (that is L3). -root_demand_target :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id) -> ast.Global_Id { - if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { - return ast.INVALID_GLOBAL +open_integer_accepts_demand :: proc(checker: ^Checker, value: i128, demand: types.Type) -> bool { + if types.is_concrete_integer(demand) { + return fits_integer_type(value, demand, checker.target) } - expr := checker.ast_module.exprs[expr_id] - if expr.kind != .Name { - return ast.INVALID_GLOBAL - } - target_pkg, available := expr_package(checker, expr, pkg, file) - if !available { - return ast.INVALID_GLOBAL - } - return find_global(checker, expr.name, target_pkg) + return types.is_float(demand, checker.target) } -// merge_open_const_demand records a concrete integer demand onto an open-constant -// global's slot. The constant is sign-agnostic until used, so it may adopt any -// integer family/width whose range holds its value (first demand wins; later demands -// may only widen within the chosen family). Non-integer or non-fitting demands are -// ignored, leaving the constant to default and the genuine mismatch to surface at the -// use's boundary coercion. -merge_open_const_demand :: proc(checker: ^Checker, slot: ^types.Type, demand: types.Type, value: i128) -> bool { - if !types.is_concrete_integer(demand) || !fits_integer_type(value, demand, checker.target) { +open_float_accepts_demand :: proc(checker: ^Checker, demand: types.Type) -> bool { + return types.is_float(demand, checker.target) +} + +// merge_open_const_demand records a concrete numeric demand onto an open numeric +// global's slot. Integer constants may adopt integer or float demands; float +// constants may adopt float demands. Later demands only widen within the chosen family. +merge_open_const_demand :: proc( + checker: ^Checker, + slot: ^types.Type, + demand: types.Type, + int_open: bool, + float_open: bool, + value: i128, +) -> bool { + if !(int_open && open_integer_accepts_demand(checker, value, demand) || + float_open && open_float_accepts_demand(checker, demand)) { return false } if !is_runtime_type(checker, slot^) { @@ -1895,8 +1949,15 @@ merge_global_demand :: proc(checker: ^Checker, global: ast.Global_Id, demand: ty return false } changed: bool - if checker.global_open_const[index] { - changed = merge_open_const_demand(checker, &checker.global_demands[index], demand, checker.global_const_value[index]) + if checker.global_open_const[index] || checker.global_open_float[index] { + changed = merge_open_const_demand( + checker, + &checker.global_demands[index], + demand, + checker.global_open_const[index], + checker.global_open_float[index], + checker.global_const_value[index], + ) } else { changed = merge_inferred_type(&checker.module.types, &checker.global_demands[index], demand) } @@ -1904,21 +1965,18 @@ merge_global_demand :: proc(checker: ^Checker, global: ast.Global_Id, demand: ty return changed } -// merge_local_demand records a concrete integer demand onto an open-constant local. -// Like an open-constant global it adopts any integer family/width whose range holds its -// value (gated by its constraint family if it has one); the first demand replaces the -// literal's signed default, later demands may only widen within the chosen family. +// merge_local_demand records a concrete numeric demand onto an open-constant local. +// The first demand replaces the literal's default type; later demands may only widen +// within the chosen family. merge_local_demand :: proc(checker: ^Checker, local: ^Infer_Local, demand: types.Type, local_types: []types.Type) -> bool { - if !local.open_const || !types.is_concrete_integer(demand) { + if !(local.open_const && open_integer_accepts_demand(checker, local.const_value, demand) || + local.open_float && open_float_accepts_demand(checker, demand)) { return false } if types.is_constraint(local.declared) && !types.constraint_accepts(local.declared, demand, &checker.module.types) { return false } - if !fits_integer_type(local.const_value, demand, checker.target) { - return false - } if !local.demanded { local.type = demand local.demanded = true @@ -1937,11 +1995,60 @@ merge_local_demand :: proc(checker: ^Checker, local: ^Infer_Local, demand: types return false } -// record_demand pushes a concrete type demand onto the slot of a bare-name expression -// (a typed declaration's initializer, a call argument, a return value). When the name -// resolves to an open-constant local or global, that slot adopts the demand; any other -// shape is ignored — demands flow only through bare names, never through arithmetic or -// across a call's result (the latter is milestone 14.5). +expr_accepts_numeric_demand :: proc( + checker: ^Checker, + expr_id: ast.Expr_Id, + demand: types.Type, + locals: []Infer_Local, + pkg: ast.Package_Id, + file: ast.File_Id, +) -> bool { + if !is_numeric_demand(demand, checker.target) || + expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + return false + } + if constant := eval_constant(checker, expr_id); constant.kind == .Value { + return open_integer_accepts_demand(checker, constant.value, demand) + } + if is_float_constant_expr(checker, expr_id) { + return open_float_accepts_demand(checker, demand) + } + expr := checker.ast_module.exprs[expr_id] + #partial switch expr.kind { + case .Name: + if !symbol.is_valid(expr.qualifier) { + if index, ok := find_infer_local_index(locals, expr.name); ok { + local := locals[index] + return local.open_const && open_integer_accepts_demand(checker, local.const_value, demand) || + local.open_float && open_float_accepts_demand(checker, demand) + } + } + target_pkg, available := expr_package(checker, expr, pkg, file) + if !available { + return false + } + global := find_global(checker, expr.name, target_pkg) + index := int(global) + if global == ast.INVALID_GLOBAL || index < 0 || index >= len(checker.global_open_const) { + return false + } + return checker.global_open_const[index] && + open_integer_accepts_demand(checker, checker.global_const_value[index], demand) || + checker.global_open_float[index] && open_float_accepts_demand(checker, demand) + case .Negate: + if !types.is_signed(demand, checker.target) && !types.is_float(demand, checker.target) { + return false + } + return expr_accepts_numeric_demand(checker, expr.left, demand, locals, pkg, file) + case .Add, .Sub, .Mul, .Div: + return expr_accepts_numeric_demand(checker, expr.left, demand, locals, pkg, file) && + expr_accepts_numeric_demand(checker, expr.right, demand, locals, pkg, file) + } + return false +} + +// record_demand pushes a concrete type demand onto open numeric slots reachable +// through bare names and numeric arithmetic. Calls remain a boundary (milestone 14.5). record_demand :: proc( checker: ^Checker, expr_id: ast.Expr_Id, @@ -1950,37 +2057,46 @@ record_demand :: proc( local_types: []types.Type, pkg: ast.Package_Id, file: ast.File_Id, -) { +) -> bool { if !is_runtime_type(checker, demand) || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { - return + return false } expr := checker.ast_module.exprs[expr_id] - if expr.kind != .Name { - return - } - if !symbol.is_valid(expr.qualifier) { - if index, ok := find_infer_local_index(locals, expr.name); ok { - _ = merge_local_demand(checker, &locals[index], demand, local_types) - return + #partial switch expr.kind { + case .Name: + if !symbol.is_valid(expr.qualifier) { + if index, ok := find_infer_local_index(locals, expr.name); ok { + return merge_local_demand(checker, &locals[index], demand, local_types) + } + } + target_pkg, available := expr_package(checker, expr, pkg, file) + if !available { + return false + } + global := find_global(checker, expr.name, target_pkg) + if global != ast.INVALID_GLOBAL { + return merge_global_demand(checker, global, demand) + } + case .Negate: + if types.is_signed(demand, checker.target) || types.is_float(demand, checker.target) { + return record_demand(checker, expr.left, demand, locals, local_types, pkg, file) + } + case .Add, .Sub, .Mul, .Div: + if is_numeric_demand(demand, checker.target) { + left := record_demand(checker, expr.left, demand, locals, local_types, pkg, file) + right := record_demand(checker, expr.right, demand, locals, local_types, pkg, file) + return left || right } } - target_pkg, available := expr_package(checker, expr, pkg, file) - if !available { - return - } - global := find_global(checker, expr.name, target_pkg) - if global != ast.INVALID_GLOBAL { - _ = merge_global_demand(checker, global, demand) - } + return false } infer_all :: proc(checker: ^Checker) { // An "open constant" global has no concrete declared type and a compile-time - // integer initializer. Its slot stays sign-agnostic so a backward demand from any - // reachable use can pick its family/width; absent a demand it defaults to the - // smallest signed type (legacy behaviour). Demands accumulate in global_demands so - // the default never blocks a later cross-family (e.g. unsigned) demand. + // numeric initializer. Its slot can adopt a backward demand from any reachable use. + // Demands accumulate in global_demands so the default never blocks a later + // cross-family demand (e.g. integer literal -> unsigned or float). for global, index in checker.ast_module.globals { declared := type_from_syntax(global.type) if is_runtime_type(checker, declared) { @@ -1994,6 +2110,8 @@ infer_all :: proc(checker: ^Checker) { if constant.kind == .Value && fits_i64(constant.value) { checker.global_open_const[index] = true checker.global_const_value[index] = constant.value + } else if is_float_constant_expr(checker, global.expr) { + checker.global_open_float[index] = true } } @@ -2007,9 +2125,8 @@ infer_all :: proc(checker: ^Checker) { checker.global_demands_dirty = false spec_count := len(checker.specs) - // Backward demands: a global whose initializer's root is a bare name referencing - // another global pushes its own (declared or already-resolved) type onto that - // referent. Open constants adopt any fitting family; other referents widen only. + // Backward demands: a global pushes its own (declared or already-resolved) type + // onto open numeric slots reachable through names and numeric arithmetic. for global, index in checker.ast_module.globals { if global.external { continue @@ -2018,10 +2135,7 @@ infer_all :: proc(checker: ^Checker) { if !is_runtime_type(checker, demand) { continue } - target := root_demand_target(checker, global.expr, global.pkg, global.file) - if target != ast.INVALID_GLOBAL { - merge_global_demand(checker, target, demand) - } + _ = record_demand(checker, global.expr, demand, nil, nil, global.pkg, global.file) } // Forward / resolution. infer_expr runs for every non-external global (even @@ -2048,6 +2162,11 @@ infer_all :: proc(checker: ^Checker) { checker.global_types[index] = resolved changed = true } + } else if checker.global_open_float[index] { + if !types.equal(checker.global_types[index], types.F64) { + checker.global_types[index] = types.F64 + changed = true + } } else { changed = merge_inferred_type(&checker.module.types, &checker.global_types[index], inferred) || changed } @@ -3723,18 +3842,20 @@ build_block :: proc( case .Declaration: declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr) // Adopt the type inference resolved for this local when the declaration has no - // concrete annotation (a constraint, `undefined`, or an un-annotated open - // integer constant): the slot may have absorbed a backward demand (e.g. `a :: 10` - // built as u16 after `take_u16(a)`). Gated to compile-time integer constants so - // strings/arrays/pointers keep their own initializer type. + // concrete annotation and inference carried useful numeric context: constraints, + // `undefined`, open numeric constants, or arithmetic expressions. open_const_decl := !is_runtime_type(checker, declared) && !is_undefined_expr(checker, statement.expr) if open_const_decl { constant := eval_constant(checker, statement.expr) - open_const_decl = constant.kind == .Value && fits_i64(constant.value) + open_const_decl = constant.kind == .Value && fits_i64(constant.value) || + is_float_constant_expr(checker, statement.expr) } + numeric_arithmetic_decl := !is_runtime_type(checker, declared) && + is_numeric_arithmetic_expr(checker, statement.expr) if statement_id != ast.INVALID_STMT && int(statement_id) < len(ctx.local_types) && is_runtime_type(checker, ctx.local_types[statement_id]) && - (types.is_constraint(declared) || is_undefined_expr(checker, statement.expr) || open_const_decl) { + (types.is_constraint(declared) || is_undefined_expr(checker, statement.expr) || + open_const_decl || numeric_arithmetic_decl) { declared = ctx.local_types[statement_id] } // A still-unresolved constraint means the initializer's numeric @@ -4600,11 +4721,13 @@ build_globals :: proc(checker: ^Checker) { } else if constant := eval_constant(checker, global.expr); constant.kind == .Value && fits_i64(constant.value) && is_runtime_type(checker, checker.global_types[global_index]) { - // Open constant: build the initializer against the type inference resolved - // for this slot, so it adopts its demanded/defaulted type (e.g. `A :: 10` - // built as u16 when a use demanded u16). Gated to compile-time values fitting - // i64 — exactly the infer-side open-constant condition — so out-of-range - // constants keep their original "exceeds signed i64 range" diagnostic. + // Open integer constant: build against its demanded/defaulted type. Gated + // to the infer-side open-constant condition so out-of-range constants keep + // their original "exceeds signed i64 range" diagnostic. + expected = checker.global_types[global_index] + } else if (is_float_constant_expr(checker, global.expr) || + is_numeric_arithmetic_expr(checker, global.expr)) && + is_runtime_type(checker, checker.global_types[global_index]) { expected = checker.global_types[global_index] } expr := build_expr(checker, global.expr, nil, &dependencies, &calls, expected, global.pkg, global.file) @@ -4901,6 +5024,7 @@ check :: proc( checker.global_types = make([]types.Type, len(ast_module.globals), allocator) checker.global_demands = make([]types.Type, len(ast_module.globals), allocator) checker.global_open_const = make([]bool, len(ast_module.globals), allocator) + checker.global_open_float = make([]bool, len(ast_module.globals), allocator) checker.global_const_value = make([]i128, len(ast_module.globals), allocator) checker.external_global_canonical = make([]ast.Global_Id, len(ast_module.globals), allocator) checker.external_global_diagnostics = make([]source.Diagnostic_Id, len(ast_module.globals), allocator) @@ -4926,6 +5050,7 @@ check :: proc( delete(checker.global_types, allocator) delete(checker.global_demands, allocator) delete(checker.global_open_const, allocator) + delete(checker.global_open_float, allocator) delete(checker.global_const_value, allocator) delete(checker.external_global_canonical, allocator) delete(checker.external_global_diagnostics, allocator) diff --git a/compiler_tests.odin b/compiler_tests.odin index 10c8e5e..57e7975 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -6629,3 +6629,120 @@ contextual_inference_rejects_local_constant_that_does_not_fit :: proc(t: ^testin } testing.expect(t, found) } + +@(test) +contextual_inference_flows_through_numeric_arithmetic :: proc(t: ^testing.T) { + text := `take_u16 :: func(v u16) void {} +take_f32 :: func(v f32) void {} +G :: 10 +H u16 :: G + 2 +GF :: 1.5 +HF f32 :: GF + 2.5 +CG :: 5 +CFG :: 1.0 +get :: func() f32 { + seed f32 :: 2.0 + c :: seed + 3.0 + d :: 4.0 + seed + return c + d +} +main :: func() void { + a :: 10 + b u16 :: a + 2 + x :: 1.5 + y f32 :: x + 2.5 + z f32 :: 2.5 + x + call_i :: 7 + call_f :: 1.25 + take_u16(call_i + 3) + take_f32(call_f + 3.0) + take_u16(CG + 1) + take_f32(CFG + 1.0) + _ = b + _ = y + _ = z + _ = H + _ = HF + _ = 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) + + testing.expect_value(t, len(diagnostics.items), 0) + + global_ok := 0 + for global in hir_module.globals { + name := symbol.resolve(&symbols, global.name) + switch name { + case "G", "H", "CG": + global_ok += 1 if types.equal(global.type, types.U16) else 0 + case "GF", "HF", "CFG": + global_ok += 1 if types.equal(global.type, types.F32) else 0 + } + } + testing.expect_value(t, global_ok, 6) + + main_ok := 0 + get_ok := false + for function in hir_module.functions { + name := symbol.resolve(&symbols, function.name) + if name == "get" { + get_ok = types.equal(function.result, types.F32) + for local in function.locals { + local_name := symbol.resolve(&symbols, local.name) + if local_name == "c" || local_name == "d" { + main_ok += 1 if types.equal(local.type, types.F32) else 0 + } + } + } else if name == "main" { + for local in function.locals { + local_name := symbol.resolve(&symbols, local.name) + switch local_name { + case "a", "call_i": + main_ok += 1 if types.equal(local.type, types.U16) else 0 + case "x", "call_f": + main_ok += 1 if types.equal(local.type, types.F32) else 0 + } + } + } + } + testing.expect(t, get_ok) + testing.expect_value(t, main_ok, 6) +} + +@(test) +contextual_inference_rejects_non_fitting_arithmetic_demand :: proc(t: ^testing.T) { + text := `BIG :: 100000 +C u8 :: BIG + 1 +main :: func() void { + _ = C +} +` + 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, "cannot implicitly convert i32 to u8") + } + testing.expect(t, found) +}