default struct fields

This commit is contained in:
2026-07-18 16:23:47 +02:00
parent e889a99e55
commit 9eb7476522
10 changed files with 191 additions and 11 deletions
+11
View File
@@ -304,6 +304,14 @@ Declaration_Alias :: struct {
diagnostic: source.Diagnostic_Id,
}
Struct_Field_Default :: struct {
record: Type_Syntax,
field: symbol.Id,
expr: Expr_Id,
pkg: Package_Id,
file: File_Id,
}
File :: struct {
source: source.Source_Id,
pkg: Package_Id,
@@ -357,6 +365,7 @@ Module :: struct {
c_trampolines: [dynamic]Trampoline,
strings: [dynamic]string,
type_fields: [dynamic]types.Field,
struct_field_defaults: [dynamic]Struct_Field_Default,
type_uses: [dynamic]Type_Use,
type_store: types.Store,
allocator: mem.Allocator,
@@ -379,6 +388,7 @@ init_module :: proc(allocator := context.allocator) -> Module {
module.c_trampolines.allocator = allocator
module.strings.allocator = allocator
module.type_fields.allocator = allocator
module.struct_field_defaults.allocator = allocator
module.type_uses.allocator = allocator
return module
}
@@ -436,6 +446,7 @@ destroy_module :: proc(module: ^Module) {
delete(module.c_trampolines)
delete(module.strings)
delete(module.type_fields)
delete(module.struct_field_defaults)
delete(module.type_uses)
types.destroy_store(&module.type_store)
}
+67
View File
@@ -4471,12 +4471,19 @@ infer_compound_expr :: proc(
}
return value
}
fields := types.fields_for(store, value)
item, item_ok := types.node(store, value)
initialized := make([]bool, len(fields), checker.allocator)
defer delete(initialized, checker.allocator)
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 && item_ok {
initialized[slot-int(item.field_start)] = true
}
if !ok || !is_inferred_record_field(checker, slot) {
_ = infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types)
continue
@@ -4491,6 +4498,19 @@ infer_compound_expr :: proc(
)
}
}
if item_ok && item.kind == .Struct && !item.tuple {
for field, index in fields {
if initialized[index] {
continue
}
if field_default, ok := find_struct_field_default(checker, value, symbol.Id(field.name)); ok {
_ = infer_nested_expr(
checker, field_default.expr, nil, field_default.pkg, field_default.file,
demanded, expected=field.type,
)
}
}
}
return value
case .Keyed:
return infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
@@ -6098,6 +6118,27 @@ infer_all :: proc(checker: ^Checker) {
checker.record_field_demands_dirty = false
spec_count := len(checker.specs)
for field_default in checker.ast_module.struct_field_defaults {
slot, field, ok := find_struct_field_slot(checker, field_default.record, field_default.field)
if !ok {
continue
}
expected := field.type if is_runtime_type(checker, field.type) else types.INVALID
inferred := infer_expr(
checker, field_default.expr, nil, field_default.pkg, field_default.file,
expected=expected,
)
_ = record_field_expr_candidate(
checker, slot, field_default.expr, inferred, nil, field_default.pkg, field_default.file,
)
if is_runtime_type(checker, checker.module.types.fields[slot].type) {
_ = record_demand(
checker, field_default.expr, checker.module.types.fields[slot].type,
nil, nil, field_default.pkg, field_default.file,
)
}
}
// Backward demands: a global pushes its own (declared or already-resolved) type
// onto open numeric slots reachable through names and numeric arithmetic.
for global, index in checker.ast_module.globals {
@@ -6753,6 +6794,21 @@ find_struct_field :: proc(checker: ^Checker, struct_type: types.Type, name: symb
return 0, {}, false
}
find_struct_field_default :: proc(
checker: ^Checker,
struct_type: types.Type,
name: symbol.Id,
) -> (ast.Struct_Field_Default, bool) {
resolved := types.resolve_alias(struct_type, &checker.module.types)
for field_default in checker.ast_module.struct_field_defaults {
if types.resolve_alias(field_default.record, &checker.module.types) == resolved &&
field_default.field == name {
return field_default, true
}
}
return {}, false
}
find_tuple_field :: proc(checker: ^Checker, tuple_type: types.Type, index: u64) -> (int, types.Field, bool) {
item, ok := types.node(&checker.module.types, tuple_type)
if !ok || item.kind != .Struct || !item.tuple || index >= u64(item.field_count) {
@@ -7895,6 +7951,17 @@ build_compound_expr :: proc(
if values[index] != hir.INVALID_EXPR {
continue
}
if field_default, ok := find_struct_field_default(checker, struct_type, symbol.Id(field.name)); ok {
values[index] = build_nested_expr(
checker, field_default.expr, nil, global_reads, calls,
field.type, field_default.pkg, field_default.file,
)
values[index] = coerce_expr(
checker, values[index], field.type,
checker.ast_module.exprs[field_default.expr].span,
)
continue
}
id := source.addf(checker.diagnostics, expr.span, "missing initializer for struct field '%s'", symbol_text(checker, symbol.Id(field.name)))
delete(values, checker.allocator)
return invalid_hir_expr(checker, expr.span, id, struct_type)
+17 -1
View File
@@ -1505,9 +1505,25 @@ ct_eval_struct_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Ty
}
if !union_record {
for field, index in fields {
if values[index] == INVALID_CT_VALUE {
if values[index] != INVALID_CT_VALUE {
continue
}
field_default, has_default := find_struct_field_default(checker, struct_type, symbol.Id(field.name))
if !has_default {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "missing initializer for struct field '%s'", symbol_text(checker, symbol.Id(field.name)))
}
previous_pkg, previous_file := state.pkg, state.file
state.pkg, state.file = field_default.pkg, field_default.file
value, flow, ok := ct_eval_expr(state, field_default.expr, field.type, depth+1)
state.pkg, state.file = previous_pkg, previous_file
if !ok || flow.kind != .Normal {
return INVALID_CT_VALUE, flow, ok
}
value, ok = ct_coerce_value(state, value, field.type, checker.ast_module.exprs[field_default.expr].span)
if !ok {
return INVALID_CT_VALUE, ct_flow(.Normal), false
}
values[index] = value
}
}
start := u32(len(state.children))
+3
View File
@@ -1886,6 +1886,9 @@ canonicalize_types :: proc(module: ^ast.Module, allocator: mem.Allocator) {
for &field in module.type_fields {
field.type = canonical_type(module, field.type, mapping, visiting)
}
for &field_default in module.struct_field_defaults {
field_default.record = canonical_type(module, field_default.record, mapping, visiting)
}
for index := 0; index < original_count; index += 1 {
_ = canonical_type(module, types.DYNAMIC_START+types.Type(index), mapping, visiting)
}
+34
View File
@@ -2532,6 +2532,7 @@ parse_record_body :: proc(
allow_anonymous_struct_payload := false,
allow_keyword_names := false,
tuple_result: ^bool = nil,
defaults: ^[dynamic]ast.Expr_Id = nil,
) -> bool {
if _, ok := allow(parser, .Left_Brace); !ok {
source.add(parser.diagnostics, current(parser).span, expected_open)
@@ -2557,6 +2558,9 @@ parse_record_body :: proc(
mode = 2
field_type := parse_type(parser)
append(fields, types.Field{name=0, type=field_type})
if defaults != nil {
append(defaults, ast.INVALID_EXPR)
}
} else {
if mode == 2 {
source.add(parser.diagnostics, current(parser).span, "struct fields cannot mix named and unnamed forms")
@@ -2575,6 +2579,14 @@ parse_record_body :: proc(
}
field_type := parse_record_field_type(parser, allow_anonymous_struct_payload)
append(fields, types.Field{name=u32(field_name.symbol), type=field_type})
if defaults != nil {
value := ast.INVALID_EXPR
if _, ok := allow(parser, .Equal); ok {
skip_newlines(parser)
value = parse_expression(parser)
}
append(defaults, value)
}
}
if _, ok := allow(parser, .Comma); ok {
skip_newlines(parser)
@@ -2678,6 +2690,9 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout, file_hidden:
fields: [dynamic]types.Field
fields.allocator = parser.module.allocator
defer delete(fields)
defaults: [dynamic]ast.Expr_Id
defaults.allocator = parser.module.allocator
defer delete(defaults)
allow_anonymous_struct_payload := is_union && (inferred_tag || types.is_valid(declared_tag))
tuple := false
_ = parse_record_body(
@@ -2687,7 +2702,13 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout, file_hidden:
allow_anonymous_struct_payload,
allow_anonymous_struct_payload,
&tuple,
&defaults,
)
for value in defaults {
if value != ast.INVALID_EXPR && (c_layout || is_union || tuple) {
source.add(parser.diagnostics, parser.module.exprs[value].span, "field defaults are only supported by named native structs")
}
}
if tuple && (c_layout || is_union) {
source.add(parser.diagnostics, start.span, "unnamed fields are only supported by native structs")
tuple = false
@@ -2697,6 +2718,19 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout, file_hidden:
}
if !types.define_record(&parser.module.type_store, id, fields[:], c_layout, false, is_union, tag=tag, declared_tag=declared_tag, tuple=tuple) {
source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name))
} else if !c_layout && !is_union && !tuple {
for value, index in defaults {
if value == ast.INVALID_EXPR {
continue
}
append(&parser.module.struct_field_defaults, ast.Struct_Field_Default{
record=id,
field=symbol.Id(fields[index].name),
expr=value,
pkg=parser.pkg,
file=parser.file,
})
}
}
_ = finish_statement(parser)
}