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 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 argument, e.g. `take_u16(a)`), and returns — including from inside a function body
back onto a referenced global back onto a referenced global
- demands flow only through bare names; they do not cross arithmetic or other operators, - at this milestone, demands flow only through bare names; they do not cross arithmetic
nor back across a call's result (the result-to-argument direction is milestone 14.5) 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 - 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 genuine mismatch then surfaces as the usual boundary coercion error at the use
(e.g. `C u8 :: BIG` where `BIG :: 100000`) (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 - 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`) 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 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 through every call site and the specialization fixpoint
- only meaningful on top of milestone 14's open constants - 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 16. add slice-by-range
- allow the use of a range in slice expressions: - allow the use of a range in slice expressions:
+208 -83
View File
@@ -38,10 +38,10 @@ Infer_Local :: struct {
declared: types.Type, declared: types.Type,
statement: ast.Stmt_Id, statement: ast.Stmt_Id,
mutable: bool, mutable: bool,
// open_const marks a local whose initializer is a compile-time integer with no // open_const/open_float mark a local whose initializer is an unannotated numeric
// concrete annotation: like an open-constant global, it is sign-agnostic until a // constant: like an open-constant global, it can adopt a backward demand from use.
// backward demand from a use picks its family/width (see merge_local_demand).
open_const: bool, open_const: bool,
open_float: bool,
const_value: i128, const_value: i128,
demanded: bool, demanded: bool,
} }
@@ -117,6 +117,7 @@ Checker :: struct {
// inference fixpoint. // inference fixpoint.
global_demands: []types.Type, global_demands: []types.Type,
global_open_const: []bool, global_open_const: []bool,
global_open_float: []bool,
global_const_value: []i128, global_const_value: []i128,
global_demands_dirty: bool, global_demands_dirty: bool,
external_global_canonical: []ast.Global_Id, 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 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 { string_literal_type :: proc(checker: ^Checker, string_id: u64) -> types.Type {
length: u64 length: u64
if string_id < u64(len(checker.ast_module.strings)) { if string_id < u64(len(checker.ast_module.strings)) {
@@ -1463,10 +1498,27 @@ infer_expr :: proc(
continue continue
} }
if frame.stage == 2 { 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 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 { } else {
last = types.widest(frame.left, last) last = types.widest(frame.left, right)
} }
_ = pop(&stack) _ = pop(&stack)
continue continue
@@ -1646,12 +1698,15 @@ infer_statements :: proc(
value_type = types.constraint_target(declared_local, value_type, &checker.module.types) value_type = types.constraint_target(declared_local, value_type, &checker.module.types)
} }
open := false open := false
open_float := false
const_val := i128(0) const_val := i128(0)
if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) { if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) {
constant := eval_constant(checker, statement.expr) constant := eval_constant(checker, statement.expr)
if constant.kind == .Value && fits_i64(constant.value) { if constant.kind == .Value && fits_i64(constant.value) {
open = true open = true
const_val = constant.value const_val = constant.value
} else if is_float_constant_expr(checker, statement.expr) {
open_float = true
} }
} }
local := Infer_Local{ local := Infer_Local{
@@ -1661,6 +1716,7 @@ infer_statements :: proc(
statement=statement_id, statement=statement_id,
mutable=!statement.immutable, mutable=!statement.immutable,
open_const=open, open_const=open,
open_float=open_float,
const_value=const_val, const_value=const_val,
} }
append(locals, local) append(locals, local)
@@ -1691,6 +1747,7 @@ infer_statements :: proc(
if statement.expr != ast.INVALID_EXPR { if statement.expr != ast.INVALID_EXPR {
returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
if is_runtime_type(checker, result_hint) { 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] expr := checker.ast_module.exprs[statement.expr]
if expr.kind == .Name && !symbol.is_valid(expr.qualifier) { if expr.kind == .Name && !symbol.is_valid(expr.qualifier) {
if local_index, ok := find_infer_local_index(locals^[:], expr.name); ok { 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 return false
} }
// root_demand_target returns the global that an initializer pushes a backward type open_integer_accepts_demand :: proc(checker: ^Checker, value: i128, demand: types.Type) -> bool {
// demand onto: when the initializer's root expression is a bare name referencing a if types.is_concrete_integer(demand) {
// global (e.g. `Z i32 :: Y`). Returns INVALID_GLOBAL for any other shape demands return fits_integer_type(value, demand, checker.target)
// 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
} }
expr := checker.ast_module.exprs[expr_id] return types.is_float(demand, checker.target)
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)
} }
// merge_open_const_demand records a concrete integer demand onto an open-constant open_float_accepts_demand :: proc(checker: ^Checker, demand: types.Type) -> bool {
// global's slot. The constant is sign-agnostic until used, so it may adopt any return types.is_float(demand, checker.target)
// 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 // merge_open_const_demand records a concrete numeric demand onto an open numeric
// use's boundary coercion. // global's slot. Integer constants may adopt integer or float demands; float
merge_open_const_demand :: proc(checker: ^Checker, slot: ^types.Type, demand: types.Type, value: i128) -> bool { // constants may adopt float demands. Later demands only widen within the chosen family.
if !types.is_concrete_integer(demand) || !fits_integer_type(value, demand, checker.target) { 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 return false
} }
if !is_runtime_type(checker, slot^) { if !is_runtime_type(checker, slot^) {
@@ -1895,8 +1949,15 @@ merge_global_demand :: proc(checker: ^Checker, global: ast.Global_Id, demand: ty
return false return false
} }
changed: bool changed: bool
if checker.global_open_const[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_const_value[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 { } else {
changed = merge_inferred_type(&checker.module.types, &checker.global_demands[index], demand) 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 return changed
} }
// merge_local_demand records a concrete integer demand onto an open-constant local. // merge_local_demand records a concrete numeric demand onto an open-constant local.
// Like an open-constant global it adopts any integer family/width whose range holds its // The first demand replaces the literal's default type; later demands may only widen
// value (gated by its constraint family if it has one); the first demand replaces the // within the chosen family.
// literal's signed default, 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 { 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 return false
} }
if types.is_constraint(local.declared) && if types.is_constraint(local.declared) &&
!types.constraint_accepts(local.declared, demand, &checker.module.types) { !types.constraint_accepts(local.declared, demand, &checker.module.types) {
return false return false
} }
if !fits_integer_type(local.const_value, demand, checker.target) {
return false
}
if !local.demanded { if !local.demanded {
local.type = demand local.type = demand
local.demanded = true local.demanded = true
@@ -1937,11 +1995,60 @@ merge_local_demand :: proc(checker: ^Checker, local: ^Infer_Local, demand: types
return false return false
} }
// record_demand pushes a concrete type demand onto the slot of a bare-name expression expr_accepts_numeric_demand :: proc(
// (a typed declaration's initializer, a call argument, a return value). When the name checker: ^Checker,
// resolves to an open-constant local or global, that slot adopts the demand; any other expr_id: ast.Expr_Id,
// shape is ignored demands flow only through bare names, never through arithmetic or demand: types.Type,
// across a call's result (the latter is milestone 14.5). 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( record_demand :: proc(
checker: ^Checker, checker: ^Checker,
expr_id: ast.Expr_Id, expr_id: ast.Expr_Id,
@@ -1950,37 +2057,46 @@ record_demand :: proc(
local_types: []types.Type, local_types: []types.Type,
pkg: ast.Package_Id, pkg: ast.Package_Id,
file: ast.File_Id, file: ast.File_Id,
) { ) -> bool {
if !is_runtime_type(checker, demand) || if !is_runtime_type(checker, demand) ||
expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return return false
} }
expr := checker.ast_module.exprs[expr_id] expr := checker.ast_module.exprs[expr_id]
if expr.kind != .Name { #partial switch expr.kind {
return case .Name:
} if !symbol.is_valid(expr.qualifier) {
if !symbol.is_valid(expr.qualifier) { if index, ok := find_infer_local_index(locals, expr.name); ok {
if index, ok := find_infer_local_index(locals, expr.name); ok { return merge_local_demand(checker, &locals[index], demand, local_types)
_ = merge_local_demand(checker, &locals[index], demand, local_types) }
return }
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) return false
if !available {
return
}
global := find_global(checker, expr.name, target_pkg)
if global != ast.INVALID_GLOBAL {
_ = merge_global_demand(checker, global, demand)
}
} }
infer_all :: proc(checker: ^Checker) { infer_all :: proc(checker: ^Checker) {
// An "open constant" global has no concrete declared type and a compile-time // 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 // numeric initializer. Its slot can adopt a backward demand from any reachable use.
// reachable use can pick its family/width; absent a demand it defaults to the // Demands accumulate in global_demands so the default never blocks a later
// smallest signed type (legacy behaviour). Demands accumulate in global_demands so // cross-family demand (e.g. integer literal -> unsigned or float).
// the default never blocks a later cross-family (e.g. unsigned) demand.
for global, index in checker.ast_module.globals { for global, index in checker.ast_module.globals {
declared := type_from_syntax(global.type) declared := type_from_syntax(global.type)
if is_runtime_type(checker, declared) { if is_runtime_type(checker, declared) {
@@ -1994,6 +2110,8 @@ infer_all :: proc(checker: ^Checker) {
if constant.kind == .Value && fits_i64(constant.value) { if constant.kind == .Value && fits_i64(constant.value) {
checker.global_open_const[index] = true checker.global_open_const[index] = true
checker.global_const_value[index] = constant.value 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 checker.global_demands_dirty = false
spec_count := len(checker.specs) spec_count := len(checker.specs)
// Backward demands: a global whose initializer's root is a bare name referencing // Backward demands: a global pushes its own (declared or already-resolved) type
// another global pushes its own (declared or already-resolved) type onto that // onto open numeric slots reachable through names and numeric arithmetic.
// referent. Open constants adopt any fitting family; other referents widen only.
for global, index in checker.ast_module.globals { for global, index in checker.ast_module.globals {
if global.external { if global.external {
continue continue
@@ -2018,10 +2135,7 @@ infer_all :: proc(checker: ^Checker) {
if !is_runtime_type(checker, demand) { if !is_runtime_type(checker, demand) {
continue continue
} }
target := root_demand_target(checker, global.expr, global.pkg, global.file) _ = record_demand(checker, global.expr, demand, nil, nil, global.pkg, global.file)
if target != ast.INVALID_GLOBAL {
merge_global_demand(checker, target, demand)
}
} }
// Forward / resolution. infer_expr runs for every non-external global (even // 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 checker.global_types[index] = resolved
changed = true 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 { } else {
changed = merge_inferred_type(&checker.module.types, &checker.global_types[index], inferred) || changed changed = merge_inferred_type(&checker.module.types, &checker.global_types[index], inferred) || changed
} }
@@ -3723,18 +3842,20 @@ build_block :: proc(
case .Declaration: case .Declaration:
declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr) 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 // Adopt the type inference resolved for this local when the declaration has no
// concrete annotation (a constraint, `undefined`, or an un-annotated open // concrete annotation and inference carried useful numeric context: constraints,
// integer constant): the slot may have absorbed a backward demand (e.g. `a :: 10` // `undefined`, open numeric constants, or arithmetic expressions.
// built as u16 after `take_u16(a)`). Gated to compile-time integer constants so
// strings/arrays/pointers keep their own initializer type.
open_const_decl := !is_runtime_type(checker, declared) && !is_undefined_expr(checker, statement.expr) open_const_decl := !is_runtime_type(checker, declared) && !is_undefined_expr(checker, statement.expr)
if open_const_decl { if open_const_decl {
constant := eval_constant(checker, statement.expr) 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) && if statement_id != ast.INVALID_STMT && int(statement_id) < len(ctx.local_types) &&
is_runtime_type(checker, ctx.local_types[statement_id]) && 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] declared = ctx.local_types[statement_id]
} }
// A still-unresolved constraint means the initializer's numeric // 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); } else if constant := eval_constant(checker, global.expr);
constant.kind == .Value && fits_i64(constant.value) && constant.kind == .Value && fits_i64(constant.value) &&
is_runtime_type(checker, checker.global_types[global_index]) { is_runtime_type(checker, checker.global_types[global_index]) {
// Open constant: build the initializer against the type inference resolved // Open integer constant: build against its demanded/defaulted type. Gated
// for this slot, so it adopts its demanded/defaulted type (e.g. `A :: 10` // to the infer-side open-constant condition so out-of-range constants keep
// built as u16 when a use demanded u16). Gated to compile-time values fitting // their original "exceeds signed i64 range" diagnostic.
// i64 exactly the infer-side open-constant condition so out-of-range expected = checker.global_types[global_index]
// constants keep their original "exceeds signed i64 range" diagnostic. } 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] expected = checker.global_types[global_index]
} }
expr := build_expr(checker, global.expr, nil, &dependencies, &calls, expected, global.pkg, global.file) 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_types = make([]types.Type, len(ast_module.globals), allocator)
checker.global_demands = 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_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.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_canonical = make([]ast.Global_Id, len(ast_module.globals), allocator)
checker.external_global_diagnostics = make([]source.Diagnostic_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_types, allocator)
delete(checker.global_demands, allocator) delete(checker.global_demands, allocator)
delete(checker.global_open_const, allocator) delete(checker.global_open_const, allocator)
delete(checker.global_open_float, allocator)
delete(checker.global_const_value, allocator) delete(checker.global_const_value, allocator)
delete(checker.external_global_canonical, allocator) delete(checker.external_global_canonical, allocator)
delete(checker.external_global_diagnostics, 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) 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)
}