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
+1
View File
@@ -39,6 +39,7 @@ roadmap and milestone history.
- optionals with `none`, `orelse`, postfix `?`, conditional unwraps, guarded unwraps, and left-to-right short-circuiting multi-unwraps - optionals with `none`, `orelse`, postfix `?`, conditional unwraps, guarded unwraps, and left-to-right short-circuiting multi-unwraps
- nominal distinct types with exact backing construction, native enums with optional explicit integer backing and explicit backing-to-scalar casts, contextual enum literals, and imported C enums as target-backed integer aliases - nominal distinct types with exact backing construction, native enums with optional explicit integer backing and explicit backing-to-scalar casts, contextual enum literals, and imported C enums as target-backed integer aliases
- source-order native structs, opaque nominal records with `Name :: opaque`, complete `c_struct { ... }`, keyed record literals, native untagged unions, and native tagged unions `union(Enum)` / `union(enum)` - source-order native structs, opaque nominal records with `Name :: opaque`, complete `c_struct { ... }`, keyed record literals, native untagged unions, and native tagged unions `union(Enum)` / `union(enum)`
- named native struct fields may declare defaults with `field T = expression`; keyed literals use defaults for omitted fields and explicit initializers override them
- void-payload tagged-union variants, anonymous struct payloads, contextual `.variant`, `.variant{payload}`, and `.variant{field = value}` construction - void-payload tagged-union variants, anonymous struct payloads, contextual `.variant`, `.variant{payload}`, and `.variant{field = value}` construction
- 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; `void ! E` functions complete successfully on fallthrough, and void-success `catch` handlers may fall through without `yield` - fallible channel types `T ! E`, where `E` is a native enum/tagged union or supported sum composition; `void ! E` functions complete successfully on fallthrough, and void-success `catch` handlers may fall through without `yield`
+10 -3
View File
@@ -731,8 +731,8 @@
- enabled `&<array literal>` (Zig's `&.{...}`): the literal is promoted to an - enabled `&<array literal>` (Zig's `&.{...}`): the literal is promoted to an
anonymous global whose address decays to a slice, so list fields like anonymous global whose address decays to a slice, so list fields like
`libraries = &["raylib"]` work; empty lists are `&[]` `libraries = &["raylib"]` work; empty lists are `&[]`
- deferred: build graph / steps / caching, multiple artifacts, computed paths - deferred: build graph / steps / caching, multiple artifacts, and computed paths
(needs string building), struct field defaults to drop `&[]` on empty lists (needs string building); milestone 44 later removed explicit `&[]` build-config fields
29. fix bugs (implemented) 29. fix bugs (implemented)
- bare `return` is the empty return for void functions; `yield` always requires a - bare `return` is the empty return for void functions; `yield` always requires a
@@ -925,7 +925,14 @@
- runtime pointer contexts implicitly materialize a bare identity; pointer-to-identity conversion is - runtime pointer contexts implicitly materialize a bare identity; pointer-to-identity conversion is
rejected, and bare-containing aggregates cannot enter runtime storage or ABI/C layouts rejected, and bare-containing aggregates cannot enter runtime storage or ABI/C layouts
44. struct field defaults on declaration 44. struct field defaults on declaration (implemented)
- named native structs accept `field T = expression`; keyed construction evaluates defaults
for omitted fields while explicit initializers override them
- defaults resolve names in the declaration file, participate in record constraint inference,
and work during runtime and comptime construction
- `@std/build.BuildConfig` uses defaults for optional list fields
- fields without defaults remain required; C-layout records, unions, tuples, and anonymous
generated structs do not accept defaults
## A word on unchecked casts ## A word on unchecked casts
+11
View File
@@ -304,6 +304,14 @@ Declaration_Alias :: struct {
diagnostic: source.Diagnostic_Id, 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 { File :: struct {
source: source.Source_Id, source: source.Source_Id,
pkg: Package_Id, pkg: Package_Id,
@@ -357,6 +365,7 @@ Module :: struct {
c_trampolines: [dynamic]Trampoline, c_trampolines: [dynamic]Trampoline,
strings: [dynamic]string, strings: [dynamic]string,
type_fields: [dynamic]types.Field, type_fields: [dynamic]types.Field,
struct_field_defaults: [dynamic]Struct_Field_Default,
type_uses: [dynamic]Type_Use, type_uses: [dynamic]Type_Use,
type_store: types.Store, type_store: types.Store,
allocator: mem.Allocator, allocator: mem.Allocator,
@@ -379,6 +388,7 @@ init_module :: proc(allocator := context.allocator) -> Module {
module.c_trampolines.allocator = allocator module.c_trampolines.allocator = allocator
module.strings.allocator = allocator module.strings.allocator = allocator
module.type_fields.allocator = allocator module.type_fields.allocator = allocator
module.struct_field_defaults.allocator = allocator
module.type_uses.allocator = allocator module.type_uses.allocator = allocator
return module return module
} }
@@ -436,6 +446,7 @@ destroy_module :: proc(module: ^Module) {
delete(module.c_trampolines) delete(module.c_trampolines)
delete(module.strings) delete(module.strings)
delete(module.type_fields) delete(module.type_fields)
delete(module.struct_field_defaults)
delete(module.type_uses) delete(module.type_uses)
types.destroy_store(&module.type_store) types.destroy_store(&module.type_store)
} }
+67
View File
@@ -4471,12 +4471,19 @@ infer_compound_expr :: proc(
} }
return value 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 { for keyed in expr.args {
keyed_expr := checker.ast_module.exprs[keyed] keyed_expr := checker.ast_module.exprs[keyed]
if keyed_expr.left == ast.INVALID_EXPR { if keyed_expr.left == ast.INVALID_EXPR {
continue continue
} }
slot, field, ok := find_struct_field_slot(checker, value, keyed_expr.name) 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) { if !ok || !is_inferred_record_field(checker, slot) {
_ = infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types) _ = infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types)
continue 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 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)
@@ -6098,6 +6118,27 @@ infer_all :: proc(checker: ^Checker) {
checker.record_field_demands_dirty = false checker.record_field_demands_dirty = false
spec_count := len(checker.specs) 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 // Backward demands: a global pushes its own (declared or already-resolved) type
// onto open numeric slots reachable through names and numeric arithmetic. // onto open numeric slots reachable through names and numeric arithmetic.
for global, index in checker.ast_module.globals { 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 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) { 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) item, ok := types.node(&checker.module.types, tuple_type)
if !ok || item.kind != .Struct || !item.tuple || index >= u64(item.field_count) { 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 { if values[index] != hir.INVALID_EXPR {
continue 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))) id := source.addf(checker.diagnostics, expr.span, "missing initializer for struct field '%s'", symbol_text(checker, symbol.Id(field.name)))
delete(values, checker.allocator) delete(values, checker.allocator)
return invalid_hir_expr(checker, expr.span, id, struct_type) 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 { if !union_record {
for field, index in fields { 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))) 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)) 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 { for &field in module.type_fields {
field.type = canonical_type(module, field.type, mapping, visiting) 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 { for index := 0; index < original_count; index += 1 {
_ = canonical_type(module, types.DYNAMIC_START+types.Type(index), mapping, visiting) _ = 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_anonymous_struct_payload := false,
allow_keyword_names := false, allow_keyword_names := false,
tuple_result: ^bool = nil, tuple_result: ^bool = nil,
defaults: ^[dynamic]ast.Expr_Id = nil,
) -> bool { ) -> bool {
if _, ok := allow(parser, .Left_Brace); !ok { if _, ok := allow(parser, .Left_Brace); !ok {
source.add(parser.diagnostics, current(parser).span, expected_open) source.add(parser.diagnostics, current(parser).span, expected_open)
@@ -2557,6 +2558,9 @@ parse_record_body :: proc(
mode = 2 mode = 2
field_type := parse_type(parser) field_type := parse_type(parser)
append(fields, types.Field{name=0, type=field_type}) append(fields, types.Field{name=0, type=field_type})
if defaults != nil {
append(defaults, ast.INVALID_EXPR)
}
} else { } else {
if mode == 2 { if mode == 2 {
source.add(parser.diagnostics, current(parser).span, "struct fields cannot mix named and unnamed forms") 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) field_type := parse_record_field_type(parser, allow_anonymous_struct_payload)
append(fields, types.Field{name=u32(field_name.symbol), type=field_type}) 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 { if _, ok := allow(parser, .Comma); ok {
skip_newlines(parser) skip_newlines(parser)
@@ -2678,6 +2690,9 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout, file_hidden:
fields: [dynamic]types.Field fields: [dynamic]types.Field
fields.allocator = parser.module.allocator fields.allocator = parser.module.allocator
defer delete(fields) 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)) allow_anonymous_struct_payload := is_union && (inferred_tag || types.is_valid(declared_tag))
tuple := false tuple := false
_ = parse_record_body( _ = 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,
allow_anonymous_struct_payload, allow_anonymous_struct_payload,
&tuple, &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) { if tuple && (c_layout || is_union) {
source.add(parser.diagnostics, start.span, "unnamed fields are only supported by native structs") source.add(parser.diagnostics, start.span, "unnamed fields are only supported by native structs")
tuple = false 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) { 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)) 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) _ = finish_statement(parser)
} }
+41
View File
@@ -13325,3 +13325,44 @@ main func() void {
testing.expect(t, unresolved_ok) testing.expect(t, unresolved_ok)
testing.expect_value(t, unresolved, types.U64) testing.expect_value(t, unresolved, types.U64)
} }
@(test)
milestone_44_struct_field_defaults_compile_and_run :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-struct-field-defaults"
main_path := "/tmp/brolang-test-struct-field-defaults/main.bro"
output := "/tmp/brolang-test-struct-field-defaults-output"
text := `Config :: struct {
count int = 7
enabled bool = true
name []u8 = "bro"
required i32
}
Lists :: struct { items [][]u8 = &[] }
read func(value Config) i32 {
return value.required + value.count
}
ANSWER :: $read(Config{required = 35})
main func() i32 {
defaults Config = Config{required = 35}
overridden Config = Config{count = 1, enabled = false, name = "x", required = 40}
lists Lists = Lists{}
if (ANSWER != 42) return 1
if (defaults.count != 7 or defaults.enabled == false) return 2
if (defaults.name.len != 3 or lists.items.len != 0) return 3
if (overridden.count != 1 or overridden.enabled or overridden.name.len != 1) return 4
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)
}
+1 -1
View File
@@ -234,7 +234,7 @@ default_build_bro :: proc(project_name: string, allocator := context.allocator)
defer strings.builder_destroy(&builder) defer strings.builder_destroy(&builder)
strings.write_string(&builder, "b :: import \"@std/build\"\n\nconfig :: b.BuildConfig{\n\tname = \"") strings.write_string(&builder, "b :: import \"@std/build\"\n\nconfig :: b.BuildConfig{\n\tname = \"")
write_escaped_brolang_string(&builder, name) write_escaped_brolang_string(&builder, name)
strings.write_string(&builder, "\",\n\tsource = \"source\",\n\tlibraries = &[],\n\tlib_paths = &[],\n\tincludes = &[],\n\tdefines = &[],\n\tlinks = &[],\n}\n") strings.write_string(&builder, "\",\n\tsource = \"source\",\n}\n")
return strings.clone(strings.to_string(builder), allocator) return strings.clone(strings.to_string(builder), allocator)
} }
+6 -6
View File
@@ -5,13 +5,13 @@
# build.bro, reads the config, and writes root/build/name. # build.bro, reads the config, and writes root/build/name.
# #
# Declarative and literal-only: one executable per build. List fields take an # Declarative and literal-only: one executable per build. List fields take an
# address-of an array literal (`&["raylib"]`); empty lists are written `&[]`. # address-of an array literal (`&["raylib"]`) and default to empty.
BuildConfig :: struct { BuildConfig :: struct {
name []u8 # output executable name under root/build name []u8 # output executable name under root/build
source []u8 # program package directory, relative to build.bro source []u8 # program package directory, relative to build.bro
libraries [][]u8 # library names to link (-l) libraries [][]u8 = &[] # library names to link (-l)
lib_paths [][]u8 # library search directories (-L) lib_paths [][]u8 = &[] # library search directories (-L)
includes [][]u8 # C include directories (-I) includes [][]u8 = &[] # C include directories (-I)
defines [][]u8 # C preprocessor defines (name or name=value) defines [][]u8 = &[] # C preprocessor defines (name or name=value)
links [][]u8 # extra linker inputs (object/source files, -framework pairs) links [][]u8 = &[] # extra linker inputs (object/source files, -framework pairs)
} }