broaden type inference from context (arithmetic expressions)

This commit is contained in:
2026-06-26 16:10:23 +02:00
parent ff69e1da83
commit cfc1b2cb42
3 changed files with 340 additions and 87 deletions
+15 -4
View File
@@ -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:
+208 -83
View File
@@ -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)
+117
View File
@@ -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)
}