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
+14
View File
@@ -39,6 +39,20 @@ roadmap and milestone history.
- native sum composition with `A | B` for unbacked enums and tagged unions, using program-global `u16` variant ids - native sum composition with `A | B` for unbacked enums and tagged unions, using program-global `u16` variant ids
- fallible channel types `T ! E`, where `E` is a native enum/tagged union or supported sum composition - fallible channel types `T ! E`, where `E` is a native enum/tagged union or supported sum composition
#### native record constraint fields
A direct `int`, `float`, or `range` field in a named native struct or union is a
program-wide constraint, not per-value polymorphism. Before record layout, all reachable keyed
constructors, field assignments, and concrete uses of field reads contribute demands and the field
resolves once to one concrete runtime type. Compatible scalar demands widen normally. Integer
literals remain provisional until inference settles, so a later `usize` use can resolve an `int`
field to `usize`; otherwise literal-only `int` fields use the widest smallest-signed type required,
and literal-only `float` fields use `f64`.
An undemanded field or incompatible demands are errors. This inference applies only to direct
fields of named native records. `c_struct` fields, nested constraints such as `[]int`, and fields in
anonymous generated records still require concrete runtime types.
#### keyword member names #### keyword member names
Reserved keywords are valid native enum members and tagged-union variants when used in an Reserved keywords are valid native enum members and tagged-union variants when used in an
+355 -13
View File
@@ -174,6 +174,14 @@ Checker :: struct {
global_open_float: []bool, global_open_float: []bool,
global_const_value: []i128, global_const_value: []i128,
global_demands_dirty: bool, 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_canonical: []ast.Global_Id,
external_global_diagnostics: []source.Diagnostic_Id, external_global_diagnostics: []source.Diagnostic_Id,
constants: []Constant, 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) { validate_type_nodes :: proc(checker: ^Checker) {
for item, index in checker.module.types.nodes { for item, index in checker.module.types.nodes {
id := types.DYNAMIC_START+types.Type(index) 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 // variant has no runtime value, only a tag. Allowed only here, not for
// structs, untagged unions, or c_structs. // structs, untagged unions, or c_structs.
tagged_union := item.kind == .Union && types.is_enum(item.child, &checker.module.types) 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) { if tagged_union && types.is_void(field.type) {
// void variant: no payload to validate. // 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) { } else if !types.is_runtime_value(field.type, &checker.module.types) {
source.add( source.add(
checker.diagnostics, checker.diagnostics,
@@ -2850,7 +3068,11 @@ infer_compound_expr :: proc(
if types.is_pointer(value, store) { if types.is_pointer(value, store) {
value = types.child_type(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 return field.type if ok else types.INVALID
case .Unwrap: case .Unwrap:
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) 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) resize(&block_locals, capture_start)
return success return success
case .Struct_Literal: case .Struct_Literal:
for keyed in expr.args { value := types.INVALID
_ = infer_nested_expr(checker, checker.ast_module.exprs[keyed].left, locals, pkg, file, demanded, local_types) if expr.left != ast.INVALID_EXPR {
} value, _ = resolve_type_argument(checker, expr.left, pkg, file)
if !symbol.is_valid(expr.name) { } else if symbol.is_valid(expr.name) {
return types.INVALID
}
target_pkg, available := expr_package(checker, expr, pkg, file) 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 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) } else {
value = expected
}
value = types.resolve_alias(value, store)
for keyed in expr.args {
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,
)
}
}
return value
case .Keyed: case .Keyed:
return infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) return infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
case: case:
@@ -2996,8 +3240,12 @@ infer_expr :: proc(
base_type = types.child_type(base_type, &checker.module.types) base_type = types.child_type(base_type, &checker.module.types)
} }
if !types.is_valid(last) { 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 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 last = field.type
} }
} }
@@ -3020,8 +3268,12 @@ infer_expr :: proc(
base_type = types.child_type(base_type, &checker.module.types) base_type = types.child_type(base_type, &checker.module.types)
} }
if !types.is_valid(last) { 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 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 last = field.type
} }
} }
@@ -3575,6 +3827,16 @@ infer_statements :: proc(
locals^[local_index].mutable { locals^[local_index].mutable {
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types) _ = 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 { if !rhs_is_arith {
_ = record_demand(checker, statement.expr, target_type, locals^[:], local_types, pkg, file) _ = 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) 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 // 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( record_demand :: proc(
checker: ^Checker, checker: ^Checker,
expr_id: ast.Expr_Id, expr_id: ast.Expr_Id,
@@ -4115,6 +4408,13 @@ record_demand :: proc(
expr := checker.ast_module.exprs[expr_id] expr := checker.ast_module.exprs[expr_id]
#partial switch expr.kind { #partial switch expr.kind {
case .Name: 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 !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) return merge_local_demand(checker, &locals[index], demand, local_types)
@@ -4128,6 +4428,12 @@ record_demand :: proc(
if global != ast.INVALID_GLOBAL { if global != ast.INVALID_GLOBAL {
return merge_global_demand(checker, global, demand) 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: case .Negate:
if types.is_signed(demand, checker.target) || types.is_float(demand, checker.target) { 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) return record_demand(checker, expr.left, demand, locals, local_types, pkg, file)
@@ -4201,6 +4507,7 @@ infer_all :: proc(checker: ^Checker) {
for { for {
changed := false changed := false
checker.global_demands_dirty = false checker.global_demands_dirty = false
checker.record_field_demands_dirty = false
spec_count := len(checker.specs) spec_count := len(checker.specs)
// Backward demands: a global pushes its own (declared or already-resolved) type // 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 { if checker.global_demands_dirty {
changed = true changed = true
} }
if checker.record_field_demands_dirty {
changed = true
}
if !changed { if !changed {
if !defaults_applied { if !defaults_applied {
defaults_applied = true defaults_applied = true
@@ -4304,6 +4614,15 @@ infer_all :: proc(checker: ^Checker) {
defaulted = true 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 { if defaulted {
continue continue
} }
@@ -4802,6 +5121,19 @@ find_struct_field :: proc(checker: ^Checker, struct_type: types.Type, name: symb
return 0, {}, false 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 { field_type_from_value :: proc(checker: ^Checker, expr: ast.Expr, base_type: types.Type) -> types.Type {
store := &checker.module.types store := &checker.module.types
field_name := symbol_text(checker, expr.name) 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_const = make([]bool, len(ast_module.globals), allocator)
checker.global_open_float = 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.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_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)
for &canonical in checker.external_global_canonical { for &canonical in checker.external_global_canonical {
@@ -9983,6 +10320,10 @@ check :: proc(
delete(checker.global_open_const, allocator) delete(checker.global_open_const, allocator)
delete(checker.global_open_float, allocator) delete(checker.global_open_float, allocator)
delete(checker.global_const_value, 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_canonical, allocator)
delete(checker.external_global_diagnostics, allocator) delete(checker.external_global_diagnostics, allocator)
delete(checker.constants, allocator) delete(checker.constants, allocator)
@@ -10043,6 +10384,7 @@ check :: proc(
validate_declarations(&checker) validate_declarations(&checker)
configure_io_main(&checker) configure_io_main(&checker)
infer_all(&checker) infer_all(&checker)
finalize_record_field_inference(&checker)
validate_external_globals(&checker) validate_external_globals(&checker)
prune_specs(&checker) prune_specs(&checker)
build_globals(&checker) build_globals(&checker)
+217
View File
@@ -7463,6 +7463,7 @@ main func() void { value Bad(i32) = undefined; _ = &value }
symbol.destroy_table(&symbols) symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics) source.destroy_diagnostics(&diagnostics)
} }
} }
@(test) @(test)
@@ -7932,6 +7933,7 @@ parser_diagnoses_malformed_conditional_unwrap_captures_and_guards :: proc(t: ^te
symbol.destroy_table(&symbols) symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics) source.destroy_diagnostics(&diagnostics)
} }
} }
@(test) @(test)
@@ -10998,3 +11000,218 @@ main func() void {
} }
testing.expect(t, found) testing.expect(t, found)
} }
named_record_field_type :: proc(
module: ^hir.Module,
symbols: ^symbol.Table,
record_name, field_name: string,
) -> (types.Type, bool) {
record := types.find_named(&module.types, 0, u32(symbol.intern(symbols, record_name)))
if !types.is_record(record, &module.types) {
return types.INVALID, false
}
field_symbol := symbol.intern(symbols, field_name)
for field in types.fields_for(&module.types, record) {
if field.name == u32(field_symbol) {
return field.type, true
}
}
return types.INVALID, false
}
@(test)
native_record_constraint_fields_resolve_program_wide :: proc(t: ^testing.T) {
text := `Token :: struct { start int }
Backward :: struct { start int }
Wide :: struct { value int }
Literal :: struct { value int }
Measurement :: struct {
ratio float
span range
}
Payload :: union { count int }
take_usize func(value usize) usize { return value }
exercise func(cursor usize, narrow i8, wider i16, count i32) i32 {
token Token = Token{start = cursor}
wide Wide = Wide{value = narrow}
wide.value = wider
small Literal = Literal{value = 1}
large Literal = Literal{value = 1000}
backward Backward = Backward{start = 1}
measurement Measurement = Measurement{ratio = 1.5, span = 0..3}
payload Payload = Payload{count = count}
_ = token.start
_ = wide.value
_ = small.value
_ = large.value
_ = take_usize(backward.start)
_ = measurement.ratio
_ = measurement.span
_ = payload.count
return 0
}
main func() i32 { return exercise(7, 1, 1000, 3) }
`
source_file := source.Source{path="record_constraints.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)
token_start, token_ok := named_record_field_type(&hir_module, &symbols, "Token", "start")
backward_start, backward_ok := named_record_field_type(&hir_module, &symbols, "Backward", "start")
wide_value, wide_ok := named_record_field_type(&hir_module, &symbols, "Wide", "value")
literal_value, literal_ok := named_record_field_type(&hir_module, &symbols, "Literal", "value")
ratio, ratio_ok := named_record_field_type(&hir_module, &symbols, "Measurement", "ratio")
span, span_ok := named_record_field_type(&hir_module, &symbols, "Measurement", "span")
count, count_ok := named_record_field_type(&hir_module, &symbols, "Payload", "count")
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, token_ok && backward_ok && wide_ok && literal_ok && ratio_ok && span_ok && count_ok)
testing.expect_value(t, token_start, types.USIZE)
testing.expect_value(t, backward_start, types.USIZE)
testing.expect_value(t, wide_value, types.I16)
testing.expect_value(t, literal_value, types.I16)
testing.expect_value(t, ratio, types.F64)
testing.expect(t, types.is_range(span, &hir_module.types))
testing.expect_value(t, types.child_type(span, &hir_module.types), types.I8)
testing.expect_value(t, count, types.I32)
}
@(test)
native_record_int_field_compiles_and_runs_as_usize :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-record-field-usize"
main_path := "/tmp/brolang-test-record-field-usize/main.bro"
output := "/tmp/brolang-test-record-field-usize-output"
text := `Token :: struct { start int }
main func() i32 {
cursor usize = 7
token Token = Token{start = cursor}
if (token.start != cursor) return 1
return 0
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
native_record_constraint_fields_report_unresolved_and_conflicting_demands :: proc(t: ^testing.T) {
cases := [2]struct {
text: string,
needle: string,
}{
{
text = `Token :: struct { start int }
main func() void {}
`,
needle = "could not resolve the 'int' constraint for field 'Token.start'",
},
{
text = `Token :: struct { start int }
use func(signed i32, unsigned usize) void {
a Token = Token{start = signed}
b Token = Token{start = unsigned}
_ = a
_ = b
}
main func() void { use(1, 2) }
`,
needle = "conflicting types i32 and usize for field 'Token.start' declared as 'int'",
},
}
for test_case in cases {
source_file := source.Source{path="bad_record_constraint.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
ir_module := lower.lower(&hir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
matching := 0
for diagnostic in diagnostics.items {
matching += 1 if strings.contains(diagnostic.message, test_case.needle) else 0
}
testing.expect_value(t, matching, 1)
testing.expect(t, len(llvm_text) > 0)
delete(llvm_text)
ir.destroy_module(&ir_module)
hir.destroy_module(&hir_module)
ast.destroy_module(&ast_module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
directory := "/tmp/brolang-test-record-field-conflict"
main_path := "/tmp/brolang-test-record-field-conflict/main.bro"
output := "/tmp/brolang-test-record-field-conflict-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)cases[1].text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
constraint_fields_remain_rejected_outside_named_native_records :: proc(t: ^testing.T) {
text := `BadC :: c_struct { value int }
BadNested :: struct { values []int }
make_type func() type {
return struct { value int }
}
main func() void {
Generated :: @make_type()
_ = Generated
}
`
source_file := source.Source{path="excluded_record_constraints.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)
record_errors := 0
anonymous_error := false
for diagnostic in diagnostics.items {
record_errors += 1 if strings.contains(diagnostic.message, "record fields must have runtime value types") else 0
anonymous_error = anonymous_error || strings.contains(diagnostic.message, "anonymous struct field 'value' requires a concrete runtime type")
}
c_field, c_ok := named_record_field_type(&hir_module, &symbols, "BadC", "value")
nested_field, nested_ok := named_record_field_type(&hir_module, &symbols, "BadNested", "values")
testing.expect_value(t, record_errors, 1)
testing.expect(t, anonymous_error)
testing.expect(t, c_ok && nested_ok)
testing.expect_value(t, c_field, types.INT)
testing.expect(t, types.is_slice(nested_field, &hir_module.types))
testing.expect_value(t, types.child_type(nested_field, &hir_module.types), types.INT)
}