deferred defaulting typing

This commit is contained in:
2026-06-26 18:00:01 +02:00
parent f9ea044169
commit 77d4c1d494
3 changed files with 189 additions and 22 deletions
+144 -18
View File
@@ -1510,10 +1510,17 @@ infer_expr :: proc(
expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) {
last = right
} else if is_numeric_demand(frame.left, checker.target) &&
!numeric_operand_is_open(checker, expr.left, locals, pkg, file) &&
expr_accepts_numeric_demand(checker, expr.right, frame.left, locals, pkg, file) {
// Propagate only from an authoritative (fixed-type) left operand. A left
// operand that is still a provisional open constant carries only its
// smallest-signed default, which must not poison the sibling's family;
// two provisional operands are resolved together by the backward demand
// from the declaration/use.
_ = record_demand(checker, expr.right, frame.left, locals, local_types, pkg, file)
last = frame.left
} else if is_numeric_demand(right, checker.target) &&
!numeric_operand_is_open(checker, expr.right, locals, pkg, file) &&
expr_accepts_numeric_demand(checker, expr.left, right, locals, pkg, file) {
_ = record_demand(checker, expr.left, right, locals, local_types, pkg, file)
last = right
@@ -1540,6 +1547,17 @@ infer_expr :: proc(
for arg_index in 0..<len(expr.args) {
record_demand(checker, expr.args[arg_index], call_arg_expected(function, arg_index), locals, local_types, pkg, file)
}
// Deferred defaulting leaves an undemanded open constant typeless; give such an
// argument its default so the call can still monomorphize (the default feeds only
// the spec arg vector, not a demand).
for arg_index in 0..<len(expr.args) {
if !is_runtime_type(checker, stack[frame_index].args[arg_index]) {
fallback := open_const_default_type(checker, expr.args[arg_index], locals, pkg, file)
if is_runtime_type(checker, fallback) {
stack[frame_index].args[arg_index] = fallback
}
}
}
if valid_call_arity(function, len(expr.args)) &&
can_specialize(checker, function, stack[frame_index].args) {
spec := INVALID_SPEC
@@ -1723,9 +1741,23 @@ infer_statements :: proc(
record_infer_local_type(local, local_types)
// A typed/constraint declaration initialized by a bare name pushes its resolved
// type backward onto that name (mirrors the `Y int :: X; Z i32 :: Y` global chain).
record_demand(checker, statement.expr, value_type, locals^[:], local_types, pkg, file)
// Skip this for a constraint/untyped declaration whose initializer is arithmetic:
// value_type is then only a provisional default (e.g. `x int = a - b` resolving to
// i16 before the operands' real uses are seen) and would poison the open-constant
// operands' family. The operands resolve from their own authoritative uses, and the
// local adopts their resolved type forward. Concrete-declared arithmetic (e.g.
// `b u16 :: a + 2`) still pushes, since value_type is the concrete declared type.
init_is_arith := is_arith_kind(checker.ast_module.exprs[statement.expr].kind)
if is_runtime_type(checker, declared_local) || !init_is_arith {
record_demand(checker, statement.expr, value_type, locals^[:], local_types, pkg, file)
}
case .Assignment:
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
// Only push the target's type back onto a bare-name RHS (e.g. `x += speed`):
// pushing through an arithmetic RHS would feed the target's (often provisional)
// type onto open-constant operands and poison their family. Operands of an
// arithmetic RHS resolve from their own authoritative uses.
rhs_is_arith := is_arith_kind(checker.ast_module.exprs[statement.expr].kind)
if statement.target != ast.INVALID_EXPR {
target_type := infer_expr(checker, statement.target, locals^[:], pkg, file, demanded, local_types)
target_expr := checker.ast_module.exprs[statement.target]
@@ -1735,16 +1767,16 @@ infer_statements :: proc(
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
}
}
// Push the target's concrete type backward onto the RHS so a const used
// only in an assignment (e.g. `x += speed`) resolves, mirroring how
// declarations and returns demand their context (record_demand self-gates
// on a concrete demand and only touches fitting open slots).
_ = record_demand(checker, statement.expr, target_type, locals^[:], local_types, pkg, file)
if !rhs_is_arith {
_ = record_demand(checker, statement.expr, target_type, locals^[:], local_types, pkg, file)
}
} else if statement.name != checker.sink_symbol {
if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok &&
locals^[local_index].mutable {
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
_ = record_demand(checker, statement.expr, locals^[local_index].type, locals^[:], local_types, pkg, file)
if !rhs_is_arith {
_ = record_demand(checker, statement.expr, locals^[local_index].type, locals^[:], local_types, pkg, file)
}
}
}
case .Expression:
@@ -2001,6 +2033,92 @@ merge_local_demand :: proc(checker: ^Checker, local: ^Infer_Local, demand: types
return false
}
is_arith_kind :: proc(k: ast.Expr_Kind) -> bool {
return k == .Add || k == .Sub || k == .Mul || k == .Div || k == .Negate
}
// open_const_default_type returns the fallback type a bare open-constant reference
// (local or global) would take if no use ever demands it: the smallest signed type
// that holds an integer constant, or f64 for a float constant. Used to give a call's
// argument a concrete type for monomorphization when deferred defaulting has left the
// open constant typeless (its param is a constraint, so the call records no demand on
// it). The fallback feeds only the specialization's arg vector, never a demand, so it
// cannot leak back onto the constant or a sibling.
open_const_default_type :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> types.Type {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) {
return types.INVALID
}
expr := checker.ast_module.exprs[expr_id]
if expr.kind != .Name {
return types.INVALID
}
if !symbol.is_valid(expr.qualifier) {
if index, ok := find_infer_local_index(locals, expr.name); ok {
local := locals[index]
if local.open_const {
return types.smallest_signed_for_literal(i64(local.const_value))
}
if local.open_float {
return types.F64
}
return types.INVALID
}
}
target_pkg, available := expr_package(checker, expr, pkg, file)
if !available {
return types.INVALID
}
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 types.INVALID
}
if checker.global_open_const[index] {
return types.smallest_signed_for_literal(i64(checker.global_const_value[index]))
}
if checker.global_open_float[index] {
return types.F64
}
return types.INVALID
}
// numeric_operand_is_open reports whether an arithmetic operand still carries a
// provisional type (an open constant at its smallest-signed default, or a bare
// numeric literal) rather than an authoritative one. A provisional operand must not
// propagate its type onto a sibling open constant: doing so locks the sibling into a
// default family and blocks the real backward demand from the declaration/use. Two
// provisional operands are instead resolved together by that backward demand.
numeric_operand_is_open :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_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]
#partial switch expr.kind {
case .Name:
// A bare open-constant name is open iff it has a deferred default to assign.
return is_runtime_type(checker, open_const_default_type(checker, expr_id, locals, pkg, file))
case .Negate:
return numeric_operand_is_open(checker, expr.left, locals, pkg, file)
case .Add, .Sub, .Mul, .Div:
return numeric_operand_is_open(checker, expr.left, locals, pkg, file) ||
numeric_operand_is_open(checker, expr.right, locals, pkg, file)
}
// A bare integer/float literal adapts freely, so it too is provisional.
return is_numeric_constant_expr(checker, expr_id)
}
expr_accepts_numeric_demand :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
@@ -2162,17 +2280,12 @@ infer_all :: proc(checker: ^Checker) {
checker.global_types[index] = checker.global_demands[index]
changed = true
}
} else if checker.global_open_const[index] {
resolved := types.smallest_signed_for_literal(i64(checker.global_const_value[index]))
if !types.equal(checker.global_types[index], resolved) {
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 if checker.global_open_const[index] || checker.global_open_float[index] {
// Defer defaulting: an undemanded open constant stays typeless during the
// fixpoint so its provisional smallest-signed default never leaks as a
// demand and poisons a sibling open constant used in the same arithmetic
// (e.g. the lagging type in `x += speed`). The pass after this loop assigns
// the default once the fixpoint settles and no further demand can arrive.
} else {
changed = merge_inferred_type(&checker.module.types, &checker.global_types[index], inferred) || changed
}
@@ -2195,6 +2308,19 @@ infer_all :: proc(checker: ^Checker) {
break
}
}
// Any open constant that no use ever demanded now takes its default: an integer
// constant the smallest signed type that holds its value, a float constant f64.
for global, index in checker.ast_module.globals {
if global.external || is_runtime_type(checker, checker.global_types[index]) {
continue
}
if checker.global_open_const[index] {
checker.global_types[index] = types.smallest_signed_for_literal(i64(checker.global_const_value[index]))
} else if checker.global_open_float[index] {
checker.global_types[index] = types.F64
}
}
}
prune_specs :: proc(checker: ^Checker) {