scalar family constraints for struct fields

This commit is contained in:
2026-07-13 23:00:42 +02:00
parent 6de4d9f9f3
commit 0eeacc2e37
3 changed files with 585 additions and 12 deletions
+354 -12
View File
@@ -174,6 +174,14 @@ Checker :: struct {
global_open_float: []bool,
global_const_value: []i128,
global_demands_dirty: bool,
// Program-wide inference for direct constraint fields in named native records.
// The arrays use Store.fields' existing dense indices; resolved concrete types
// live directly in module.types.fields so layout and lowering need no side table.
record_field_constraints: []types.Type,
record_field_defaults: []types.Type,
record_field_conflicts: []types.Type,
record_field_conflict_spans: []source.Span,
record_field_demands_dirty: bool,
external_global_canonical: []ast.Global_Id,
external_global_diagnostics: []source.Diagnostic_Id,
constants: []Constant,
@@ -2399,6 +2407,211 @@ validate_declarations :: proc(checker: ^Checker) {
}
}
init_record_field_inference :: proc(checker: ^Checker) {
store := &checker.module.types
for item in store.nodes {
if !(item.declared && (item.kind == .Struct || item.kind == .Union) &&
!item.c_layout && symbol.is_valid(symbol.Id(item.name))) {
continue
}
start := int(item.field_start)
end := start+int(item.field_count)
if start < 0 || end > len(store.fields) {
continue
}
for slot in start..<end {
if types.is_constraint(store.fields[slot].type) {
checker.record_field_constraints[slot] = store.fields[slot].type
}
}
}
}
is_inferred_record_field :: proc(checker: ^Checker, slot: int) -> bool {
return slot >= 0 && slot < len(checker.record_field_constraints) &&
types.is_constraint(checker.record_field_constraints[slot])
}
record_field_owner :: proc(checker: ^Checker, slot: int) -> (symbol.Id, symbol.Id, bool) {
for item in checker.module.types.nodes {
if !(item.declared && (item.kind == .Struct || item.kind == .Union) &&
!item.c_layout && symbol.is_valid(symbol.Id(item.name))) {
continue
}
start := int(item.field_start)
if slot >= start && slot < start+int(item.field_count) &&
slot >= 0 && slot < len(checker.module.types.fields) {
return symbol.Id(item.name), symbol.Id(checker.module.types.fields[slot].name), true
}
}
return symbol.INVALID, symbol.INVALID, false
}
record_field_conflict :: proc(checker: ^Checker, slot: int, actual: types.Type, span: source.Span) {
if !is_inferred_record_field(checker, slot) ||
types.is_valid(checker.record_field_conflicts[slot]) {
return
}
checker.record_field_conflicts[slot] = actual
checker.record_field_conflict_spans[slot] = span
}
merge_record_field_default :: proc(checker: ^Checker, slot: int, candidate: types.Type, span: source.Span) -> bool {
if !is_inferred_record_field(checker, slot) || !is_runtime_type(checker, candidate) {
return false
}
constraint := checker.record_field_constraints[slot]
if !types.constraint_accepts(constraint, candidate, &checker.module.types) {
record_field_conflict(checker, slot, candidate, span)
return false
}
current := checker.record_field_defaults[slot]
if !is_runtime_type(checker, current) {
checker.record_field_defaults[slot] = candidate
checker.record_field_demands_dirty = true
return true
}
if types.equal(current, candidate) {
return false
}
merged := types.widest(current, candidate)
if types.is_concrete_scalar(merged) {
if !types.equal(current, merged) {
checker.record_field_defaults[slot] = merged
checker.record_field_demands_dirty = true
return true
}
return false
}
record_field_conflict(checker, slot, candidate, span)
return false
}
merge_record_field_demand :: proc(checker: ^Checker, slot: int, demand: types.Type, span: source.Span) -> bool {
if !is_inferred_record_field(checker, slot) || !is_runtime_type(checker, demand) {
return false
}
constraint := checker.record_field_constraints[slot]
if !types.constraint_accepts(constraint, demand, &checker.module.types) {
record_field_conflict(checker, slot, demand, span)
return false
}
current := checker.module.types.fields[slot].type
if types.is_constraint(current) {
checker.module.types.fields[slot].type = demand
checker.record_field_demands_dirty = true
return true
}
if types.equal(current, demand) {
return false
}
merged := types.widest(current, demand)
if types.is_concrete_scalar(merged) {
if !types.equal(current, merged) {
checker.module.types.fields[slot].type = merged
checker.record_field_demands_dirty = true
return true
}
return false
}
record_field_conflict(checker, slot, demand, span)
return false
}
record_field_expr_candidate :: proc(
checker: ^Checker,
slot: int,
expr_id: ast.Expr_Id,
inferred: types.Type,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> bool {
if !is_inferred_record_field(checker, slot) || expr_id == ast.INVALID_EXPR {
return false
}
expr := checker.ast_module.exprs[expr_id]
constraint := checker.record_field_constraints[slot]
if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file);
constant.kind == .Value && fits_i64(constant.value) {
candidate := types.smallest_signed_for_literal(i64(constant.value))
if constraint == types.FLOAT {
candidate = types.F64
}
return merge_record_field_default(checker, slot, candidate, expr.span)
}
if is_float_constant_expr(checker, expr_id) {
return merge_record_field_default(checker, slot, types.F64, expr.span)
}
if numeric_operand_is_open(checker, expr_id, locals, pkg, file) {
candidate := inferred
if constraint == types.FLOAT && types.is_concrete_integer(candidate) {
candidate = types.F64
}
return merge_record_field_default(checker, slot, candidate, expr.span)
}
concrete := types.constraint_target(constraint, inferred, &checker.module.types)
if !is_runtime_type(checker, concrete) {
if is_runtime_type(checker, inferred) {
record_field_conflict(checker, slot, inferred, expr.span)
}
return false
}
return merge_record_field_demand(checker, slot, concrete, expr.span)
}
finalize_record_field_inference :: proc(checker: ^Checker) {
for constraint, slot in checker.record_field_constraints {
if !types.is_constraint(constraint) {
continue
}
record_name, field_name, ok := record_field_owner(checker, slot)
if !ok {
continue
}
current := checker.module.types.fields[slot].type
conflict := checker.record_field_conflicts[slot]
if types.is_valid(conflict) {
if is_runtime_type(checker, current) {
source.addf(
checker.diagnostics,
checker.record_field_conflict_spans[slot],
"conflicting types %s and %s for field '%s.%s' declared as '%s'",
types.name(current), types.name(conflict),
symbol_text(checker, record_name), symbol_text(checker, field_name),
types.name(constraint),
)
} else {
source.addf(
checker.diagnostics,
checker.record_field_conflict_spans[slot],
"type %s does not satisfy the '%s' constraint for field '%s.%s'",
types.name(conflict), types.name(constraint),
symbol_text(checker, record_name), symbol_text(checker, field_name),
)
}
} else if types.is_constraint(current) {
source.addf(
checker.diagnostics,
source.Span{},
"could not resolve the '%s' constraint for field '%s.%s'",
types.name(constraint),
symbol_text(checker, record_name), symbol_text(checker, field_name),
)
}
if types.is_constraint(current) {
switch constraint {
case types.INT:
checker.module.types.fields[slot].type = types.I64
case types.FLOAT:
checker.module.types.fields[slot].type = types.F64
case types.RANGE:
checker.module.types.fields[slot].type = types.range(&checker.module.types, types.I64)
}
}
}
}
validate_type_nodes :: proc(checker: ^Checker) {
for item, index in checker.module.types.nodes {
id := types.DYNAMIC_START+types.Type(index)
@@ -2459,9 +2672,14 @@ validate_type_nodes :: proc(checker: ^Checker) {
// variant has no runtime value, only a tag. Allowed only here, not for
// structs, untagged unions, or c_structs.
tagged_union := item.kind == .Union && types.is_enum(item.child, &checker.module.types)
for field in types.fields_for(&checker.module.types, id) {
for field, field_index in types.fields_for(&checker.module.types, id) {
field_slot := int(item.field_start)+field_index
if tagged_union && types.is_void(field.type) {
// void variant: no payload to validate.
} else if is_inferred_record_field(checker, field_slot) {
// Direct constraints in named native records are validated after the
// program-wide inference fixpoint. C, nested, and anonymous fields do
// not enter this state and retain the existing validation below.
} else if !types.is_runtime_value(field.type, &checker.module.types) {
source.add(
checker.diagnostics,
@@ -2850,7 +3068,11 @@ infer_compound_expr :: proc(
if types.is_pointer(value, store) {
value = types.child_type(value, store)
}
_, field, ok := find_struct_field(checker, value, expr.name)
slot, field, ok := find_struct_field_slot(checker, value, expr.name)
if ok && is_inferred_record_field(checker, slot) && is_runtime_type(checker, expected) {
_ = merge_record_field_demand(checker, slot, expected, expr.span)
field = checker.module.types.fields[slot]
}
return field.type if ok else types.INVALID
case .Unwrap:
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
@@ -2887,15 +3109,37 @@ infer_compound_expr :: proc(
resize(&block_locals, capture_start)
return success
case .Struct_Literal:
value := types.INVALID
if expr.left != ast.INVALID_EXPR {
value, _ = resolve_type_argument(checker, expr.left, pkg, file)
} else if symbol.is_valid(expr.name) {
target_pkg, available := expr_package(checker, expr, pkg, file)
value = types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID
} else {
value = expected
}
value = types.resolve_alias(value, store)
for keyed in expr.args {
_ = infer_nested_expr(checker, checker.ast_module.exprs[keyed].left, locals, pkg, file, demanded, local_types)
keyed_expr := checker.ast_module.exprs[keyed]
if keyed_expr.left == ast.INVALID_EXPR {
continue
}
slot, field, ok := find_struct_field_slot(checker, value, keyed_expr.name)
if !ok || !is_inferred_record_field(checker, slot) {
_ = infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types)
continue
}
field_expected := field.type if is_runtime_type(checker, field.type) else types.INVALID
actual := infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types, field_expected)
_ = record_field_expr_candidate(checker, slot, keyed_expr.left, actual, locals, pkg, file)
if is_runtime_type(checker, checker.module.types.fields[slot].type) {
_ = record_demand(
checker, keyed_expr.left, checker.module.types.fields[slot].type,
locals, local_types, pkg, file,
)
}
}
if !symbol.is_valid(expr.name) {
return types.INVALID
}
target_pkg, available := expr_package(checker, expr, pkg, file)
value := types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID
return types.resolve_alias(value, store)
return value
case .Keyed:
return infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
case:
@@ -2996,8 +3240,12 @@ infer_expr :: proc(
base_type = types.child_type(base_type, &checker.module.types)
}
if !types.is_valid(last) {
_, field, ok := find_struct_field(checker, base_type, expr.name)
slot, field, ok := find_struct_field_slot(checker, base_type, expr.name)
if ok {
if is_inferred_record_field(checker, slot) && is_runtime_type(checker, frame.expected) {
_ = merge_record_field_demand(checker, slot, frame.expected, expr.span)
field = checker.module.types.fields[slot]
}
last = field.type
}
}
@@ -3020,8 +3268,12 @@ infer_expr :: proc(
base_type = types.child_type(base_type, &checker.module.types)
}
if !types.is_valid(last) {
_, field, ok := find_struct_field(checker, base_type, expr.name)
slot, field, ok := find_struct_field_slot(checker, base_type, expr.name)
if ok {
if is_inferred_record_field(checker, slot) && is_runtime_type(checker, frame.expected) {
_ = merge_record_field_demand(checker, slot, frame.expected, expr.span)
field = checker.module.types.fields[slot]
}
last = field.type
}
}
@@ -3575,6 +3827,16 @@ infer_statements :: proc(
locals^[local_index].mutable {
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
}
} else if target_expr.kind == .Field ||
target_expr.kind == .Name && symbol.is_valid(target_expr.qualifier) {
if slot, ok := inferred_record_field_slot_from_expr(
checker, target_expr, locals^[:], local_types, pkg, file,
); ok {
_ = record_field_expr_candidate(
checker, slot, statement.expr, value_type, locals^[:], pkg, file,
)
target_type = checker.module.types.fields[slot].type
}
}
if !rhs_is_arith {
_ = record_demand(checker, statement.expr, target_type, locals^[:], local_types, pkg, file)
@@ -4097,8 +4359,39 @@ record_demand_shallow :: proc(
return record_demand(checker, expr_id, demand, locals, local_types, pkg, file)
}
inferred_record_field_slot_from_expr :: proc(
checker: ^Checker,
expr: ast.Expr,
locals: []Infer_Local,
local_types: []types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> (int, bool) {
base := types.INVALID
field_name := expr.name
if expr.kind == .Field {
base = infer_nested_expr(checker, expr.left, locals, pkg, file, nil, local_types)
} else if expr.kind == .Name && symbol.is_valid(expr.qualifier) &&
find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT {
base = find_infer_local(locals, expr.qualifier)
if !types.is_valid(base) {
if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL {
base = checker.global_types[global]
}
}
} else {
return -1, false
}
if types.is_pointer(base, &checker.module.types) {
base = types.child_type(base, &checker.module.types)
}
slot, _, ok := find_struct_field_slot(checker, base, field_name)
return slot, ok && is_inferred_record_field(checker, slot)
}
// 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).
// through bare names, inferred record fields, and numeric arithmetic. Calls remain
// a boundary (milestone 14.5).
record_demand :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
@@ -4115,6 +4408,13 @@ record_demand :: proc(
expr := checker.ast_module.exprs[expr_id]
#partial switch expr.kind {
case .Name:
if symbol.is_valid(expr.qualifier) {
if slot, ok := inferred_record_field_slot_from_expr(
checker, expr, locals, local_types, pkg, file,
); ok {
return merge_record_field_demand(checker, slot, demand, expr.span)
}
}
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)
@@ -4128,6 +4428,12 @@ record_demand :: proc(
if global != ast.INVALID_GLOBAL {
return merge_global_demand(checker, global, demand)
}
case .Field:
if slot, ok := inferred_record_field_slot_from_expr(
checker, expr, locals, local_types, pkg, file,
); ok {
return merge_record_field_demand(checker, slot, demand, expr.span)
}
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)
@@ -4201,6 +4507,7 @@ infer_all :: proc(checker: ^Checker) {
for {
changed := false
checker.global_demands_dirty = false
checker.record_field_demands_dirty = false
spec_count := len(checker.specs)
// Backward demands: a global pushes its own (declared or already-resolved) type
@@ -4286,6 +4593,9 @@ infer_all :: proc(checker: ^Checker) {
if checker.global_demands_dirty {
changed = true
}
if checker.record_field_demands_dirty {
changed = true
}
if !changed {
if !defaults_applied {
defaults_applied = true
@@ -4304,6 +4614,15 @@ infer_all :: proc(checker: ^Checker) {
defaulted = true
}
}
for fallback, slot in checker.record_field_defaults {
if !is_inferred_record_field(checker, slot) ||
!types.is_constraint(checker.module.types.fields[slot].type) ||
!is_runtime_type(checker, fallback) {
continue
}
checker.module.types.fields[slot].type = fallback
defaulted = true
}
if defaulted {
continue
}
@@ -4802,6 +5121,19 @@ find_struct_field :: proc(checker: ^Checker, struct_type: types.Type, name: symb
return 0, {}, false
}
find_struct_field_slot :: proc(checker: ^Checker, struct_type: types.Type, name: symbol.Id) -> (int, types.Field, bool) {
item, ok := types.node(&checker.module.types, struct_type)
if !ok || (item.kind != .Struct && item.kind != .Union) {
return -1, {}, false
}
for field, index in types.fields_for(&checker.module.types, struct_type) {
if field.name == u32(name) {
return int(item.field_start)+index, field, true
}
}
return -1, {}, false
}
field_type_from_value :: proc(checker: ^Checker, expr: ast.Expr, base_type: types.Type) -> types.Type {
store := &checker.module.types
field_name := symbol_text(checker, expr.name)
@@ -9956,6 +10288,11 @@ check :: proc(
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.record_field_constraints = make([]types.Type, len(checker.module.types.fields), allocator)
checker.record_field_defaults = make([]types.Type, len(checker.module.types.fields), allocator)
checker.record_field_conflicts = make([]types.Type, len(checker.module.types.fields), allocator)
checker.record_field_conflict_spans = make([]source.Span, len(checker.module.types.fields), allocator)
init_record_field_inference(&checker)
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)
for &canonical in checker.external_global_canonical {
@@ -9983,6 +10320,10 @@ check :: proc(
delete(checker.global_open_const, allocator)
delete(checker.global_open_float, allocator)
delete(checker.global_const_value, allocator)
delete(checker.record_field_constraints, allocator)
delete(checker.record_field_defaults, allocator)
delete(checker.record_field_conflicts, allocator)
delete(checker.record_field_conflict_spans, allocator)
delete(checker.external_global_canonical, allocator)
delete(checker.external_global_diagnostics, allocator)
delete(checker.constants, allocator)
@@ -10043,6 +10384,7 @@ check :: proc(
validate_declarations(&checker)
configure_io_main(&checker)
infer_all(&checker)
finalize_record_field_inference(&checker)
validate_external_globals(&checker)
prune_specs(&checker)
build_globals(&checker)