diff --git a/LANGUAGE.md b/LANGUAGE.md index a9dbd2c..b068053 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -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` diff --git a/TODO.md b/TODO.md index 4affae9..d9261df 100644 --- a/TODO.md +++ b/TODO.md @@ -731,8 +731,8 @@ - enabled `&` (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 diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index ba87f2f..d96e914 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -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) } diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 95fa45a..609e859 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -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) diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index fa6b480..8f68f26 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -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)) diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index 81a8508..7eaea74 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -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) } diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 9742501..2d30a09 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -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) } diff --git a/compiler_tests.odin b/compiler_tests.odin index eb81a0c..b3429d0 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -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) +} diff --git a/main.odin b/main.odin index 024c86c..53aa202 100644 --- a/main.odin +++ b/main.odin @@ -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) } diff --git a/std/build/build.bro b/std/build/build.bro index fad3c9e..cf2f884 100644 --- a/std/build/build.bro +++ b/std/build/build.bro @@ -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) }