diff --git a/README.md b/README.md index c074792..33c14a8 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,8 @@ ordinary procedures may compute integer, boolean, float and string compile-time type `==` and `!=` comparisons in compile-time expressions and specializations preserve aliases, nominal declaration identity and structural pointer/array/callback signatures. composed error-set identity and specialization caching ignore member ordering, grouping and repetition. for example, `read proc($T type, value T) i16 { if (T == i16) return value else return value.count }` checks the scalar or record path for each concrete call without requiring both operations on every `T`. +`field!(base, name)` selects a member using a compile-time string, without runtime reflection metadata. names may come from constants, constant-input procedures or erased generic parameters; only the name is evaluated at compile time, while the base retains ordinary selection semantics. records, tuples (canonical decimal names such as `"0"`), enum/error-set members, tagged payloads, callbacks and sequence `"len"`/`"ptr"` members reuse exact typing, single base evaluation, address-taking and writable-place checks. type-producing bodies also snapshot selector names for nested aggregate updates, preserving earlier by-value copies. unknown members, runtime/nonstring names and invalid calls are diagnosed; tagged payload reads still check the active field. `typeinfo!`, `struct_type!` and inline expansion remain open. + callback signatures use `@proc(value i16) i16`, including grouped parameter names and trailing commas. compatible named functions can initialize callbacks, which may be passed, returned, replaced and stored in records/arrays; `Callback :: alias @proc(value i16) i16` names a reusable signature. parameter/result types match structurally, retaining nominal record identity and exact pointer mutability. callback fields call directly, such as `allocator.vtable.alloc(context, size, alignment)` once the remaining argument types are supported. `@mut proc` and `*proc` are invalid; `@mut Callback` instead points to writable callback storage. `true` and `false` are boolean literals; `bool` is available for parameters, storage and results. `==`, `!=`, `<`, `<=`, `>` and `>=` compare compatible numeric operands and produce `bool`; `==` and `!=` also accept boolean operands. comparisons preserve contextual numeric inference without making their operands boolean. arithmetic binds tighter than relational comparisons, which bind tighter than equality; comparisons associate left to right. boolean arithmetic/ordering and string/function/void comparisons are invalid; there is no implicit numeric-to-boolean conversion. diff --git a/TODO.md b/TODO.md index 1677207..fd6f92a 100644 --- a/TODO.md +++ b/TODO.md @@ -36,7 +36,9 @@ initialized type-valued fixed arrays support nested copies, indexed/whole update checked projections and by-value collection captures in type-producing bodies. type-array parameters and results cross compile-time procedure boundaries with owned snapshots, cached nominal identity and erased arguments. -general type-valued execution, reflection and generic library acceptance remain open. +`field!` selects record/tuple/enum and sequence members using compile-time names, +including runtime places and type-control updates without runtime reflection. +general type-valued execution, metadata reflection and generic library acceptance remain open. the driver retains frontend storage and source provenance through `@source/compilation`, with compilation-wide AST node identities. file commands retain single-file behavior; directory commands compile the @@ -89,7 +91,8 @@ test discovery and FFI remain outside these completed slices. - [x] collection-iteration checkpoint: arrays, slices, strings and single pointers-to-arrays support immutable exact-type copy captures, storage-qualified pointer captures and optional `usize` indices through interpreter/native execution. iterables/bounds evaluate once, array lvalues remain borrowed and later element updates are visible; temporary arrays, aggregate copy isolation, zero-cell/empty collections, nested loops and cleanup across transfers retain ordinary semantics. ordinary constant-input procedures reuse the same checked IR; type-producing bodies iterate materializable arrays/strings with by-value captures, fixed entry-time place indices, generated storage, nominal cache identity, dependency diagnostics, shared execution bounds and repeatable compilation. storage-bearing type-control captures retain their existing boundary; type-valued elements are covered below. - [x] type-array storage checkpoint: type-producing bodies own initialized fixed arrays of type values, including explicit/inferred nested arrays, `.len`, checked `usize` indexing, whole/indexed updates and immutable by-value collection captures. persistent snapshots preserve copies and exact shape/mutability while borrowed array places observe later updates. selected types generate nested storage, constructors, signatures and defaults through interpreter/native execution, retaining selected-body and index/type/mutability diagnostics, imported provenance, nominal cache identity, shared bounds and repeatable compilation. uninitialized arrays, type-valued record storage and reflection remain open. - [x] type-array procedure checkpoint: fixed arrays of type values cross compile-time procedure inputs/results, including nested/aliased results, dependent `$N` lengths, empty shapes and erased `$types` parameters in runtime generic procedures. all inputs to type/type-array producers are explicit constants; owned snapshots preserve by-value isolation, exact shape/mutability and canonical/nominal cache identity. projections generate signatures, constructors, storage and defaults through interpreter/native execution. malformed inputs/results, runtime dependencies, imported provenance, shared execution bounds and repeatable compilation retain diagnostics and isolation. runtime type-array storage, uninitialized arrays, type-valued record storage and reflection remain open. - - [ ] remaining gate: general type-valued compile-time execution, inline match/loop expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), broader constant materialization (including untagged unions and storage-bearing values), plus existing-library acceptance. required expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates. + - [x] field-selection reflection checkpoint: `field!(base, name)` evaluates/erases a constant string name and reuses ordinary selection through interpreter/native execution. records/tuples, enum/error-set members, tagged payloads, callbacks, pointer-qualified places and sequence `.len`/`.ptr` retain exact types, single base evaluation, address-taking, mutability and active-field checks. type-producing bodies snapshot changing selector names for nested aggregate updates without losing by-value isolation. runtime/nonstring names, malformed calls, missing members and noncanonical/overflowing tuple ordinals retain diagnostics; full metadata reflection and inline expansion remain open. + - [ ] remaining gate: general type-valued compile-time execution, inline match/loop expansion and metadata reflection (`typeinfo!`, `struct_type!`, generated type/default arrays), broader constant materialization (including untagged unions and storage-bearing values), plus existing-library acceptance. required expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates. - [ ] remaining source constructs: expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. - [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library. - [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support. diff --git a/source/ast/ast.hon b/source/ast/ast.hon index 90040c7..35bdd64 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -514,6 +514,21 @@ add_identifier proc( }, span) } +#! copies selector spelling into AST-owned storage, independently of its diagnostic span. +add_generated_identifier proc( + builder @mut Builder, + at Span, + interned_name strpool.StringId, + text []u8, +) NodeId ! mem.AllocError { + id :: try add_generated_literal(builder, at, .string, text) + node :: &builder.storage.nodes.items[usize(id)] + node.kind = .expr_identifier + node.data1 = node.main + node.main = NodeData{ name_id = interned_name } + return id +} + add_unary proc( builder @mut Builder, span Span, @@ -2141,10 +2156,12 @@ add_owned_literal proc( return id } -#! borrows a literal's spelling, independently of its diagnostic source span. +#! borrows literal or selector spelling, independently of its diagnostic source span. #! query float_value first for generated IEEE literals. literal_text proc(tree @Tree, id NodeId) []u8 { node :: get_node(&tree.storage, id) + if (node.kind == .expr_identifier and node.data0.extra == 1) + return tree.storage.generated.items[usize(node.data1.extra)] if (node.data0.extra == 1) return tree.storage.generated.items[usize(node.main.extra)] return source_text(tree, get_span(&tree.storage, id)) } diff --git a/source/compilation/compilation.hon b/source/compilation/compilation.hon index 6e17a7f..66ea136 100644 --- a/source/compilation/compilation.hon +++ b/source/compilation/compilation.hon @@ -398,7 +398,14 @@ compile proc( } resolution := try sema.resolve_packages(compilation.allocator, &tree, roots.items, imports.items, &compilation.strings, diagnostic_store) defer sema.deinit(&resolution) - elaborated := try specialize.elaborate(compilation.allocator, &compilation.builder, compilation.module_root?, &resolution, diagnostic_store) + elaborated := try specialize.elaborate( + compilation.allocator, + &compilation.builder, + compilation.module_root?, + &resolution, + &compilation.strings, + diagnostic_store, + ) defer specialize.deinit(&elaborated) tree = ast.view(&compilation.builder, compilation.module_root?) checked := try typecheck.check_elaborated(compilation.allocator, &tree, &resolution, elaborated.constraints.items, diagnostic_store) diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index 114af25..5d60338 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -138,8 +138,8 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { ` config struct { counts [2]mut usize, bias i16 } := { counts = [0, 0], bias = 0 } ` original :: config ` for (usize(0)..2) |index| { - ` config.counts[index] = index + 1 - ` config.bias = config.bias + 21 + ` field!(config, "counts")[index] = index + 1 + ` field!(config, "bias") = field!(config, "bias") + 21 ` } ` return struct { ` values [config.counts[1]]i16 @@ -232,7 +232,7 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { ` if (same_type_array.values[0] + same_type_array.values[1] + same_type_array.bias != 42) return 0 ` boundary BoundaryType(TypeRows()) :: { values = [19, 21] } ` same_boundary BoundaryType(TypeRows()) :: boundary - ` if (same_boundary.values[0] + same_boundary.values[1] + same_boundary.bias != 42) return 0 + ` if (same_boundary.values[0] + same_boundary.values[1] + field!(same_boundary, "bias") != 42) return 0 ` if (copy_type_rows(TypeRows())[0][0] != u8 or copy_type_rows(TypeRows())[1][0] != i16) return 0 ` if (TypeRows()[0][0] != i16) return 0 ` if (read_type_rows(TypeRows(), 40) != 42 or read_type_rows(TypeRows(), 40) != 42) return 0 diff --git a/source/lower/lower.hon b/source/lower/lower.hon index 0047a06..d9d608d 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -1972,9 +1972,10 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr and sema.binding(state.resolution, id).kind == .none ) { selection :: ast.selection_expr(tree, id) + if (trap_type(state, id)) return ir.no_value base_type :: typecheck.type_of(state.checked, selection.base) if (base_type.kind == .slice or base_type.kind == .string or base_type.kind == .array) { - member :: ast.source_text(tree, ast.node_span(tree, selection.member)) + member :: ast.literal_text(tree, selection.member) if (mem.eql(member, "len") or mem.eql(member, "ptr")) { parts :: try slice_parts(state, selection.base) if (state.terminated or trap_type(state, id)) return ir.no_value @@ -1986,7 +1987,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr if (!descriptor.many and descriptor.element.kind == .array) { pointer :: try lower_expression(state, selection.base) if (state.terminated or trap_type(state, id)) return ir.no_value - member :: ast.source_text(tree, ast.node_span(tree, selection.member)) + member :: ast.literal_text(tree, selection.member) array :: typecheck.array_type(state.checked, descriptor.element.array_type) if (mem.eql(member, "len")) return try constant(state, ir.Value{ type = .usize, data = ir.ValueData{ unsigned = u64(array.len) }, diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 26f6eb4..f3c4e40 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -2830,7 +2830,7 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All } base := try infer_shape(state, view.base, defaults) if ((base.kind == .array or base.kind == .slice or base.kind == .string) and base.nominal == ast.NodeId(maxval!(u32))) { - member :: ast.source_text(state.tree, ast.node_span(state.tree, view.member)) + member :: ast.literal_text(state.tree, view.member) if (mem.eql(member, "len")) { seed(state, id, Type{ kind = .usize }) return group_type(state, id) @@ -2852,7 +2852,7 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All !pointer.many and pointer.element.kind == .array and ast.expr_kind(state.tree, view.member) == .identifier ) { - member :: ast.source_text(state.tree, ast.node_span(state.tree, view.member)) + member :: ast.literal_text(state.tree, view.member) if (mem.eql(member, "len")) { type :: Type{ kind = .usize } seed(state, id, type) @@ -3259,7 +3259,7 @@ named_field proc(tree @ast.Tree, fields []RecordField, name ast.NodeId) usize { selected_field proc(state @State, fields []RecordField, member ast.NodeId) usize { if (ast.expr_kind(state.tree, member) == .identifier) return named_field(state.tree, fields, member) value := usize(0) - for ast.source_text(state.tree, ast.node_span(state.tree, member)) |byte| { + for ast.literal_text(state.tree, member) |byte| { if (byte < '0' or byte > '9') return no_index digit :: usize(byte - '0') if (value > divtrunc!(maxval!(usize) - digit, usize(10))) return no_index diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 89f4656..07ef3e0 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -15,7 +15,7 @@ typecheck :: import "@source/sema/check" @hide:file no_index :: maxval!(usize) @hide:file -Error :: alias mem.AllocError | diagnostics.CapacityError +Error :: alias mem.AllocError | diagnostics.CapacityError | strpool.InternError @hide:file ValueKind :: enum { @@ -82,6 +82,7 @@ State :: struct { allocator mem.Allocator builder @mut ast.Builder tree ast.Tree + strings @mut strpool.StringPool bindings arraylist.ArrayList(sema.Binding) declarations arraylist.ArrayList(ast.NodeId) instances arraylist.ArrayList(Instance) @@ -108,6 +109,7 @@ elaborate proc( builder @mut ast.Builder, root ast.NodeId, resolution @mut sema.Resolution, + strings @mut strpool.StringPool, diagnostic_store @mut diagnostics.Store, ) Result ! Error { result := Result{ builder = builder, changes = arraylist.init(allocator), constraints = arraylist.init(allocator) } @@ -119,6 +121,10 @@ elaborate proc( id :: ast.NodeId(i) class :: ast.node_class(&original, id) if (class == .expression and ast.expr_kind(&original, id) == .comptime_expr) needed = true + if (class == .expression and ast.expr_kind(&original, id) == .builtin_call) { + name :: ast.builtin_call(&original, id).name + if (mem.eql(ast.source_text(&original, ast.node_span(&original, name)), "field")) needed = true + } if (class == .function and ast.function_generic(&original, id)) needed = true if (class == .function) { result_type :: sema.binding(resolution, ast.function_decl(&original, id).return_type) @@ -134,7 +140,7 @@ elaborate proc( mapping :: try mem.alloc(ast.NodeId, allocator, ast.node_count(&original)) defer mem.free(allocator, mapping) state := State{ - allocator = allocator, builder = builder, tree = ast.view(builder, root), + allocator = allocator, builder = builder, tree = ast.view(builder, root), strings = strings, bindings = arraylist.init(allocator), declarations = arraylist.init(allocator), instances = arraylist.init(allocator), cache = hashmap.init(allocator), expanded = arraylist.init(allocator), diagnostics = diagnostic_store, @@ -269,6 +275,13 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { try refresh(state) return } + if (class == .expression and ast.expr_kind(&state.tree, id) == .builtin_call) { + name :: ast.builtin_call(&state.tree, id).name + if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "field")) { + try expand_field(state, id) + return + } + } if (class == .expression and ast.expr_kind(&state.tree, id) == .call) { callee :: ast.call_expr(&state.tree, id).callee try expand(state, callee) @@ -318,6 +331,66 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { } } +# erase only the name: ordinary selection checks the base and retains its place, +# evaluation order, mutability and active-union checks through both backends. +@hide:file +expand_field proc(state @mut State, id ast.NodeId) void ! Error { + if (ast.call_arg_count(&state.tree, id) != 2) { + try reject(state, id, .argument_count_mismatch) + return + } + base :: ast.call_arg(&state.tree, id, 0) + argument :: ast.call_arg(&state.tree, id, 1) + state.evaluation_diagnostic = diagnostics.no_diagnostic + value :: try evaluate(state, argument) + if (value.kind != .string) { + if (state.evaluation_diagnostic == diagnostics.no_diagnostic) + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .comptime_argument_required, ast.node_span(&state.tree, argument)) + try reject_evaluation(state, id) + return + } + try arraylist.append(&state.rewrites.constraints, argument) + text :: ast.literal_text(&state.tree, value.node) + bytes := arraylist.init(u8, state.allocator) + defer arraylist.deinit(&bytes) + cursor := usize(1) + while (cursor < text.len - 1) { + byte :: ast.string_next(text, &cursor) catch { + try reject(state, id, .invalid_escape) + return + } + try arraylist.append(&bytes, byte) + } + # only canonical decimal names denote tuple ordinals; "01" is a name. + numeric := bytes.items.len > 0 and (bytes.items.len == 1 or bytes.items[0] != '0') + ordinal := u64(0) + for (bytes.items) |byte| { + if (byte < '0' or byte > '9') { + numeric = false + break + } + digit :: u64(byte - '0') + if (ordinal > divtrunc!(maxval!(u64) - digit, u64(10))) { + numeric = false + break + } + ordinal = ordinal * 10 + digit + } + span :: ast.node_span(&state.tree, argument) + member := no_node + if (numeric) member = try ast.add_integer(state.builder, span, ordinal) + else { + interned :: try strpool.intern(state.strings, bytes.items) + member = try ast.add_generated_identifier(state.builder, span, interned, bytes.items) + } + selection :: try ast.add_selection(state.builder, ast.node_span(&state.tree, id), base, member) + try replace(state, id, selection) + try refresh(state) + state.expanded.items[usize(id)] = 0 + try expand(state, id) +} + # runtime bodies share the type-producer selector, but retain ordinary runtime # parameters and control transfers in the selected arm. @hide:file @@ -1413,7 +1486,7 @@ type_array_projection proc(state @mut State, id ast.NodeId) bool ! Error { selected = ast.array_element(&state.tree, array.value, index) } else { member :: ast.selection_expr(&state.tree, id).member - if (!mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, member)), "len")) { + if (!mem.eql(ast.literal_text(&state.tree, member), "len")) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .unknown_field, ast.node_span(&state.tree, member)) return false @@ -1717,6 +1790,41 @@ execute_type_block proc(state @mut State, execution @mut TypeExecution, block as return .next } +# substitute selector names without snapshotting away the assignment's root place. +@hide:file +type_assignment_target proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error { + kind :: ast.expr_kind(&state.tree, id) + if (kind == .builtin_call) { + name :: ast.builtin_call(&state.tree, id).name + if (!mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "field")) return id + if (ast.call_arg_count(&state.tree, id) != 2) { + try reject(state, id, .argument_count_mismatch) + return no_node + } + base :: try type_assignment_target(state, execution, ast.call_arg(&state.tree, id, 0)) + if (base == no_node) return no_node + argument :: try type_snapshot(state, execution, ast.call_arg(&state.tree, id, 1)) + arguments [2]ast.NodeId :: [base, argument] + result :: try ast.add_builtin_call(state.builder, ast.node_span(&state.tree, id), name, arguments[..]) + try refresh(state) + try expand(state, result) + if (binding(state, result).kind == .invalid) return no_node + return result + } + if (kind != .selection and kind != .index) return id + base :: if (kind == .selection) ast.selection_expr(&state.tree, id).base else ast.index_expr(&state.tree, id).base + selected :: try type_assignment_target(state, execution, base) + if (selected == no_node or selected == base) return if (selected == no_node) no_node else id + result := no_node + if (kind == .selection) + result = try ast.add_selection(state.builder, ast.node_span(&state.tree, id), + selected, ast.selection_expr(&state.tree, id).member) + else result = try ast.add_index(state.builder, ast.node_span(&state.tree, id), + selected, ast.index_expr(&state.tree, id).index) + try refresh(state) + return result +} + @hide:file execute_type_statement proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error { match ast.stmt_kind(&state.tree, statement) { @@ -1756,6 +1864,18 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat } .assignment_stmt: { assignment :: ast.assignment_stmt(&state.tree, statement) + target :: try type_assignment_target(state, execution, assignment.target) + if (target == no_node) return .invalid + if (target != assignment.target) { + normalized :: try ast.add_assignment_stmt(state.builder, + ast.node_span(&state.tree, statement), target, assignment.value) + try refresh(state) + # synthesized places still need local substitution in snapshots. + previous :: execution.mapping.len + execution.mapping = try mem.realloc(state.allocator, execution.mapping, ast.node_count(&state.tree)) + mem.set(execution.mapping[previous..], no_node) + return try execute_type_statement(state, execution, normalized) + } root := assignment.target while (true) { kind :: ast.expr_kind(&state.tree, root) @@ -2564,7 +2684,7 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! if (base == no_node) return no_node if (ast.expr_kind(&state.tree, base) == .pointer_type) base = try canonical_type(state, ast.pointer_type(&state.tree, base).element) if (base == no_node) return no_node - member :: ast.source_text(&state.tree, ast.node_span(&state.tree, view.member)) + member :: ast.literal_text(&state.tree, view.member) base_kind :: ast.expr_kind(&state.tree, base) if ( mem.eql(member, "len") and ( diff --git a/tests/native.py b/tests/native.py index 4f1a7da..175f392 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,142 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "field_reflection_runtime_places_callbacks_and_single_evaluation": ( + """ + Point :: struct { value i16, callback @proc(value i16) i16 } + add proc(value i16) i16 { return value + 2 } + place proc(point @mut Point, calls @mut i16) @mut Point { + calls^ = calls^ + 1 + return point + } + name proc() string { return "val\\x75e" } + main proc() [3]i16 { + point Point := { value = 19, callback = add } + calls i16 := 0 + field!(place(&point, &calls), name()) = 40 + pointer @mut i16 :: &field!(point, "value") + pointer^ = pointer^ + 1 + return [field!(point, "callback")(field!(point, "value")), calls, point.value] + } + """, b"[43, 1, 41]\n", None, + ), + "field_reflection_tuple_enum_union_and_sequence_members": ( + """ + A :: enum { first, second } + B :: enum { third } + E :: alias A | B + Choice :: union(enum) { payload i16, empty void } + read proc($name string, value struct { count i16 }) i16 { + return field!(value, name) + } + main proc() [4]i16 { + tuple :: {i16(19), i16(21)} + choice Choice :: { payload = 2 } + values [2]i16 :: [19, 21] + member E :: field!(E, "third") + if (member != E.third) return [0, 0, 0, 0] + return [ + field!(tuple, "0") + field!(tuple, "1") + field!(choice, "payload"), + i16(field!(values, "len")), + field!(&values, "ptr")[1], + read("count", { count = 42 }), + ] + } + """, b"[42, 2, 21, 42]\n", None, + ), + "field_reflection_type_control_and_constant_materialization": ( + """ + Config :: struct { count usize, value i16 } + config proc() Config { return { count = 2, value = 21 } } + Storage proc(name string) type { + settings :: config() + types :: [i16, bool] + return struct { + values [field!(settings, name)]types[0] + bias i16 = i16(field!(types, "len")) + } + } + main proc() i16 { + value Storage("count") :: { values = [19, 21] } + constant :: $(field!(config(), "value")) + return value.values[0] + constant + value.bias + } + """, b"42\n", None, + ), + "field_reflection_type_control_updates_names_and_nested_places": ( + """ + Selected proc() type { + config struct { left [1]mut usize, right [1]mut usize } := { left = [0], right = [0] } + original :: config + for ["left", "right"] |name| { field!(config, name)[0] = 2 } + if (original.left[0] != 0 or original.right[0] != 0) return bool + return [config.left[0] + config.right[0]]i16 + } + main proc() i16 { + values Selected() :: [10, 10, 10, 12] + return values[0] + values[1] + values[2] + values[3] + } + """, b"42\n", None, + ), + "field_reflection_preserves_readonly_pointer_assignment": ( + """ + Point :: struct { value i16 } + update proc(point @Point) void { field!(point, "value") = 0 } + main proc() i16 { + point Point :: { value = 42 } + update(&point) + return point.value + } + """, b"", b"mutable pointee", + ), + "field_reflection_rejects_nonstring_names": ( + """ + main proc() i16 { return field!({ value = i16(42) }, 0) } + """, b"", b"compile-time value", + ), + "field_reflection_rejects_unknown_enum_members": ( + """ + E :: enum { first } + main proc() E { return field!(E, "missing") } + """, b"", b"no such field", + ), + "field_reflection_rejects_overflowing_tuple_ordinals": ( + """ + main proc() i16 { return field!({i16(42)}, "18446744073709551616") } + """, b"", b"no such field", + ), + "field_reflection_rejects_runtime_names": ( + """ + read proc(name string, value struct { count i16 }) i16 { + return field!(value, name) + } + main proc() i16 { return read("count", { count = 42 }) } + """, b"", b"compile-time value", + ), + "field_reflection_rejects_unknown_fields": ( + """ + main proc() i16 { return field!({ count = i16(42) }, "missing") } + """, b"", b"no such field", + ), + "field_reflection_rejects_noncanonical_tuple_names": ( + """ + main proc() i16 { return field!({i16(42)}, "00") } + """, b"", b"no such field", + ), + "field_reflection_rejects_wrong_arity": ( + """ + main proc() i16 { return field!(42) } + """, b"", b"argument count", + ), + "field_reflection_preserves_union_active_field_checks": ( + """ + Choice :: union(enum) { value i16, empty void } + main proc() i16 { + choice Choice :: .empty + return field!(choice, "value") + } + """, b"", b"not active", + ), "collection_iteration_borrows_array_and_reads_each_element": ( """ main proc() [2]i16 {