diff --git a/README.md b/README.md index adefb30..0f081ba 100644 --- a/README.md +++ b/README.md @@ -106,13 +106,13 @@ type-producing bodies also iterate materializable fixed arrays and strings with 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 also live inside type/type-array 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, record-valued compile-time procedure inputs/results, 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, partial record initialization, metadata reflection and inline expansion remain open. 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. this does not enable runtime `undefined` storage, partial record-field initialization, uninitialized arrays in ordinary value-returning procedures, or storage-bearing constant materialization such as nonnull pointers/slices. -procedures returning fixed arrays of `type`, such as `types proc() [2]type { return [i16, bool] }`, also execute entirely at compile time. every argument is explicitly supplied, constant, cached and erased; nested/aliased results, empty shapes and dependent `$N` lengths retain exact shape and mutability. type-producing procedures accept ordinary type-array inputs, and runtime generic procedures accept erased `$types [N]type` parameters. direct projections such as `types()[0]` generate signatures, constructors, storage and defaults; explicit `$()` calls inside producers retain owned array values. cache identity uses canonical type leaves without merging distinct nominal types, and copies cannot mutate cached inputs/results. imported diagnostics, shared type-control execution limits and repeatable compilation work through interpreter/native execution. ordinary parameters remain excluded from dependent signatures; use `$N` for lengths. runtime type-array storage, type-valued record procedure boundaries and reflection remain open. +procedures returning fixed arrays or records/tuples containing `type`, such as `types proc() [2]type { return [i16, bool] }` or `config proc() struct { child type, count usize } { return { child = i16, count = 2 } }`, execute entirely at compile time. every argument is explicitly supplied, constant, cached and erased; nested/aliased results, empty shapes and dependent `$N` lengths retain exact shape and mutability. compile-time producers accept ordinary type-array/type-record inputs, and runtime generic procedures accept erased `$` parameters with these shapes. direct projections generate signatures, constructors, storage and defaults; explicit `$()` calls inside producers retain owned values. cache identity uses canonical type leaves without merging distinct nominal types, and copies cannot mutate cached inputs/results. imported diagnostics, shared type-control execution limits and repeatable compilation work through interpreter/native execution. ordinary parameters remain excluded from dependent signatures; use `$N` for lengths. runtime type-array/type-record storage and reflection remain open. optional `if`/`while` captures also execute in type-producing bodies: `if count |length| { return [length]i16 }` generates storage from an optional `usize`. ordinary checked IR validates each executed guard; present payloads become immutable owned constants with exact scalar/aggregate types and strings, and absent optionals skip the body. while guards observe updated locals on each iteration; captures stay body-local across break/continue and early return. generated lengths/defaults, imported producers, cache identity and repeated compilation preserve these values. null data/function pointers materialize as null, but nonnull pointer/slice/callback-bearing payloads retain the constant-materialization escape boundary. diff --git a/TODO.md b/TODO.md index 434f691..771300c 100644 --- a/TODO.md +++ b/TODO.md @@ -35,10 +35,10 @@ 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. -type-array parameters and results cross compile-time procedure boundaries with -owned snapshots, cached nominal identity and erased arguments. -initialized type-valued records/tuples support local schemas/defaults, nested -field/index updates and owned copies/captures inside type-producing bodies. +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 +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. general type-valued execution, metadata reflection and generic library acceptance remain open. @@ -97,7 +97,8 @@ test discovery and FFI remain outside these completed slices. - [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 and local type-valued records are covered below; runtime undefined storage and metadata reflection 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, 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. runtime type-valued records, type-record procedure inputs/results, partial record initialization, metadata reflection and inline expansion 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. - [ ] 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. diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index da2ccf4..c1eb117 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -228,6 +228,29 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { ` if (original.child != i8 or original.counts[1] != 0) return bool ` return struct { values [config.counts[1]]config.child, bias i16 = config.bias } `} + `BoundaryConfig :: struct { child type, counts [2]mut usize, bias i16 = 2 } + `record_config proc() BoundaryConfig { return { child = i16, counts = [1, 2] } } + `change_record_config proc(input BoundaryConfig) BoundaryConfig { + ` copied := input + ` copied.child = u8 + ` copied.counts[1] = 3 + ` return copied + `} + `RecordBoundary proc(input BoundaryConfig) type { + ` changed :: change_record_config(input) + ` if (input.child != i16 or input.counts[1] != 2) return bool + ` if (changed.child != u8 or changed.counts[1] != 3) return bool + ` return struct { values [input.counts[1]]input.child, bias i16 = input.bias } + `} + `RecordAlias :: alias RecordBoundary(record_config()) + `read_record_config proc($input BoundaryConfig, value input.child) input.child { + ` return value + input.bias + `} + `record_pair proc() struct { type, usize } { return {i16, 2} } + `RecordPairElement proc(input struct { type, usize }) type { return field!(input, "0") } + `read_record_pair proc($input struct { type, usize }, value RecordPairElement(input)) RecordPairElement(input) { + ` return value + i16(field!(input, "1")) + `} `read_selected proc($choice Choice, value i16) i16 { ` match choice { ` .payload |detail|: { @@ -280,6 +303,11 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { ` type_record TypeRecord() :: { values = [19, 21] } ` same_type_record TypeRecord() :: type_record ` if (same_type_record.values[0] + same_type_record.values[1] + same_type_record.bias != 42) return 0 + ` record_boundary RecordAlias :: { values = [19, 21] } + ` 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 + ` 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 ` if (payload.values[0] != 40 or payload.values[1] != 2 or payload.text.len != 2) return 0 diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index d7d09bc..001ba0c 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -127,13 +127,12 @@ elaborate proc( } 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) - if ( - (result_type.kind == .builtin and result_type.data.builtin == .type) - or type_array_annotation(&original, resolution.storage.bindings, - ast.function_decl(&original, id).return_type) - ) needed = true + if (type_storage_annotation(&original, resolution.storage.bindings, + ast.function_decl(&original, id).return_type, 0)) needed = true } + if (class == .statement and ast.stmt_kind(&original, id) == .declaration + and type_storage_annotation(&original, resolution.storage.bindings, + ast.statement_decl(&original, id).initializer, 0)) needed = true if (needed) break } if (!needed) return result @@ -169,6 +168,29 @@ elaborate proc( try expand(&state, state.declarations.items[index]) index += 1 } + # metadata definitions have no runtime layout. retain their AST and bindings: + # surviving runtime annotations still resolve the definition and are rejected. + retained := usize(0) + index = 0 + while (index < state.declarations.items.len) { + declaration :: state.declarations.items[index] + index += 1 + if (ast.node_class(&state.tree, declaration) == .statement + and ast.stmt_kind(&state.tree, declaration) == .declaration) { + view :: ast.statement_decl(&state.tree, declaration) + if (view.mutability == .constant and type_record_storage(&state, view.initializer, 0)) { + state.type_execution_remaining = 4096 + state.type_execution_depth += 1 + defer state.type_execution_depth -= 1 + state.evaluation_diagnostic = diagnostics.no_diagnostic + if (try type_record_check(&state, view.initializer)) continue + try reject_evaluation(&state, view.initializer) + } + } + state.declarations.items[retained] = declaration + retained += 1 + } + state.declarations.items = state.declarations.items[..retained] try remember(&state, root) try ast.set_package_declarations(builder, root, state.declarations.items) try refresh(&state) @@ -258,7 +280,7 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { operand :: ast.comptime_operand(&state.tree, id) state.evaluation_diagnostic = diagnostics.no_diagnostic value :: try evaluate(state, operand) - if (value.annotation == no_node or !type_array_annotation(&state.tree, state.bindings.items, value.annotation)) + if (value.annotation == no_node or !type_record_storage(state, value.annotation, 0)) try arraylist.append(&state.rewrites.constraints, operand) if (value.kind == .invalid) { try reject_evaluation(state, id) @@ -517,34 +539,36 @@ is_type proc(state @State, id ast.NodeId) bool { return value.kind == .builtin and value.data.builtin == .type } -# recognize erased result signatures without evaluating dependent lengths or -# elaborating their bodies. aliases retain the same compile-time-only contract. +# recognize erased storage without evaluating dependent lengths or elaborating +# bodies. pointers retain the runtime materialization boundary. @hide:file -type_array_annotation proc(tree @ast.Tree, bindings []sema.Binding, input ast.NodeId) bool { - current := input - array := false - for (0..128) |_| { - resolved :: bindings[usize(current)] - if (resolved.kind == .builtin) return array and resolved.data.builtin == .type - if (resolved.kind == .declaration) { - declaration :: resolved.data.declaration - if (ast.node_class(tree, declaration) != .statement) return false - view :: ast.statement_decl(tree, declaration) - if (view.mutability != .constant) return false - current = view.initializer - } else if (ast.expr_kind(tree, current) == .array_type) { - array = true - current = ast.array_type(tree, current).element - } else if (ast.expr_kind(tree, current) == .alias_expr) { - current = ast.type_operand(tree, current) - } else return false +type_storage_annotation proc(tree @ast.Tree, bindings []sema.Binding, input ast.NodeId, depth usize) bool { + if (depth >= 128 or usize(input) >= bindings.len) return false + resolved :: bindings[usize(input)] + if (resolved.kind == .builtin) return resolved.data.builtin == .type + if (resolved.kind == .declaration) { + declaration :: resolved.data.declaration + if (ast.node_class(tree, declaration) != .statement) return false + view :: ast.statement_decl(tree, declaration) + return (view.mutability == .constant + and type_storage_annotation(tree, bindings, view.initializer, depth + 1)) + } + kind :: ast.expr_kind(tree, input) + if (kind == .alias_expr) + return type_storage_annotation(tree, bindings, ast.type_operand(tree, input), depth + 1) + if (kind == .array_type) + return type_storage_annotation(tree, bindings, ast.array_type(tree, input).element, depth + 1) + if (kind != .record_type) return false + for (0..ast.record_field_count(tree, input)) |i| { + field :: ast.field(tree, ast.record_field(tree, input, i)) + if (field.type != null and type_storage_annotation(tree, bindings, field.type?, depth + 1)) return true } return false } @hide:file compiletime_result proc(state @State, annotation ast.NodeId) bool { - return is_type(state, annotation) or type_array_annotation(&state.tree, state.bindings.items, annotation) + return type_record_storage(state, annotation, 0) } @hide:file @@ -556,7 +580,7 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { state.depth += 1 defer state.depth -= 1 count :: ast.function_param_count(&state.tree, function) - # type and type-array results have no runtime call: every input is specialized. + # type-containing results have no runtime call: every input is specialized. type_result :: compiletime_result(state, ast.function_decl(&state.tree, function).return_type) runtime := usize(0) for (0..count) |i| { @@ -589,7 +613,7 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { type :: try canonical_type(state, annotation) contextual = type != no_node and ast.expr_kind(&state.tree, type) == .fallible_type } - if (contextual or type_array_annotation(&state.tree, state.bindings.items, annotation)) + if (contextual or type_record_storage(state, annotation, 0)) annotation = try argument_annotation(state, function, view.type, arguments) arguments[i] = try evaluate_expected(state, argument, annotation) if (arguments[i].kind == .invalid) { @@ -708,7 +732,7 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { view :: ast.param(&state.tree, parameter) if ( (!type_result and !view.comptime) or is_type(state, view.type) - or type_array_annotation(&state.tree, state.bindings.items, view.type) + or type_record_storage(state, view.type, 0) ) continue original :: ast.call_arg(&state.tree, call, i) constraint :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, original), view.type, original) @@ -799,6 +823,15 @@ instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error { } if (!is_type(state, view.return_type)) { state.instances.items[index].value = returned + constraint_count :: state.rewrites.constraints.items.len + for (constraints_start..constraint_count) |i| { + constraint :: state.rewrites.constraints.items[i] + if (!try expand_type_matches(state, constraint)) { + try reject_type_result(state, call, returned, constraints_start) + return + } + try expand(state, constraint) + } return } result := returned @@ -918,7 +951,12 @@ argument_annotation proc( for (0..ast.function_param_count(&state.tree, function)) |i| { if (arguments[i].kind == .invalid) continue parameter :: ast.function_param(&state.tree, function, i) - value :: try materialize(state, arguments[i]) + value := try materialize(state, arguments[i]) + if (value.kind != .type and value.annotation != no_node) { + value.node = try ast.add_typed_constant(state.builder, + ast.node_span(&state.tree, value.node), value.annotation, value.node) + try refresh(state) + } for (0..mapping.len) |n| { resolved :: binding(state, ast.NodeId(n)) if ( @@ -939,7 +977,10 @@ argument_annotation proc( @hide:file evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Error { - if (type_array_annotation(&state.tree, state.bindings.items, annotation)) { + if (!is_type(state, annotation) and type_record_storage(state, annotation, 0)) { + if (state.type_execution_depth == 0) state.type_execution_remaining = 4096 + state.type_execution_depth += 1 + defer state.type_execution_depth -= 1 constant :: try type_execution_value(state, id, annotation) if (constant == no_node or !try type_array_initialized(state, constant, id)) return Value{} value :: ast.typed_constant(&state.tree, constant) @@ -1062,7 +1103,7 @@ constant_equal proc(state @mut State, left, right ast.NodeId) bool ! Error { if (kind == .typed_constant) { a :: ast.typed_constant(&state.tree, left) b :: ast.typed_constant(&state.tree, right) - return try same_type(state, a.type, b.type) and try constant_equal(state, a.value, b.value) + return try same_type_context(state, a.type, b.type, true) and try constant_equal(state, a.value, b.value) } if (kind == .array_literal) { count :: ast.array_element_count(&state.tree, left) @@ -1096,7 +1137,7 @@ constant_hash proc(state @mut State, id ast.NodeId) usize ! Error { if (kind == .literal_void) return hash if (kind == .typed_constant) { value :: ast.typed_constant(&state.tree, id) - return mix(mix(hash, try type_hash(state, value.type)), try constant_hash(state, value.value)) + return mix(mix(hash, try type_hash_context(state, value.type, true)), try constant_hash(state, value.value)) } if (kind == .array_literal) { result := hash @@ -1482,14 +1523,15 @@ 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) - record :: (type_record_storage(state, array.type, 0) - and ast.expr_kind(&state.tree, array.type) == .record_type) + 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) if (!record and !try execution_array_storage(state, array.type)) return true selected := no_node if (record) { if (kind != .selection) return true member :: ast.selection_expr(&state.tree, id).member - slot :: type_record_slot(state, array.type, member) + slot :: type_record_slot(state, type, member) if (slot == no_index) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .unknown_field, ast.node_span(&state.tree, member)) @@ -1515,7 +1557,7 @@ type_array_projection proc(state @mut State, id ast.NodeId) bool ! Error { } } else { member :: ast.selection_expr(&state.tree, id).member - if (!type_array_annotation(&state.tree, state.bindings.items, array.type) + if (!type_record_storage(state, array.type, 0) and !mem.eql(ast.literal_text(&state.tree, member), "len")) return true if (!mem.eql(ast.literal_text(&state.tree, member), "len")) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, @@ -1551,31 +1593,9 @@ execution_array_storage proc(state @mut State, annotation ast.NodeId) bool ! Err return current != no_node and ast.expr_kind(&state.tree, current) == .array_type } -# inspect storage syntax without elaboration: nominal results must enter the cache -# before any recursive field calls expand. pointers retain the materialization boundary. @hide:file type_record_storage proc(state @State, annotation ast.NodeId, depth usize) bool { - if (depth >= 128) return false - resolved :: binding(state, annotation) - if (resolved.kind == .builtin) return resolved.data.builtin == .type - if (resolved.kind == .declaration) { - declaration :: resolved.data.declaration - if (ast.node_class(&state.tree, declaration) != .statement) return false - view :: ast.statement_decl(&state.tree, declaration) - return (view.mutability == .constant - and type_record_storage(state, view.initializer, depth + 1)) - } - kind :: ast.expr_kind(&state.tree, annotation) - if (kind == .alias_expr) - return type_record_storage(state, ast.type_operand(&state.tree, annotation), depth + 1) - if (kind == .array_type) - return type_record_storage(state, ast.array_type(&state.tree, annotation).element, depth + 1) - if (kind != .record_type) return false - for (0..ast.record_field_count(&state.tree, annotation)) |i| { - field :: ast.field(&state.tree, ast.record_field(&state.tree, annotation, i)) - if (field.type != null and type_record_storage(state, field.type?, depth + 1)) return true - } - return false + return type_storage_annotation(&state.tree, state.bindings.items, annotation, depth) } @hide:file @@ -1607,8 +1627,19 @@ type_record_check proc(state @mut State, annotation ast.NodeId) bool ! Error { if (is_type(state, annotation)) return true type :: try canonical_type(state, annotation) if (type == no_node) return false - if (ast.expr_kind(&state.tree, type) == .array_type) - return try type_record_check(state, ast.array_type(&state.tree, type).element) + if (ast.expr_kind(&state.tree, type) == .array_type) { + if (!try type_step(state, type)) return false + descriptor :: ast.array_type(&state.tree, type) + if (descriptor.count) |count| { + value :: try evaluate(state, count) + if (value.kind != .integer or value.negative) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .invalid_array_length, ast.node_span(&state.tree, count)) + return false + } + } + return try type_record_check(state, descriptor.element) + } for (0..ast.record_field_count(&state.tree, type)) |i| { field_id :: ast.record_field(&state.tree, type, i) field :: ast.field(&state.tree, field_id) @@ -1853,6 +1884,7 @@ type_array_value proc(state @mut State, expression, annotation ast.NodeId) ast.N } child = descriptor.element } + if (type_record_storage(state, child, 0) and !try type_record_check(state, child)) return no_node } span :: ast.node_span(&state.tree, expression) length :: try ast.add_integer(state.builder, span, u64(count)) @@ -1983,6 +2015,18 @@ type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.No state.evaluation_diagnostic = resolved.data.diagnostic return no_node } + if (!is_type(state, annotation) and type_record_storage(state, annotation, 0) + and 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 or !try type_step(state, expression)) return no_node + if (declaration.type != null and !try same_type(state, annotation, declaration.type?)) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .type_mismatch, ast.node_span(&state.tree, expression)) + return no_node + } + return try type_execution_value(state, declaration.initializer, annotation) + } if (try execution_array_storage(state, annotation)) { value :: try type_array_value(state, expression, annotation) if (value != no_node and !type_record_storage(state, annotation, 0)) { @@ -2019,7 +2063,12 @@ type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.No try arraylist.append(&state.rewrites.constraints, constraint) return concrete.node } - value :: try evaluate_ir(state, expression, annotation) + # check the leaf as an assignment, not as the evaluation wrapper's return: + # exact value-type failures belong to the source leaf and use type_mismatch. + contextual :: try ast.add_typed_constant(state.builder, + ast.node_span(&state.tree, expression), annotation, expression) + try refresh(state) + value :: try evaluate_ir(state, contextual, annotation) if (value.kind == .invalid) return no_node concrete :: try materialize(state, value) result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, expression), annotation, concrete.node) @@ -2708,8 +2757,30 @@ canonical_type proc(state @mut State, id ast.NodeId) ast.NodeId ! Error { return if (value.kind == .type) value.node else no_node } +# metadata storage compares anonymous annotation shapes, including ordinary nested +# fields. named definitions and nominal type results retain their original identity. +@hide:file +# ponytail: scan declaration syntax for nominal identity; index record origins if metadata-heavy builds make this hot. +structural_type_record proc(state @State, id ast.NodeId) bool { + for (state.instances.items) |instance| { + if (instance.type == id) return false + } + for (0..ast.node_count(&state.tree)) |i| { + declaration :: ast.NodeId(i) + if (ast.node_class(&state.tree, declaration) != .statement + or ast.stmt_kind(&state.tree, declaration) != .declaration) continue + if (ast.statement_decl(&state.tree, declaration).initializer == id) return false + } + return true +} + @hide:file same_type proc(state @mut State, left, right ast.NodeId) bool ! Error { + return try same_type_context(state, left, right, false) +} + +@hide:file +same_type_context proc(state @mut State, left, right ast.NodeId, metadata bool) bool ! Error { if (is_type(state, left) or is_type(state, right)) return is_type(state, left) and is_type(state, right) a :: try canonical_type(state, left) @@ -2724,10 +2795,24 @@ same_type proc(state @mut State, left, right ast.NodeId) bool ! Error { return try type_subset(state, a, b) and try type_subset(state, b, a) if (kind != ast.expr_kind(&state.tree, b)) return false match kind { + .record_type: { + if (!metadata and (!type_record_storage(state, a, 0) or !type_record_storage(state, b, 0))) return false + if (!structural_type_record(state, a) or !structural_type_record(state, b)) return false + count :: ast.record_field_count(&state.tree, a) + if (count != ast.record_field_count(&state.tree, b)) return false + for (0..count) |i| { + p :: ast.field(&state.tree, ast.record_field(&state.tree, a, i)) + q :: ast.field(&state.tree, ast.record_field(&state.tree, b, i)) + if ((p.name == null) != (q.name == null)) return false + if (p.name != null and ast.name_id(&state.tree, p.name?) != ast.name_id(&state.tree, q.name?)) return false + if (p.type == null or q.type == null or !try same_type_context(state, p.type?, q.type?, true)) return false + } + return true + } .pointer_type: { p :: ast.pointer_type(&state.tree, a) q :: ast.pointer_type(&state.tree, b) - return p.mutable == q.mutable and p.many == q.many and try same_type(state, p.element, q.element) + return p.mutable == q.mutable and p.many == q.many and try same_type_context(state, p.element, q.element, metadata) } .array_type: { p :: ast.array_type(&state.tree, a) @@ -2735,7 +2820,7 @@ same_type proc(state @mut State, left, right ast.NodeId) bool ! Error { if (p.mutable != q.mutable or p.count == null or q.count == null) return false pv :: try evaluate(state, p.count?) qv :: try evaluate(state, q.count?) - return try value_equal(state, pv, qv) and try same_type(state, p.element, q.element) + return try value_equal(state, pv, qv) and try same_type_context(state, p.element, q.element, metadata) } .slice_type: { p :: ast.slice_type(&state.tree, a) @@ -2744,22 +2829,22 @@ same_type proc(state @mut State, left, right ast.NodeId) bool ! Error { if (p.sentinel) |sentinel| { if (!try value_equal(state, try evaluate(state, sentinel), try evaluate(state, q.sentinel?))) return false } - return try same_type(state, p.element, q.element) + return try same_type_context(state, p.element, q.element, metadata) } - .optional_type: return try same_type(state, ast.type_operand(&state.tree, a), ast.type_operand(&state.tree, b)) + .optional_type: return try same_type_context(state, ast.type_operand(&state.tree, a), ast.type_operand(&state.tree, b), metadata) .fallible_type: { p :: ast.binary_expr(&state.tree, a) q :: ast.binary_expr(&state.tree, b) - return try same_type(state, p.left, q.left) and try same_type(state, p.right, q.right) + return try same_type_context(state, p.left, q.left, metadata) and try same_type_context(state, p.right, q.right, metadata) } .function_type: { count :: ast.function_param_count(&state.tree, a) if (count != ast.function_param_count(&state.tree, b)) return false - if (!try same_type(state, ast.callable_result(&state.tree, a), ast.callable_result(&state.tree, b))) return false + if (!try same_type_context(state, ast.callable_result(&state.tree, a), ast.callable_result(&state.tree, b), metadata)) return false for (0..count) |i| { p :: ast.param(&state.tree, ast.function_param(&state.tree, a, i)).type q :: ast.param(&state.tree, ast.function_param(&state.tree, b, i)).type - if (!try same_type(state, p, q)) return false + if (!try same_type_context(state, p, q, metadata)) return false } return true } @@ -2831,6 +2916,11 @@ string_hash proc(seed usize, text []u8) usize { @hide:file type_hash proc(state @mut State, id ast.NodeId) usize ! Error { + return try type_hash_context(state, id, false) +} + +@hide:file +type_hash_context proc(state @mut State, id ast.NodeId, metadata bool) usize ! Error { node :: try canonical_type(state, id) if (node == no_node) return 0 resolved :: binding(state, node) @@ -2838,44 +2928,54 @@ type_hash proc(state @mut State, id ast.NodeId) usize ! Error { kind :: ast.expr_kind(&state.tree, node) hash := enum_index(kind) match kind { + .record_type: { + if ((!metadata and !type_record_storage(state, node, 0)) + or !structural_type_record(state, node)) return mix(hash, usize(node)) + for (0..ast.record_field_count(&state.tree, node)) |i| { + field :: ast.field(&state.tree, ast.record_field(&state.tree, node, i)) + if (field.name) |name| hash = mix(hash, usize(ast.name_id(&state.tree, name))) + if (field.type) |annotation| hash = mix(hash, try type_hash_context(state, annotation, true)) + } + return hash + } .pointer_type: { view :: ast.pointer_type(&state.tree, node) mutable :: if (view.mutable) usize(1) else usize(0) many :: if (view.many) usize(1) else usize(0) hash = mix(hash, mutable) hash = mix(hash, many) - return mix(hash, try type_hash(state, view.element)) + return mix(hash, try type_hash_context(state, view.element, metadata)) } .array_type: { view :: ast.array_type(&state.tree, node) mutable :: if (view.mutable) usize(1) else usize(0) hash = mix(hash, mutable) if (view.count) |count| hash = mix(hash, try value_hash(state, try evaluate(state, count))) - return mix(hash, try type_hash(state, view.element)) + return mix(hash, try type_hash_context(state, view.element, metadata)) } .slice_type: { view :: ast.slice_type(&state.tree, node) mutable :: if (view.mutable) usize(1) else usize(0) hash = mix(hash, mutable) if (view.sentinel) |sentinel| hash = mix(hash, try value_hash(state, try evaluate(state, sentinel))) - return mix(hash, try type_hash(state, view.element)) + return mix(hash, try type_hash_context(state, view.element, metadata)) } - .optional_type: return mix(hash, try type_hash(state, ast.type_operand(&state.tree, node))) + .optional_type: return mix(hash, try type_hash_context(state, ast.type_operand(&state.tree, node), metadata)) .sum_type: { view :: ast.binary_expr(&state.tree, node) # set fingerprints must ignore ordering, grouping and repeated members. # colliding fingerprints still use full type equality in the cache chain. - return try type_hash(state, view.left) | try type_hash(state, view.right) + return try type_hash_context(state, view.left, metadata) | try type_hash_context(state, view.right, metadata) } .fallible_type: { view :: ast.binary_expr(&state.tree, node) - return mix(mix(hash, try type_hash(state, view.left)), try type_hash(state, view.right)) + return mix(mix(hash, try type_hash_context(state, view.left, metadata)), try type_hash_context(state, view.right, metadata)) } .function_type: { - hash = mix(hash, try type_hash(state, ast.callable_result(&state.tree, node))) + hash = mix(hash, try type_hash_context(state, ast.callable_result(&state.tree, node), metadata)) for (0..ast.function_param_count(&state.tree, node)) |i| { parameter :: ast.function_param(&state.tree, node, i) - hash = mix(hash, try type_hash(state, ast.param(&state.tree, parameter).type)) + hash = mix(hash, try type_hash_context(state, ast.param(&state.tree, parameter).type, metadata)) } return hash } diff --git a/tests/native.py b/tests/native.py index 545b9bd..c066f29 100644 --- a/tests/native.py +++ b/tests/native.py @@ -79,6 +79,180 @@ CASES = { } """, b"42\n", None, ), + "type_record_procedure_owned_results_inputs_and_cache_identity": ( + """ + Config :: struct { child type, counts [2]mut usize, bias i16 = 2 } + config proc() Config { return { child = i16, counts = [1, 2] } } + change proc(input Config) Config { + copied := input + copied.child = u8 + copied.counts[1] = 3 + return copied + } + Storage proc(input Config) type { + saved :: input + changed :: change(input) + if (input.child != i16 or saved.counts[1] != 2) return bool + if (changed.child != u8 or changed.counts[1] != 3) return bool + return struct { values [input.counts[1]]input.child, bias i16 = input.bias } + } + Selected :: alias Storage(config()) + main proc() i16 { + value Selected :: { values = [19, 21] } + same Storage({ child = i16, counts = [1, 2] }) :: value + return same.values[0] + same.values[1] + same.bias + } + """, b"42\n", None, + ), + "type_record_procedure_nested_array_snapshots_and_nominal_leaves": ( + """ + Item :: struct { child type, counts [1]mut usize } + Config :: struct { items [2]mut Item } + config proc() Config { + Child :: struct { value i16 } + return { items = [{ child = Child, counts = [1] }, { child = Child, counts = [2] }] } + } + change proc(input Config) Config { + copied := input + copied.items[0].child = u8 + copied.items[1].counts[0] = 3 + return copied + } + Storage proc(input Config) type { + changed :: change(input) + if (input.items[0].child != input.items[1].child) return bool + if (input.items[0].child == changed.items[0].child) return bool + if (input.items[1].counts[0] != 2 or changed.items[1].counts[0] != 3) return bool + return struct { first input.items[0].child, second input.items[1].child } + } + main proc() i16 { + value Storage(config()) :: { first = { value = 19 }, second = { value = 23 } } + same Storage(config()) :: value + copied := same.first + copied = same.second + return same.first.value + copied.value + } + """, b"42\n", None, + ), + "type_record_procedure_tuple_results_and_erased_runtime_inputs": ( + """ + Pair :: struct { type, usize } + pair proc() Pair { return {i16, 2} } + copy proc(input Pair) Pair { return input } + Element proc(input Pair) type { return field!(input, "0") } + add proc($input Pair, value Element(input)) Element(input) { + return value + i16(field!(input, "1")) + } + forward proc(value i16) i16 { return add(copy(pair()), value) } + main proc() i16 { + if (add({i16, usize(1)}, 40) != 41) return 0 + return forward(40) + } + """, b"42\n", None, + ), + "type_record_procedure_dependent_annotations_and_erased_cache_values": ( + """ + config proc($T type, value T) struct { child type, bias T, detail struct { bias T } } { + return { child = T, bias = value, detail = { bias = value } } + } + Storage proc($T type, input struct { child type, bias T, detail struct { bias T } }) type { + return struct { value input.child, bias T = input.detail.bias } + } + add proc($input struct { child type, bias i16, detail struct { bias i16 } }, value input.child) input.child { + return value + input.detail.bias + } + main proc() i16 { + value Storage(i16, config(i16, 2)) :: { value = 40 } + same Storage(i16, { child = i16, bias = 2, detail = { bias = 2 } }) :: value + if (add(config(i16, 1), same.value) != 41) return 0 + return add(config(i16, 2), same.value) + } + """, b"42\n", None, + ), + "type_record_procedure_result_rejects_unknown_field": ( + """ + config proc() struct { child type } { return { child = i16, missing = 1 } } + main proc() config().child { return 42 } + """, b"", b"no such field", + ), + "type_record_procedure_result_rejects_duplicate_field": ( + """ + config proc() struct { child type } { return { child = i16, child = i8 } } + main proc() config().child { return 42 } + """, b"", b"more than once", + ), + "type_record_procedure_argument_rejects_missing_field": ( + """ + Selected proc(input struct { child type, count usize }) type { return input.child } + main proc() Selected({ child = i16 }) { return 42 } + """, b"", b"missing a required field", + ), + "type_record_procedure_result_rejects_non_type_leaf": ( + """ + config proc() struct { child type } { return { child = 42 } } + main proc() config().child { return 42 } + """, b"", b"incompatible value type", + ), + "type_record_procedure_argument_rejects_exact_value_leaf_mismatch": ( + """ + Selected proc(input struct { child type, count usize }) type { return input.child } + Bad proc() type { + wrong u8 :: 2 + return Selected({ child = i16, count = wrong }) + } + main proc() Bad() { return 42 } + """, b"", b"incompatible value type", + ), + "type_record_procedure_erased_argument_rejects_exact_type_leaf_mismatch": ( + """ + add proc($input struct { child type }, value input.child) input.child { return value } + main proc() i16 { + wrong u8 :: 42 + return add({ child = i16 }, wrong) + } + """, b"", b"incompatible value type", + ), + "type_record_procedure_result_preserves_value_leaf_overflow": ( + """ + config proc() struct { child type, count i8 } { return { child = i16, count = 128 } } + main proc() config().child { return 42 } + """, b"", b"outside its runtime type", + ), + "type_record_procedure_rejects_runtime_dependency": ( + """ + config proc(wide bool) struct { child type } { + if (wide) return { child = i16 } + return { child = i8 } + } + read proc(wide bool) i16 { + value config(wide).child :: 42 + return i16(value) + } + main proc() i16 { return read(true) } + """, b"", b"generic argument must be a compile-time value", + ), + "type_record_procedure_nested_execution_shares_step_limit": ( + """ + config proc(count usize) struct { child type } { + for (0..count) |_| { } + return { child = i16 } + } + Selected proc() type { + for (0..usize(100)) |index| { input :: config(index + 100) } + return i16 + } + main proc() Selected() { return 42 } + """, b"", b"compile-time execution exceeds its recursion or step limit", + ), + "type_record_procedure_results_do_not_enable_runtime_storage": ( + """ + config proc() struct { child type, count i16 } { return { child = i16, count = 42 } } + main proc() i16 { + input :: config() + return input.count + } + """, b"", b"", + ), "type_records_reject_unknown_initializer_field": ( """ Bad proc() type { @@ -5902,7 +6076,47 @@ def check_specialization_packages(temporary, executable): interpreted = run(str(HONEY), "run", str(app)) assert interpreted.stderr.startswith(f"{sizing.resolve()}:3:".encode()), outcome(interpreted) assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted) - return 18 + sizing.write_text(textwrap.dedent(""" + Config :: struct { child type, counts [2]mut usize, bias i16 = 2 } + record_config proc() Config { return { child = i16, counts = [1, 2] } } + FromRecord proc(input Config) type { + return struct { values [input.counts[1]]input.child, bias i16 = input.bias } + } + add proc($input Config, value input.child) input.child { return value + input.bias } + NominalConfig :: struct { child type } + nominal_config proc() NominalConfig { + return { child = struct { value i16 } } + } + FromNominal proc(input NominalConfig) type { return struct { value input.child } } + """).strip() + "\n") + source.write_text(textwrap.dedent(""" + lib :: import "../library" + Imported :: alias lib.FromRecord(lib.record_config()) + main proc() i16 { + value Imported :: { values = [19, 21] } + same lib.FromRecord({ child = i16, counts = [1, 2] }) :: value + nominal lib.FromNominal(lib.nominal_config()) :: { value = { value = 40 } } + same_nominal lib.FromNominal(lib.nominal_config()) :: nominal + if (same_nominal.value.value != 40) return 0 + return lib.add(lib.record_config(), same.values[0] + same.values[1]) + } + """).strip() + "\n") + check(app, executable, (b"42\n", None)) + sizing.write_text(textwrap.dedent(""" + Config :: struct { child type, rows [1]mut [1]mut type } + record_config proc() Config { return { child = i16, rows = [[i16]] } } + bad_config proc() Config { + copied := record_config() + copied.child = copied.rows[0][1] + return copied + } + """).strip() + "\n") + source.write_text('lib :: import "../library"\nmain proc() lib.bad_config().child { return 42 }\n') + check(app, executable, (b"", b"index out of bounds")) + interpreted = run(str(HONEY), "run", str(app)) + assert interpreted.stderr.startswith(f"{sizing.resolve()}:5:".encode()), outcome(interpreted) + assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted) + return 20 def check_dependency_provenance(temporary, executable):