default struct fields
This commit is contained in:
@@ -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
|
||||
- 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)`
|
||||
- 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
|
||||
- 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`
|
||||
|
||||
@@ -731,8 +731,8 @@
|
||||
- enabled `&<array literal>` (Zig's `&.{...}`): the literal is promoted to an
|
||||
anonymous global whose address decays to a slice, so list fields like
|
||||
`libraries = &["raylib"]` work; empty lists are `&[]`
|
||||
- deferred: build graph / steps / caching, multiple artifacts, computed paths
|
||||
(needs string building), struct field defaults to drop `&[]` on empty lists
|
||||
- deferred: build graph / steps / caching, multiple artifacts, and computed paths
|
||||
(needs string building); milestone 44 later removed explicit `&[]` build-config fields
|
||||
|
||||
29. fix bugs (implemented)
|
||||
- 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
|
||||
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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -13325,3 +13325,44 @@ main func() void {
|
||||
testing.expect(t, unresolved_ok)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ default_build_bro :: proc(project_name: string, allocator := context.allocator)
|
||||
defer strings.builder_destroy(&builder)
|
||||
strings.write_string(&builder, "b :: import \"@std/build\"\n\nconfig :: b.BuildConfig{\n\tname = \"")
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -5,13 +5,13 @@
|
||||
# build.bro, reads the config, and writes root/build/name.
|
||||
#
|
||||
# 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 {
|
||||
name []u8 # output executable name under root/build
|
||||
source []u8 # program package directory, relative to build.bro
|
||||
libraries [][]u8 # library names to link (-l)
|
||||
lib_paths [][]u8 # library search directories (-L)
|
||||
includes [][]u8 # C include directories (-I)
|
||||
defines [][]u8 # C preprocessor defines (name or name=value)
|
||||
links [][]u8 # extra linker inputs (object/source files, -framework pairs)
|
||||
libraries [][]u8 = &[] # library names to link (-l)
|
||||
lib_paths [][]u8 = &[] # library search directories (-L)
|
||||
includes [][]u8 = &[] # C include directories (-I)
|
||||
defines [][]u8 = &[] # C preprocessor defines (name or name=value)
|
||||
links [][]u8 = &[] # extra linker inputs (object/source files, -framework pairs)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user