diff --git a/README.md b/README.md index 0f081ba..1117085 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,11 @@ 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. +`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!` and inline expansion remain open. + +`struct_type!(layout, names, types, defaults)` generates an ordinary record from constant fixed arrays or tuples of equal length. for example, `struct_type!(.auto, ["value", "bias"], [i16, i8], [null, 2])` requires a `value` initializer and supplies an `i8` default for `bias`. literal collections retain each field's context, including heterogeneous types and defaults; computed collections can come from helpers and loop-filled producer locals. names must be unique ASCII identifiers. each default is checked as `?Field`: absent means required, present supplies an owned field value. generated fields retain ordinary exact typing, aggregate copy isolation, field selection and checked updates through interpreter/native execution. producer specializations cache nominal identity; imported producers and repeated compilation retain source provenance and fresh owned values. incomplete arrays and runtime dependencies are rejected, and construction shares the type-control execution bound. + +layout accepts `.auto` or `.c`, including constant enum members such as `Layout.c`. C-layout records must be nonempty; fields may contain concrete scalars, data pointers, optional data pointers, explicitly backed enums, nonempty fixed arrays and nested C-layout records. distinct types and non-C representations are rejected. this does not add aggregate C calling conventions, storage-bearing constant materialization, metadata inspection or inline expansion. 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. diff --git a/TODO.md b/TODO.md index 771300c..17e5ba5 100644 --- a/TODO.md +++ b/TODO.md @@ -41,6 +41,8 @@ initialized type-valued records/tuples support schemas/defaults, nested field/index updates and owned copies/captures inside compile-time producers. `field!` selects record/tuple/enum and sequence members using compile-time names, including runtime places and type-control updates without runtime reflection. +`struct_type!` generates ordinary/C-layout records from constant name/type/default +arrays or tuples, including loop-filled producer storage and owned defaults. 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. @@ -99,7 +101,8 @@ test discovery and FFI remain outside these completed slices. - [x] uninitialized value-array checkpoint: ordinary fixed arrays in type/type-array producers start with `undefined`, fill nested indices or replace whole arrays/rows, and generate storage/defaults through interpreter/native execution. owned snapshots preserve independent partial initialization in local copies; initialized scalar/string/optional/record/tagged-union elements retain exact types and ordinary checked operations. premature leaf reads and incomplete procedure arguments diagnose read-before-initialization. empty shapes, `.len` before initialization, mutability/type/index/bounds/overflow diagnostics, per-cell construction limits and repeatable compilation are verified. runtime undefined storage, uninitialized arrays in ordinary value-returning procedures, partial record construction and storage-bearing constant materialization remain open. - [x] type-record storage checkpoint: initialized records/tuples containing type values live inside type/type-array producers, including local schemas/defaults, typed/contextual/inferred construction, nested records/arrays, checked field/index reads and updates, whole-value replacement and immutable by-value collection captures. persistent snapshots isolate copies/captures while borrowed array places observe later updates; type leaves retain nominal identity and ordinary value leaves reuse checked IR. generated storage/defaults, invalid field/type/default diagnostics, readonly places, narrow overflow, imported producers/provenance, cached nominal results and repeatable compilation work through interpreter/native execution. field validation/construction shares the type-control bound. type-record procedure inputs/results are covered below; runtime type-valued records, partial record initialization, metadata reflection and inline expansion remain open. - [x] type-record procedure checkpoint: records/tuples containing type values cross compile-time procedure inputs/results, including nested arrays/records, aliases, defaults and dependent annotations. compile-time producers accept ordinary constant inputs; runtime generics accept erased `$` metadata parameters. owned snapshots isolate argument/result copies, exact type/value leaves retain checked diagnostics, and normalized cache keys preserve nominal generated types. generated storage/defaults, imported producers/provenance, shared execution bounds and repeatable compilation work through interpreter/native execution. runtime metadata storage, partial record initialization, 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. + - [x] generated-record checkpoint: `struct_type!(layout, names, types, defaults)` builds ordinary records from constant fixed arrays or tuples with unique ASCII field names, concrete types and contextual optional defaults. literal heterogeneous collections, helper results and loop-filled producer arrays preserve exact types, owned defaults/copies and cached nominal identity through interpreter/native execution. `.auto` permits empty records; `.c` requires nonempty C-compatible fields and retains native field order/layout. malformed inputs, incomplete arrays, runtime dependencies, field/default validation, imported provenance, shared construction bounds and repeatable compilation retain diagnostics and isolation. metadata inspection, inline expansion, aggregate C calling conventions and storage-bearing defaults remain open. + - [ ] remaining gate: general type-valued compile-time execution, inline match/loop expansion and metadata reflection (`typeinfo!` and metadata collections), 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 b2310cf..0122ad8 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -747,6 +747,18 @@ add_record_type proc(builder @mut Builder, span Span, fields []NodeId) NodeId ! }, span) } +set_record_c_layout proc(builder @mut Builder, id NodeId) void { + node :: &builder.storage.nodes.items[usize(id)] + debug.assert(node.kind == .expr_record_type) + node.main = NodeData{ extra = 1 } +} + +record_c_layout proc(tree @Tree, id NodeId) bool { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_record_type) + return node.main.extra == 1 +} + add_enum_type proc(builder @mut Builder, span Span, backing ?NodeId, members []NodeId) NodeId ! mem.AllocError { start :: builder.storage.extra.items.len for (members) |member| _ = try add_extra(builder, u32(member)) diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index c1eb117..422e594 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -338,6 +338,57 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { } } +recompiles_generated_records_without_borrowing_previous_ir test { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + modules [2]mut ir.Module := undefined + { + session := init(mem.c_allocator) + defer deinit(&session) + root :: try add_package(&session) + bytes := try owned_text( + `Child proc(value i16) type { + ` return struct_type!(.auto, ["value"], [i16], [value]) + `} + `Parent proc(value i16) type { + ` ChildType :: Child(value) + ` child ChildType :: {} + ` return struct_type!( + ` .auto, + ` ["child", "bias", "text"], + ` [ChildType, i16, string], + ` [child, 2, "owned"], + ` ) + `} + `main proc() i16 { + ` parent Parent(40) :: {} + ` same Parent(40) :: parent + ` child Child(40) :: same.child + ` if (Child(40) == Child(41) or Parent(40) == Parent(41)) return 0 + ` other Parent(41) :: {} + ` if (other.child.value != 41 or same.text.len != 5) return 0 + ` return child.value + same.bias + `} + , + ) + defer arraylist.deinit(&bytes) + _ = try add_source(&session, root, "generated.hon", &bytes, &diagnostic_store) + modules[0] = try compile(&session, root, &diagnostic_store) + errdefer ir.deinit(&modules[0]) + modules[1] = try compile(&session, root, &diagnostic_store) + } + defer ir.deinit(&modules[0]) + defer ir.deinit(&modules[1]) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + for (0..modules.len) |index| { + outcome := try interpreter.run(mem.c_allocator, &modules[index], &diagnostic_store) + defer interpreter.deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(42), outcome.value.data.integer) + } + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) +} + dependency_traps_keep_original_spans_and_remain_demand_driven test { mains [2][]u8 :: [ `dep :: import "../dependency" diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index de20963..b8d3cef 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -113,6 +113,9 @@ Code :: enum { specialization_limit comptime_execution_limit comptime_value_escape + invalid_generated_field_name + invalid_struct_layout + invalid_c_record_field_type } Diagnostic :: struct { @@ -247,5 +250,8 @@ messages :: $enummap.init(Code, []u8, { specialization_limit = "compile-time specialization exceeds its recursion or instance limit", comptime_execution_limit = "compile-time execution exceeds its recursion or step limit", comptime_value_escape = "compile-time result cannot escape evaluation as a source value", + invalid_generated_field_name = "generated field name must be an ASCII identifier", + invalid_struct_layout = "generated record layout must be .auto or .c; C-layout records require at least one field", + invalid_c_record_field_type = "record field is not a supported C-layout value type", }) diff --git a/source/parser/parse.hon b/source/parser/parse.hon index aa5dfac..6d3cf39 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -1428,7 +1428,7 @@ parse_call proc(state @mut State, callee ast.NodeId, start u32, builtin bool) as defer state.allow_record_literals = previous state.paren_depth += 1 defer state.paren_depth -= 1 - skip_operand_newlines(state) + skip_argument_newlines(state) arguments arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator) defer arraylist.deinit(&arguments) argument_start :: state.next_token @@ -1441,7 +1441,7 @@ parse_call proc(state @mut State, callee ast.NodeId, start u32, builtin bool) as try arraylist.append(&arguments, argument) if (!next_is(state, .comma)) break state.next_token += 1 - skip_operand_newlines(state) + skip_argument_newlines(state) } if (!previous and !next_is(state, .close_paren)) { # a guard's body may look like a record argument until its missing ')' is known. @@ -1744,6 +1744,15 @@ skip_operand_newlines proc(state @mut State) void { ) state.next_token = start } +@hide:file +skip_argument_newlines proc(state @mut State) void { + skip_operand_newlines(state) + if (!next_is(state, .newline)) return + start :: state.next_token + skip_newlines(state) + if (!next_is(state, .open_curly)) state.next_token = start +} + @hide:file skip_continuation_newlines proc(state @mut State) void { if (state.paren_depth == 0 or !next_is(state, .newline)) return diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index dc107f9..419af1a 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -3312,6 +3312,28 @@ intern_record proc(state @mut State, declaration ast.NodeId, fields []mut Record return Type{ kind = .record, record_type = RecordId(state.records.items.len - 1) } } +@hide:file +c_record_field_type proc(state @State, type Type, depth usize) bool { + if (depth >= 128 or type.nominal != ast.NodeId(maxval!(u32))) return false + if (numeric_kind(type.kind) or type.kind == .bool or type.kind == .pointer) return true + if (type.kind == .optional) + return state.arrays.items[usize(type.array_type)].element.kind == .pointer + if (type.kind == .enumeration) + return state.enums.items[usize(type.enum_type)].explicit_backing + if (type.kind == .array) { + array :: state.arrays.items[usize(type.array_type)] + return array.len > 0 and c_record_field_type(state, array.element, depth + 1) + } + if (type.kind != .record) return false + record :: state.records.items[usize(type.record_type)] + if (record.is_union or record.declaration == ast.NodeId(maxval!(u32)) + or !ast.record_c_layout(state.tree, record.declaration) or record.fields.len == 0) return false + for (record.fields) |field| { + if (!c_record_field_type(state, field.type, depth + 1)) return false + } + return true +} + @hide:file check_record proc(state @mut State, id ast.NodeId, nominal bool) Type ! (mem.AllocError | diagnostics.CapacityError) { cached := state.cells[usize(id)].type @@ -3358,6 +3380,9 @@ check_record proc(state @mut State, id ast.NodeId, nominal bool) Type ! (mem.All if (type.kind == .void or type.kind == .range or type.kind == .type_value or type.kind == .none) { type = invalid_type(try diagnostics.add(state.diagnostics, .invalid_record_field_type, view.span)) } + if (type.kind != .invalid and ast.record_c_layout(state.tree, id) + and !c_record_field_type(state, type, 0)) + type = invalid_type(try diagnostics.add(state.diagnostics, .invalid_c_record_field_type, view.span)) if (view.name) |name| { if (named_field(state.tree, fields[0..i], name) != no_index) { type = invalid_type(try diagnostics.add(state.diagnostics, .duplicate_field, ast.node_span(state.tree, name))) diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 001ba0c..55ef5f4 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -123,7 +123,8 @@ elaborate proc( 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 + text :: ast.source_text(&original, ast.node_span(&original, name)) + if (mem.eql(text, "field") or mem.eql(text, "struct_type")) needed = true } if (class == .function and ast.function_generic(&original, id)) needed = true if (class == .function) { @@ -303,6 +304,10 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { try expand_field(state, id) return } + if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "struct_type")) { + try expand_struct_type(state, id) + return + } } if (class == .expression and ast.expr_kind(&state.tree, id) == .call) { callee :: ast.call_expr(&state.tree, id).callee @@ -413,6 +418,235 @@ expand_field proc(state @mut State, id ast.NodeId) void ! Error { try expand(state, id) } +# literal collections deliberately retain per-field context: generated records +# may pair heterogeneous types and optional defaults in arrays as well as tuples. +@hide:file +struct_collection proc(state @mut State, input ast.NodeId) ast.NodeId ! Error { + if (!try type_step(state, input)) return no_node + try expand(state, input) + resolved :: binding(state, input) + if (resolved.kind == .invalid) { + state.evaluation_diagnostic = resolved.data.diagnostic + return no_node + } + if (resolved.kind == .declaration + and ast.node_class(&state.tree, resolved.data.declaration) == .statement) { + declaration :: ast.statement_decl(&state.tree, resolved.data.declaration) + if (declaration.mutability != .constant) return no_node + if (declaration.type) |annotation| { + value :: try type_execution_value(state, declaration.initializer, annotation) + if (value == no_node) return no_node + return try struct_collection(state, value) + } + return try struct_collection(state, declaration.initializer) + } + kind :: ast.expr_kind(&state.tree, input) + if (kind == .array_literal) return input + if (kind == .record_literal) { + if (ast.record_literal(&state.tree, input).type != null) { + annotation :: try expression_type(state, input, 0) + value :: try type_execution_value(state, input, annotation) + if (value == no_node) return no_node + return try struct_collection(state, value) + } + for (0..ast.record_field_count(&state.tree, input)) |i| { + if (ast.field(&state.tree, ast.record_field(&state.tree, input, i)).name != null) return no_node + } + return input + } + if (kind == .typed_constant) { + value :: ast.typed_constant(&state.tree, input) + annotation :: try canonical_type(state, value.type) + if (annotation == no_node) return no_node + if (ast.expr_kind(&state.tree, annotation) != .array_type) { + if (ast.expr_kind(&state.tree, annotation) != .record_type) return no_node + for (0..ast.record_field_count(&state.tree, annotation)) |i| { + if (ast.field(&state.tree, ast.record_field(&state.tree, annotation, i)).name != null) return no_node + } + } + if (!try type_array_initialized(state, input, input)) return no_node + return input + } + value :: try evaluate(state, input) + if (value.kind != .aggregate or value.annotation == no_node) return no_node + owned :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, input), + value.annotation, value.node) + try refresh(state) + return try struct_collection(state, owned) +} + +@hide:file +struct_collection_count proc(state @State, input ast.NodeId) usize { + value :: if (ast.expr_kind(&state.tree, input) == .typed_constant) + ast.typed_constant(&state.tree, input).value else input + return if (ast.expr_kind(&state.tree, value) == .array_literal) + ast.array_element_count(&state.tree, value) else ast.record_field_count(&state.tree, value) +} + +@hide:file +struct_collection_item proc(state @mut State, input ast.NodeId, index usize) ast.NodeId ! Error { + value := input + annotation := no_node + if (ast.expr_kind(&state.tree, input) == .typed_constant) { + view :: ast.typed_constant(&state.tree, input) + value = view.value + type :: try canonical_type(state, view.type) + if (ast.expr_kind(&state.tree, type) == .array_type) annotation = ast.array_type(&state.tree, type).element + else annotation = ast.field(&state.tree, ast.record_field(&state.tree, type, index)).type? + } + element :: if (ast.expr_kind(&state.tree, value) == .array_literal) + ast.array_element(&state.tree, value, index) + else ast.field(&state.tree, ast.record_field(&state.tree, value, index)).value? + if (annotation == no_node or is_type(state, annotation) + or ast.expr_kind(&state.tree, element) == .typed_constant) return element + result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, element), annotation, element) + try refresh(state) + return result +} + +@hide:file +expand_struct_type proc(state @mut State, id ast.NodeId) void ! Error { + if (ast.call_arg_count(&state.tree, id) != 4) { + try reject(state, id, .argument_count_mismatch) + return + } + if (state.type_execution_depth == 0) state.type_execution_remaining = 4096 + state.type_execution_depth += 1 + defer state.type_execution_depth -= 1 + state.evaluation_diagnostic = diagnostics.no_diagnostic + layout_input :: ast.call_arg(&state.tree, id, 0) + try expand(state, layout_input) + layout := layout_input + if (ast.expr_kind(&state.tree, layout) != .enum_literal) { + value :: try evaluate_ir(state, layout, try expression_type(state, layout, 0)) + layout = value.node + } + if (layout == no_node or ast.expr_kind(&state.tree, layout) != .enum_literal) { + if (state.evaluation_diagnostic == diagnostics.no_diagnostic) + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .invalid_struct_layout, ast.node_span(&state.tree, layout_input)) + try reject_evaluation(state, id) + return + } + layout_name :: ast.literal_text(&state.tree, ast.enum_literal(&state.tree, layout)) + c_layout :: mem.eql(layout_name, "c") + if (!c_layout and !mem.eql(layout_name, "auto")) { + try reject(state, id, .invalid_struct_layout) + return + } + collections [3]mut ast.NodeId := undefined + for (0..usize(3)) |i| { + collections[i] = try struct_collection(state, ast.call_arg(&state.tree, id, i + 1)) + if (collections[i] == no_node) { + try reject_evaluation(state, id) + return + } + } + count :: struct_collection_count(state, collections[0]) + if (count != struct_collection_count(state, collections[1]) + or count != struct_collection_count(state, collections[2])) { + try reject(state, id, .array_length_mismatch) + return + } + if (c_layout and count == 0) { + try reject(state, id, .invalid_struct_layout) + return + } + if (count > state.type_execution_remaining) { + try reject(state, id, .comptime_execution_limit) + return + } + fields := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&fields) + bytes := arraylist.init(u8, state.allocator) + defer arraylist.deinit(&bytes) + for (0..count) |i| { + if (!try type_step(state, id)) { + try reject_evaluation(state, id) + return + } + name_input :: try struct_collection_item(state, collections[0], i) + name_value :: try evaluate(state, name_input) + if (name_value.kind != .string) { + try reject_evaluation(state, id) + return + } + bytes.items = bytes.items[..0] + text :: ast.literal_text(&state.tree, name_value.node) + cursor := usize(1) + while (cursor < text.len - 1) { + byte :: ast.string_next(text, &cursor) catch { + try reject(state, id, .invalid_escape) + return + } + valid :: ( + byte == '_' or (byte >= 'a' and byte <= 'z') or (byte >= 'A' and byte <= 'Z') + or (bytes.items.len > 0 and byte >= '0' and byte <= '9') + ) + if (!valid) { + try reject(state, id, .invalid_generated_field_name) + return + } + try arraylist.append(&bytes, byte) + } + if (bytes.items.len == 0) { + try reject(state, id, .invalid_generated_field_name) + return + } + span :: ast.node_span(&state.tree, name_input) + name :: try ast.add_generated_identifier(state.builder, span, + try strpool.intern(state.strings, bytes.items), bytes.items) + try refresh(state) + for (fields.items) |previous| { + if (ast.name_id(&state.tree, ast.field(&state.tree, previous).name?) == ast.name_id(&state.tree, name)) { + try reject(state, id, .duplicate_field) + return + } + } + type_input :: try struct_collection_item(state, collections[1], i) + type_value :: try evaluate(state, type_input) + if (type_value.kind != .type) { + if (state.evaluation_diagnostic == diagnostics.no_diagnostic) + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .invalid_record_field_type, ast.node_span(&state.tree, type_input)) + try reject_evaluation(state, id) + return + } + field_type :: (try materialize(state, type_value)).node + default_input :: try struct_collection_item(state, collections[2], i) + optional :: try ast.add_optional_type(state.builder, span, field_type) + contextual :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, default_input), optional, default_input) + capture :: try ast.add_identifier(state.builder, span, strpool.no_str) + try refresh(state) + present :: try type_capture_guard(state, contextual, capture) + if (present.kind != .boolean) { + try reject_evaluation(state, id) + return + } + default ?ast.NodeId := null + if (present.boolean) { + unwrapped :: try ast.add_unary(state.builder, + ast.node_span(&state.tree, default_input), .unwrap, contextual) + try refresh(state) + value :: try type_execution_value(state, unwrapped, field_type) + if (value == no_node) { + try reject_evaluation(state, id) + return + } + default = value + } + field :: try ast.add_field(state.builder, span, name, field_type, default) + try refresh(state) + try arraylist.append(&fields, field) + } + record :: try ast.add_record_type(state.builder, ast.node_span(&state.tree, id), fields.items) + if (c_layout) ast.set_record_c_layout(state.builder, record) + try replace(state, id, record) + 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 @@ -3100,6 +3334,10 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! view :: ast.selection_expr(&state.tree, id) base := try expression_type(state, view.base, depth + 1) if (base == no_node) return no_node + if (is_type(state, base)) { + selected :: try canonical_type(state, view.base) + if (selected != no_node and ast.expr_kind(&state.tree, selected) == .enum_type) return selected + } 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.literal_text(&state.tree, view.member) diff --git a/tests/native.py b/tests/native.py index c066f29..2ab577a 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,162 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "struct_type_literal_fields_contextual_defaults_and_required_null": ( + """ + present proc() ??i16 { return 2 } + Record :: struct_type!( + .auto, + {"number", "ready", "text", "maybe", "required"}, + {i16, bool, string, ?i16, ?i16}, + {null, true, "ok", present(), null}, + ) + main proc() i16 { + value Record :: { number = 40, required = 2 } + if (!value.ready or value.text.len != 2) return -1 + if (value.maybe? != 2) return -2 + return value.number + value.required? + } + """, b"42\n", None, + ), + "struct_type_computed_arrays_snapshots_and_nominal_cache_identity": ( + """ + names proc() [2]mut string { + result [2]mut string := ["", ""] + result[0] = "value" + result[1] = "bias" + return result + } + change proc(input [2]mut type, T type) [2]mut type { + copied := input + copied[0] = T + return copied + } + Generated proc(T type, bias i16) type { + fields := names() + types [2]mut type := undefined + types[0] = i8 + types[1] = i16 + changed := change(types, T) + if (types[0] != i8 or changed[0] != T) return bool + defaults [2]mut ?i16 := undefined + defaults[0] = null + defaults[1] = bias + Result :: struct_type!(.auto, fields, changed, defaults) + fields[0] = "replaced" + types[1] = bool + changed[0] = bool + defaults[1] = 0 + return Result + } + Other proc() type { + return struct_type!(.auto, ["value", "bias"], [i16, i16], [null, 2]) + } + Selected :: alias Generated(i16, 2) + main proc() i16 { + if (Selected != Generated(i16, 2)) return -1 + if (Selected == Generated(i16, 3) or Selected == Other()) return -2 + value Selected :: { value = 40 } + same Generated(i16, 2) :: value + return same.value + same.bias + } + """, b"42\n", None, + ), + "struct_type_tuple_helper_nested_aggregate_defaults_and_copy_isolation": ( + """ + Child :: struct { values [2]mut i8, maybe ?i8 = null } + names proc() struct { string, string } { return {"rows", "bias"} } + Generated proc() type { + return struct_type!( + .auto, + names(), + [[2]mut Child, i8], + [[{ values = [19, 0] }, { values = [0, 23], maybe = 2 }], 127], + ) + } + copy proc(value Generated()) Generated() { return value } + main proc() i16 { + first Generated() := {} + saved :: copy(first) + first.rows[0].values[0] = 0 + first.rows[1].maybe = null + second Generated() :: {} + if (saved.rows[1].maybe? != 2 or second.bias != 127) return -1 + return i16(saved.rows[0].values[0]) + i16(second.rows[1].values[1]) + } + """, b"42\n", None, + ), + "struct_type_empty_auto_and_nested_c_layout": ( + """ + Layout :: enum { auto, c } + Tag :: enum(u8) { ready = 7 } + Empty :: struct_type!(.auto, [], [], []) + Inner :: struct_type!(.c, ["value"], [i16], [null]) + Packet :: struct_type!( + Layout.c, + {"tag", "items", "pointer", "maybe"}, + {Tag, [2]Inner, @i16, ?@i16}, + {.ready, null, null, null}, + ) + identity proc(value Empty) Empty { return value } + main proc() i16 { + empty Empty :: identity({}) + if (sizeof!(Packet) != 24 or alignof!(Packet) != 8) return -3 + extra i16 :: 2 + packet Packet :: { + items = [{ value = 19 }, { value = 21 }], + pointer = &extra, + maybe = null, + } + if (packet.tag != .ready) return -1 + if packet.maybe |_| return -2 + return packet.items[0].value + packet.items[1].value + packet.pointer^ + } + """, b"42\n", None, + ), + "struct_type_required_optional_is_not_a_null_default": ( + """ + Record :: struct_type!(.auto, ["required"], [?i16], [null]) + main proc() i16 { + value Record :: {} + return 42 + } + """, b"", b"missing a required field", + ), + "struct_type_default_preserves_narrow_field_arithmetic": ( + """ + Record :: struct_type!(.auto, ["value"], [i8], [127]) + main proc() i16 { + value Record := {} + value.value = value.value + 1 + return i16(value.value) + } + """, b"", b"integer arithmetic overflow", + ), + "struct_type_runtime_dependencies_are_not_captured": ( + """ + read proc(value i16) i16 { + Record :: struct_type!(.auto, ["value"], [i16], [value]) + result Record :: {} + return result.value + } + main proc() i16 { return read(42) } + """, b"", b"compile-time value", + ), + "struct_type_incomplete_array_snapshot_stays_incomplete": ( + """ + Generated proc() type { + types [2]mut type := undefined + types[0] = i16 + saved :: types + types[1] = i16 + return struct_type!(.auto, ["first", "second"], saved, [null, null]) + } + main proc() i16 { + value Generated() :: { first = 19, second = 23 } + return value.first + value.second + } + """, b"", b"initialization", + ), "type_records_local_schema_defaults_and_owned_copies": ( """ Storage proc() type { @@ -5327,6 +5483,94 @@ CASES = { ), } +# Keep malformed schemas grouped: each must fail before generated storage is used. +for name, expression, diagnostic in ( + ( + "argument_count", + 'struct_type!(.auto, ["value"], [i16])', + b"argument count", + ), + ( + "invalid_layout", + 'struct_type!(.packed, ["value"], [i16], [null])', + b"layout", + ), + ( + "invalid_field_name", + 'struct_type!(.auto, ["9value"], [i16], [null])', + b"generated field name", + ), + ( + "duplicate_field_name", + 'struct_type!(.auto, ["value", "value"], [i16, i16], [null, null])', + b"specified more than once", + ), + ( + "named_record_collection", + 'struct_type!(.auto, {name = "value"}, [i16], [null])', + b"compile-time value", + ), + ( + "unequal_collection_lengths", + 'struct_type!(.auto, ["value", "bias"], [i16], [null, 2])', + b"length does not match", + ), + ( + "non_type_field", + 'struct_type!(.auto, ["value"], [42], [null])', + b"record fields require a concrete value type", + ), + ( + "compile_time_only_field", + 'struct_type!(.auto, ["value"], [type], [null])', + b"record fields require a concrete value type", + ), + ( + "wrong_typed_default", + 'struct_type!(.auto, ["value"], [i16], [u8(2)])', + b"incompatible value type", + ), + ( + "out_of_range_default", + 'struct_type!(.auto, ["value"], [i8], [128])', + b"outside its runtime type", + ), + ( + "empty_c_layout", + 'struct_type!(.c, [], [], [])', + b"C-layout", + ), + ( + "non_c_field", + 'struct_type!(.c, ["value"], [string], [null])', + b"C-layout", + ), + ( + "non_c_nested_record", + 'struct_type!(.c, ["value"], [struct { value i16 }], [null])', + b"C-layout", + ), +): + CASES[f"struct_type_rejects_{name}"] = ( + f""" + Generated proc() type {{ return {expression} }} + main proc() i16 {{ + value Generated() :: {{}} + return 42 + }} + """, b"", diagnostic, + ) + +generated_field_count = 4097 +CASES["struct_type_bounds_generated_field_construction"] = ( + "Record :: struct_type!(.auto, [" + + ", ".join(f'"field_{index}"' for index in range(generated_field_count)) + + "], [" + ", ".join(["i16"] * generated_field_count) + + "], [" + ", ".join(["null"] * generated_field_count) + + "])\nmain proc() i16 { value Record :: {}\nreturn 42 }\n", + b"", b"compile-time execution exceeds", +) + # Exercise each representation through storage, calls, ranges and checked updates. for integer_type, bits, signed in ( ("i8", 8, True), ("i16", 16, True), ("i32", 32, True), @@ -5720,7 +5964,33 @@ def check_record_packages(temporary, executable): } """).strip() + "\n") check(app, executable, (b"42\n", None)) - return 4 + types.write_text(textwrap.dedent(""" + Generated proc(bias i16) type { + return struct_type!(.auto, ["value", "bias"], [i16, i16], [null, bias]) + } + """).strip() + "\n") + (library / "functions.hon").write_text( + "identity proc(value Generated(2)) Generated(2) { return value }\n" + ) + (app / "main.hon").write_text(textwrap.dedent(""" + lib :: import "../library" + Selected :: alias lib.Generated(2) + main proc() i16 { + value Selected :: { value = 40 } + same lib.Generated(2) :: lib.identity(value) + return same.value + same.bias + } + """).strip() + "\n") + check(app, executable, (b"42\n", None)) + types.write_text(textwrap.dedent(""" + Generated proc(bias i16) type { + return struct_type!(.auto, ["bad-name"], [i16], [null]) + } + """).strip() + "\n") + check(app, executable, (b"", b"generated field name")) + diagnosed = run(str(HONEY), "run", str(app)) + assert diagnosed.stderr.startswith(f"{types.resolve()}:2:".encode()), outcome(diagnosed) + return 6 def check_data_model_packages(temporary, executable):