From 83b6153e72b9b6100a6ce011a605677abd11c934 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Fri, 30 Oct 2026 18:04:05 +0100 Subject: [PATCH] feat: construct partial compile-time type records --- README.md | 6 +- TODO.md | 10 +- source/compilation/compilation.test.hon | 14 +++ source/sema/specialize/specialize.hon | 65 ++++++---- tests/native.py | 153 ++++++++++++++++++++++++ 5 files changed, 221 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 888628e..76cf125 100644 --- a/README.md +++ b/README.md @@ -112,11 +112,13 @@ inline tuples may mix types and values, including nested aggregates, strings, op initialized fixed arrays of `type` live entirely inside compile-time execution, including nested arrays, explicit/inferred annotations, `.len`, checked `usize` indexing and collection captures. for example, `types [2]mut type := [i8, bool]`, then `types[1] = i16` and `return [types.len]types[1]`, generates `[2]i16` without runtime type metadata. whole-array and nested indexed assignments preserve shape and mutability at every level; copies and loop captures retain earlier values while borrowed array places observe later updates. -initialized records/tuples containing `type` fields live inside compile-time producers. local schemas, typed/contextual/anonymous construction, defaults, nested records/arrays, field/index reads and updates, whole-value replacement and by-value collection captures retain owned snapshots. for example, `config struct { child type, count usize } := { child = i8, count = 2 }`, then `config.child = i16` and `return [config.count]config.child`, generates `[2]i16`. earlier copies and loop captures remain independent, while borrowed array places observe later updates. type leaves retain nominal identity; ordinary value leaves reuse checked constant execution. unknown/duplicate/missing fields, invalid types/defaults, readonly places, narrow overflow and dependency provenance retain diagnostics. record validation/construction shares the type-control bound. runtime type-valued records, partial record initialization, metadata reflection and inline expansion remain open. +initialized records/tuples containing `type` fields live inside compile-time producers. local schemas, typed/contextual/anonymous construction, defaults, nested records/arrays, field/index reads and updates, whole-value replacement and by-value collection captures retain owned snapshots. for example, `config struct { child type, count usize } := { child = i8, count = 2 }`, then `config.child = i16` and `return [config.count]config.child`, generates `[2]i16`. earlier copies and loop captures remain independent, while borrowed array places observe later updates. type leaves retain nominal identity; ordinary value leaves reuse checked constant execution. unknown/duplicate/missing fields, invalid types/defaults, readonly places, narrow overflow and dependency provenance retain diagnostics. record validation/construction shares the type-control bound. runtime type-valued records remain unsupported. + +type-valued records/tuples may also start with `undefined`: `config struct { child type, count usize } := undefined`, then `config.child = i16` and `config.count = 2`. nested record fields, arrays of records, tuple ordinals and `field!` updates use the same persistent snapshots as initialized storage; partial copies retain independent initialization state. defaults do not initialize `undefined` fields. initialized leaves and array lengths can be read while other leaves remain uninitialized, but premature leaf reads and incomplete procedure arguments/results diagnose read-before-initialization. fully initialized results cross compile-time procedure boundaries and supply erased generic inputs, generated storage and defaults. empty shapes are initialized vacuously; each expanded record field/array cell shares the construction bound. fixed type arrays may also start with `undefined` inside type/type-array producers: `types [2]mut type := undefined`, then fill `types[index] = i16` in a loop. nested and whole-array updates preserve shape, mutability and independent initialization state in local copies; `.len` is available before elements initialize. reading an uninitialized type leaf, returning an incomplete array or passing one to a compile-time procedure diagnoses a read before initialization. empty shapes are valid, but every dimension must be explicit and nonnegative. construction charges each expanded cell against the shared execution bound, including nested zero-leaf shapes. -ordinary fixed value arrays use the same construction inside type/type-array producers: `counts [2]mut usize := undefined`, then fill indexed elements or replace whole rows/arrays. initialized leaves retain exact scalar, string, optional, record and tagged-union values; local copies retain independent partial initialization state. generated lengths/defaults and compile-time arguments can use initialized elements, while incomplete arrays cannot enter checked IR or procedure arguments. array `.len` and empty shapes do not read leaves. partial type-valued record construction and constant materialization of mutable slices or nonnull data pointers remain unsupported. +ordinary fixed value arrays use the same construction inside type/type-array producers: `counts [2]mut usize := undefined`, then fill indexed elements or replace whole rows/arrays. initialized leaves retain exact scalar, string, optional, record and tagged-union values; local copies retain independent partial initialization state. generated lengths/defaults and compile-time arguments can use initialized elements, while incomplete arrays cannot enter checked IR or procedure arguments. array `.len` and empty shapes do not read leaves. constant materialization of mutable slices or nonnull data pointers remains unsupported. ordinary procedures also accept `undefined` as the initializer of a mutable local with a concrete runtime type, including nested records and fixed arrays. field/index writes initialize storage without reading its previous contents; aggregate copies retain independent contents and initialization state in the interpreter. ordinary compile-time calls can fill this storage and return materializable values, but observing or materializing an uninitialized leaf is diagnosed. native storage uses LLVM poison, not zero initialization: assign every leaf before observing it, and do not rely on native read-before-initialization traps for `undefined` storage. immutable bindings, globals, inferred array lengths and other expression uses remain invalid. diff --git a/TODO.md b/TODO.md index 0386715..add58b6 100644 --- a/TODO.md +++ b/TODO.md @@ -40,7 +40,8 @@ captures and `usize` indices through both paths; type-producing bodies iterate materializable arrays/strings by value without retaining interpreter addresses. type-valued fixed arrays support nested copies, indexed/whole updates, checked projections and by-value collection captures in type-producing bodies; -`undefined` type/value arrays track initialization per leaf while loops fill generated types/defaults. +`undefined` type/value arrays and type-valued records/tuples track initialization +per leaf while field/index updates fill generated types/defaults. type-array and type-record parameters/results cross compile-time procedure boundaries with owned snapshots, cached nominal identity and erased arguments. initialized type-valued records/tuples support schemas/defaults, nested @@ -131,9 +132,9 @@ test discovery and FFI remain outside these completed slices. - [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, type-valued record procedure boundaries and reflection remain open. - [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. - [x] uninitialized type-array checkpoint: fixed type arrays in type/type-array producers start with `undefined` and fill through loop-driven indexed, nested and whole-array updates. persistent local copies retain independent initialization state; `.len` works before leaves initialize. premature leaf reads and incomplete procedure arguments/results diagnose read-before-initialization without caching invalid values. explicit nonnegative dimensions, empty shapes, ordinary mutability/shape checks, nominal identity and repeatable compilation work through interpreter/native execution. construction shares the type-control bound per expanded cell, including nested zero-leaf shapes. ordinary value-array construction, local type-valued records, runtime undefined storage and metadata reflection are covered below. - - [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 and ordinary value-returning procedures are covered below; partial type-valued 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. + - [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, ordinary value-returning procedures and partial type-valued records are covered below; storage-bearing constant materialization remains 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. procedure inputs/results, partial record initialization, metadata reflection and inline expansion are covered below; runtime type-valued records 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. partial record initialization, reflection and inline expansion are covered below; runtime metadata storage remains open. - [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. - [x] inline-loop checkpoint: `inline for` expands constant integer ranges, fixed arrays (including type-valued arrays) and strings into separately checked runtime bodies through interpreter/native execution. exact immutable captures, `usize` collection indices, narrow inclusive maxima, selected conditional/tagged-match bodies, static break/continue tail pruning, nested inline/ordinary loops, runtime returns/errors and scoped cleanup retain ordinary semantics. empty loops skip body elaboration; runtime-dependent inputs/transfers, pointer captures, storage-bearing escapes and shared expansion bounds retain diagnostics. imported producers/provenance and repeatable compilation are verified. heterogeneous tuples and inline-match syntax are covered below; metadata iteration remains open. - [x] heterogeneous inline-tuple checkpoint: `inline for` iterates constant tuples with exact per-field value/type captures and `usize` indices through interpreter/native execution. literal/helper/aliased tuples, erased generic inputs, nested tuple projections, generated storage, empty shapes, static exits, cleanup and aggregate copy isolation retain existing semantics. type-producing bodies borrow tuple places so later fields observe updates while saved copies remain isolated. runtime dependencies and storage-bearing escapes are rejected before iteration, including discarded captures and early exits; named records and ordinary tuple loops remain invalid. imported producers, nominal type leaves and repeatable compilation are verified. inline-match syntax is covered below; metadata collections remain open. @@ -153,6 +154,7 @@ test discovery and FFI remain outside these completed slices. - [x] nonzero-sentinel byte-slice checkpoint: readonly `[;sentinel]u8` compile-time results own exact payload bytes and any byte sentinel through interpreter/native execution. canonical string-backed views preserve logical lengths, empty views, embedded NULs, high bytes, computed/direct specialization-cache identity, generated defaults, inline captures, nested aggregates, optionals and both fallible channels. sentinel checks observe final evaluation storage, diagnosing mutation, uninitialized bytes and invalid addresses; imported helpers, source/session disposal and repeated compilation retain ownership. nonbyte/nominal-byte slices are covered below; mutable slices, nonnull data pointers and address-bearing untagged union layouts retain the escape boundary. - [x] untagged-union materialization checkpoint: address-free untagged unions own full native-layout byte snapshots through interpreter/native execution, including scalar/IEEE reinterpretation, nested padded records/arrays, smaller overlapping updates, zero-sized storage, distinct members and generated nominal unions. explicit `$()` values, compile-time arguments, full-byte specialization-cache identity, generated defaults, nested optionals/tagged payloads and both fallible channels preserve exact types and independent copies. uninitialized tail/padding bytes diagnose read-before-initialization; every member layout is checked recursively to reject data pointers, callbacks, strings and slices rather than retain interpreter handles. imported helpers/diagnostics, source/session disposal and repeated compilation retain ownership. address-bearing union layouts and broader constant materialization remain open. - [x] readonly typed-slice materialization checkpoint: ordinary compile-time calls snapshot readonly slices of materializable elements into module-lifetime backing through interpreter/native execution. nominal bytes, wider integers, arrays/records, source-bound callbacks, nested readonly slices, empty/zero-sized elements and full-width signed/unsigned sentinels retain exact types and logical lengths. explicit `$()` values, computed/direct content-based specialization identity, generated defaults and nested optional/tagged/fallible channels preserve ownership across helper returns, imported private declarations, source/session disposal and repeat compilation. initialization, address-range, sentinel, mutable-slice and nonnull-pointer escape diagnostics remain checked; 100,000-value/128-level snapshot bounds reject oversized and cyclic storage. mutable slices, nonnull data pointers and address-bearing untagged union layouts remain open. + - [x] uninitialized type-record checkpoint: compile-time records/tuples containing type values start with `undefined` and fill through nested field/index updates or whole-value replacement. persistent snapshots preserve independent partial copies; defaults do not initialize undefined fields. initialized leaves and empty shapes work while premature type/value reads and incomplete procedure arguments/results diagnose read-before-initialization. fully initialized results supply erased generic inputs, generated storage/defaults and cached nominal types through interpreter/native execution. ordinary mutability/schema validation and shared per-field/cell construction bounds remain checked; general type-valued execution and broader materialization remain open. - [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including address-bearing untagged unions, nonnull data pointers and mutable slices), plus remaining existing-library acceptance for broader `meta` operations and `io.print`. formatting parsing and broader imported C-binding support may interleave with later gates. - [ ] remaining source constructs: multiline strings. boolean and optional-capture conditional expressions, yielding value blocks, `yield`, compound updates and bitwise operators are covered above. 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. diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index ff96f86..e4c945e 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -231,6 +231,17 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { `} `BoundaryConfig :: struct { child type, counts [2]mut usize, bias i16 = 2 } `record_config proc() BoundaryConfig { return { child = i16, counts = [1, 2] } } + `partial_record_config proc() BoundaryConfig { + ` config BoundaryConfig := undefined + ` config.child = i8 + ` config.counts[0] = 1 + ` copied := config + ` config.child = bool + ` copied.child = i16 + ` copied.counts[1] = 2 + ` copied.bias = 2 + ` return copied + `} `change_record_config proc(input BoundaryConfig) BoundaryConfig { ` copied := input ` copied.child = u8 @@ -310,6 +321,9 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { ` same_record_boundary RecordBoundary({ child = i16, counts = [1, 2] }) :: record_boundary ` if (same_record_boundary.values[0] + same_record_boundary.values[1] + same_record_boundary.bias != 42) return 0 ` if (read_record_config(record_config(), 40) != 42) return 0 + ` partial_boundary RecordBoundary(partial_record_config()) :: record_boundary + ` if (partial_boundary.values[0] + partial_boundary.values[1] + partial_boundary.bias != 42) return 0 + ` if (read_record_config(partial_record_config(), 40) != 42) return 0 ` if (read_record_pair(record_pair(), 40) != 42 or read_record_pair(record_pair(), 40) != 42) return 0 ` copied_initialized = same_initialized.second ` payload :: packet.choices[0].payload diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index cd80f76..ea86b2a 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -3679,14 +3679,14 @@ type_array_projection proc(state @mut State, id ast.NodeId) bool ! Error { if (ast.expr_kind(&state.tree, base) != .typed_constant) return true array :: ast.typed_constant(&state.tree, base) type :: try canonical_type(state, array.type) - record :: (type != no_node and type_record_storage(state, type, 0) - and (ast.expr_kind(&state.tree, type) == .record_type - or ast.expr_kind(&state.tree, type) == .union_type)) + record :: (type != no_node and (ast.expr_kind(&state.tree, type) == .record_type + or (type_record_storage(state, type, 0) and ast.expr_kind(&state.tree, type) == .union_type))) if ( !record and !try execution_array_storage(state, array.type) and !try metadata_sequence(state, array.type) ) return true selected := no_node + selected_type := no_node if (record) { if (kind != .selection) return true member :: ast.selection_expr(&state.tree, id).member @@ -3705,23 +3705,15 @@ type_array_projection proc(state @mut State, id ast.NodeId) bool ! Error { } selected = active.value? } else selected = ast.field(&state.tree, ast.record_field(&state.tree, array.value, slot)).value? + if (!type_record_storage(state, array.type, 0)) + selected_type = ast.field(&state.tree, ast.record_field(&state.tree, type, slot)).type? } else if (kind == .index) { index :: try type_array_index(state, ast.index_expr(&state.tree, id).index, ast.array_element_count(&state.tree, array.value)) if (index == no_index) return false selected = ast.array_element(&state.tree, array.value, index) - if (ast.expr_kind(&state.tree, selected) == .literal_undefined) { - state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, - .read_before_initialization, ast.node_span(&state.tree, id)) - return false - } - if (!type_record_storage(state, array.type, 0) - and ast.expr_kind(&state.tree, selected) != .typed_constant) { - descriptor :: ast.array_type(&state.tree, try canonical_type(state, array.type)) - selected = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, id), - descriptor.element, selected) - try refresh(state) - } + if (!type_record_storage(state, array.type, 0)) + selected_type = ast.array_type(&state.tree, type).element } else { member :: ast.selection_expr(&state.tree, id).member if (!type_record_storage(state, array.type, 0) @@ -3737,6 +3729,16 @@ type_array_projection proc(state @mut State, id ast.NodeId) bool ! Error { try builtin(state, id, .usize), count) try refresh(state) } + if (ast.expr_kind(&state.tree, selected) == .literal_undefined) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .read_before_initialization, ast.node_span(&state.tree, id)) + return false + } + if (selected_type != no_node and ast.expr_kind(&state.tree, selected) != .typed_constant) { + selected = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, id), + selected_type, selected) + try refresh(state) + } # copying a nominal definition into the projection would create a new # identity on every read. retain its original node behind a type alias. if (nominal_type_value(state, selected)) { @@ -3888,13 +3890,15 @@ type_record_check proc(state @mut State, annotation ast.NodeId) bool ! Error { return true } -# normalize complete record snapshots in declaration order, evaluating every leaf -# through its declared context. no record containing type values enters runtime IR. +# normalize record snapshots in declaration order through their declared context. +# undefined records retain per-leaf initialization state without applying defaults. @hide:file type_record_value proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { type :: try canonical_type(state, annotation) if (ast.expr_kind(&state.tree, type) == .union_type) return try type_union_value(state, expression, type) + if (ast.expr_kind(&state.tree, expression) == .literal_undefined) + return try type_storage_undefined(state, expression, annotation) if (ast.expr_kind(&state.tree, expression) == .typed_constant) { actual :: ast.typed_constant(&state.tree, expression) if (try same_type(state, type, actual.type)) return expression @@ -4023,9 +4027,28 @@ type_array_initialized proc(state @mut State, value, at ast.NodeId) bool ! Error # bound the expanded snapshot, not just its dimensions, so later reads and # initialization checks cannot traverse an exponentially shared empty shape. @hide:file -type_array_undefined proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { +type_storage_undefined proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { if (is_type(state, annotation)) return expression type :: try canonical_type(state, annotation) + if (type != no_node and ast.expr_kind(&state.tree, type) == .record_type) { + if (!try type_record_check(state, type)) return no_node + fields := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&fields) + for (0..ast.record_field_count(&state.tree, type)) |i| { + field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i)) + if (!try type_step(state, expression)) return no_node + value :: try type_storage_undefined(state, expression, field.type?) + if (value == no_node) return no_node + generated :: try ast.add_field(state.builder, field.span, field.name, null, value) + try refresh(state) + try arraylist.append(&fields, generated) + } + span :: ast.node_span(&state.tree, expression) + literal :: try ast.add_record_literal(state.builder, span, null, fields.items) + result :: try ast.add_typed_constant(state.builder, span, type, literal) + try refresh(state) + return result + } if (type == no_node or ast.expr_kind(&state.tree, type) != .array_type) return expression descriptor :: ast.array_type(&state.tree, type) length := Value{} @@ -4036,14 +4059,14 @@ type_array_undefined proc(state @mut State, expression, annotation ast.NodeId) a return no_node } # validate child dimensions even when this dimension is empty. - child :: try type_array_undefined(state, expression, descriptor.element) + child :: try type_storage_undefined(state, expression, descriptor.element) if (child == no_node) return no_node elements := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&elements) for (0..length.magnitude) |i| { if (!try type_step(state, expression)) return no_node element := child - if (i != 0) element = try type_array_undefined(state, expression, descriptor.element) + if (i != 0) element = try type_storage_undefined(state, expression, descriptor.element) if (element == no_node) return no_node try arraylist.append(&elements, element) } @@ -4062,7 +4085,7 @@ type_array_undefined proc(state @mut State, expression, annotation ast.NodeId) a @hide:file type_array_value proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { if (ast.expr_kind(&state.tree, expression) == .literal_undefined) - return try type_array_undefined(state, expression, annotation) + return try type_storage_undefined(state, expression, annotation) type :: try canonical_type(state, annotation) array :: ast.array_type(&state.tree, type) if (ast.expr_kind(&state.tree, expression) == .typed_constant) { diff --git a/tests/native.py b/tests/native.py index 5cee5ce..e12e867 100644 --- a/tests/native.py +++ b/tests/native.py @@ -2278,6 +2278,159 @@ CASES = { } """, b"", b"initialization", ), + "type_records_undefined_nested_updates_preserve_partial_copies": ( + """ + Config :: struct { + child type = bool + detail struct { count usize, bias i16 } + rows [2]mut struct { child type, count usize } + } + config proc() Config { + value Config := undefined + value.child = i16 + value.detail.count = 2 + copied := value + value.detail.bias = 99 + copied.detail.bias = 2 + for (usize(0)..copied.rows.len) |index| { + copied.rows[index].child = copied.child + copied.rows[index].count = index + 1 + } + return copied + } + Storage proc(input Config) type { + return struct { values [input.detail.count]input.rows[1].child, bias i16 = input.detail.bias } + } + main proc() i16 { + value Storage(config()) :: { values = [19, 21] } + same Storage(config()) :: value + return same.values[0] + same.values[1] + same.bias + } + """, b"42\n", None, + ), + "type_records_undefined_tuples_replacement_and_empty_fields": ( + """ + Pair :: struct { type, struct { child type, values [0]mut type } } + pair proc() Pair { + value Pair := undefined + value.0 = bool + value.1.child = i8 + copied := value + value = {i16, { child = i16, values = [] }} + copied.0 = value.0 + copied.1 = value.1 + return copied + } + Element proc(input Pair) type { return field!(input, "0") } + add proc($schema Pair, value Element(schema)) Element(schema) { + if (schema.1.child != i16) return 0 + return value + 2 + } + main proc() i16 { return add(pair(), 40) } + """, b"42\n", None, + ), + "type_records_undefined_reject_type_leaf_read": ( + """ + Bad proc() type { + value struct { child type } := undefined + return value.child + } + main proc() Bad() { return 42 } + """, b"", b"local value was read before initialization", + ), + "type_records_undefined_reject_value_leaf_read": ( + """ + Bad proc() type { + value struct { child type, count usize } := undefined + value.child = i16 + return [value.count]value.child + } + main proc() Bad() { return 42 } + """, b"", b"local value was read before initialization", + ), + "type_records_undefined_defaults_do_not_initialize_fields": ( + """ + config proc() struct { child type = i16 } { + value struct { child type = i16 } := undefined + return value + } + main proc() config().child { return 42 } + """, b"", b"local value was read before initialization", + ), + "type_records_undefined_copy_retains_uninitialized_leaves": ( + """ + Bad proc() type { + value struct { child type, count usize } := undefined + value.child = i16 + copied :: value + value.count = 2 + return [copied.count]copied.child + } + main proc() Bad() { return 42 } + """, b"", b"local value was read before initialization", + ), + "type_records_undefined_reject_partial_argument": ( + """ + Config :: struct { child type, count usize } + Selected proc(value Config) type { return value.child } + Bad proc() type { + value Config := undefined + value.child = i16 + return Selected(value) + } + main proc() Bad() { return 42 } + """, b"", b"local value was read before initialization", + ), + "type_records_undefined_reject_partial_erased_argument": ( + """ + Config :: struct { child type, count usize } + selected proc($value Config) i16 { return 42 } + Bad proc() type { + value Config := undefined + value.child = i16 + return [usize(selected(value))]i16 + } + main proc() Bad() { return [] } + """, b"", b"local value was read before initialization", + ), + "type_records_undefined_reject_readonly_field_update": ( + """ + Bad proc() type { + value struct { child type } :: undefined + value.child = i16 + return i16 + } + main proc() Bad() { return 42 } + """, b"", b"indexed assignment requires a writable array element", + ), + "type_records_undefined_reject_readonly_nested_array_update": ( + """ + Bad proc() type { + value struct { children [1]type } := undefined + value.children[0] = i16 + return i16 + } + main proc() Bad() { return 42 } + """, b"", b"indexed assignment requires a writable array element", + ), + "type_records_undefined_reject_invalid_schema": ( + """ + Bad proc() type { + value struct { child type, count i8 = 128 } := undefined + return i16 + } + main proc() Bad() { return 42 } + """, b"", b"numeric literal is outside its runtime type's range", + ), + "type_records_undefined_construction_is_bounded": ( + """ + Bad proc() type { + value struct { child type, rows [1000000000]mut struct { child type } } := undefined + return i16 + } + main proc() Bad() { return 42 } + """, b"", b"compile-time execution exceeds its recursion or step limit", + ), "type_records_local_schema_defaults_and_owned_copies": ( """ Storage proc() type {