scalar family constraints for struct fields
This commit is contained in:
+14
@@ -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
|
||||
- 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
|
||||
|
||||
Reserved keywords are valid native enum members and tagged-union variants when used in an
|
||||
|
||||
+354
-12
@@ -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)
|
||||
|
||||
@@ -7463,6 +7463,7 @@ main func() void { value Bad(i32) = undefined; _ = &value }
|
||||
symbol.destroy_table(&symbols)
|
||||
source.destroy_diagnostics(&diagnostics)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -7932,6 +7933,7 @@ parser_diagnoses_malformed_conditional_unwrap_captures_and_guards :: proc(t: ^te
|
||||
symbol.destroy_table(&symbols)
|
||||
source.destroy_diagnostics(&diagnostics)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -10998,3 +11000,218 @@ main func() void {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user