scalar family constraints for struct fields
This commit is contained in:
+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)
|
||||
|
||||
Reference in New Issue
Block a user