broaden type inference from context (arithmetic expressions)
This commit is contained in:
@@ -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:
|
||||
|
||||
+200
-75
@@ -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
|
||||
}
|
||||
#partial switch expr.kind {
|
||||
case .Name:
|
||||
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
|
||||
return merge_local_demand(checker, &locals[index], demand, local_types)
|
||||
}
|
||||
}
|
||||
target_pkg, available := expr_package(checker, expr, pkg, file)
|
||||
if !available {
|
||||
return
|
||||
return false
|
||||
}
|
||||
global := find_global(checker, expr.name, target_pkg)
|
||||
if global != ast.INVALID_GLOBAL {
|
||||
_ = merge_global_demand(checker, global, demand)
|
||||
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
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user