diff --git a/README.md b/README.md index b0faef6..7fda328 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ ordinary procedures also accept `undefined` as the initializer of a mutable loca 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. -ordinary parameters containing `type` also make a procedure compile-time-only, even when its result is a runtime value. for example, `count proc(types []type) usize { return types.len }` computes `count([i16, bool])` without runtime metadata. every input must be explicitly supplied and constant; use `$` parameters when later signature annotations depend on them. helpers can return owned scalar, aggregate and string values, recurse over metadata and use checked `try`/`catch` with scoped cleanup. plain and fallible `void` support bare returns and successful fallthrough. ordinary runtime dependencies and incomplete metadata arguments remain diagnostic; mutable metadata borrowing and general type-valued execution remain open. +ordinary parameters containing `type` also make a procedure compile-time-only, even when its result is a runtime value. for example, `count proc(types []type) usize { return types.len }` computes `count([i16, bool])` without runtime metadata. every input must be explicitly supplied and constant; use `$` parameters when later signature annotations depend on them. helpers can return owned scalar, aggregate and string values, recurse over metadata and use checked `try`/`catch` with scoped cleanup. plain and fallible `void` support bare returns and successful fallthrough. ordinary runtime dependencies and incomplete owned metadata arguments remain diagnostic; mutable slice parameters may borrow partially initialized backing to fill it. local metadata storage uses owned compile-time snapshots alongside ordinary runtime parameters and effects. explicit annotations (`T type := i16`), inferred mutable type bindings (`T := i16`), metadata arrays/records/tuples, helper results and checked projections are erased before runtime checking. sequential updates, saved copies, static branches, bounded construction loops and scoped metadata cleanup select concrete runtime storage without emitting type values. runtime branches and loops may create independent local metadata; they cannot conditionally update an enclosing compile-time snapshot. runtime optional/collection captures and ordinary effects keep their execution semantics. @@ -145,9 +145,11 @@ compute proc(value i16) i16 { } ``` -procedures whose metadata depends on ordinary parameters retain fully constant-input execution; every input to those calls remains explicitly supplied and constant. metadata parameters/results also retain their existing compile-time-only contract. nested storage, partial initialization, checked updates, recursion and fallible cleanup reuse type-control execution and its shared 4096-step bound. inferred arrays retain readonly elements unless their annotation permits updates; mutable bindings still permit whole-value replacement. ordinary local aliases and scalar metadata projections keep their runtime-call contract. mutable metadata borrowing remains open. +procedures whose metadata depends on ordinary parameters retain fully constant-input execution; every input to those calls remains explicitly supplied and constant. metadata parameters/results also retain their existing compile-time-only contract. nested storage, partial initialization, checked updates, recursion and fallible cleanup reuse type-control execution and its shared 4096-step bound. inferred arrays retain readonly elements unless their annotation permits updates; mutable bindings still permit whole-value replacement. ordinary local aliases and scalar metadata projections keep their runtime-call contract. -readonly type-valued slices (`[]type` and slices of metadata records/arrays) also cross compile-time procedure boundaries as owned snapshots. contextual array literals, including `[]`, construct them; explicit views of immutable readonly arrays and existing metadata slices support checked `usize` bounds, empty end views, indexing and `.len`. nested slices, ordinary/inline by-value iteration, erased `$` inputs and optional/fallible channels retain exact nominal leaves and content-based specialization identity. snapshots generate runtime types/defaults, but metadata itself has no runtime storage. mutable slice types, sentinels and views of mutable array places remain rejected rather than silently losing borrowing semantics; copy an array into an immutable binding before taking an owned view. construction shares the type-control step bound, and uninitialized leaves cannot cross procedure boundaries. +readonly type-valued slices (`[]type` and slices of metadata records/arrays) cross compile-time procedure boundaries as owned snapshots. contextual array literals, including `[]`, construct them. local array/slice views retain live aliases with checked `usize` bounds, frozen subview offsets, indexing and `.len`; later backing writes or whole-array replacement remain visible, while by-value array/record copies stay independent. nested slices, ordinary/inline by-value iteration, erased `$` inputs and optional/fallible channels retain exact nominal leaves and content-based specialization identity. + +mutable metadata slice views (`[]mut type` and slices of metadata records/arrays) borrow writable array places, including nested fields/indices. immutable slice bindings may update their mutable elements; readonly backing and enclosing places never gain write access. compile-time helpers share aliases and commit writes to caller storage, including initialization and scoped `defer`/`errdefer` cleanup. calls with mutable borrowed arguments execute afresh rather than reusing cached effects; readonly-only inputs snapshot at call entry and retain content-based nominal caching. readonly results snapshot before return cleanup, and owned arguments/results require initialized leaves. expired local views, bounds errors and runtime-controlled metadata mutation diagnose. mutable results, metadata sentinels, data-pointer borrowing and pointer captures remain unsupported; metadata has no runtime storage. 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; callbacks retain source declaration identity, while nonnull data pointers and unsupported slice types retain the constant-materialization escape boundary. diff --git a/TODO.md b/TODO.md index f95a150..b865e86 100644 --- a/TODO.md +++ b/TODO.md @@ -54,8 +54,9 @@ tagged type-valued unions own active payloads across compile-time calls and eras arguments; checked payload updates preserve saved copies, and whole-value assignments initialize undefined storage or select another variant. readonly type-valued slices own nested metadata leaves across compile-time calls, -checked immutable-array/slice views, by-value iteration and erased arguments; -mutable metadata borrowing remains open. +by-value iteration and erased arguments. local metadata slice views borrow live +array places; mutable views and helper parameters update shared backing, while +readonly results snapshot before cleanup. metadata data-pointer borrowing remains open. ordinary metadata-input procedures also compute owned runtime values, including reflected field names/counts, recursive helpers and fallible cleanup. explicit and inferred local metadata storage also coexist with ordinary runtime @@ -71,7 +72,7 @@ with static tags and exact by-value payloads, preserving runtime dispatch and sc `typeinfo!` exposes owned compile-time array/distinct/record/enum metadata, including readonly field collections, exact nominal types and reflected generated storage/defaults. the existing `meta.EnumFieldStruct` and `enummap.EnumMap`, `init` and `get` work; -mutable metadata borrowing, broader materialization and full library acceptance remain open. +metadata data-pointer borrowing, broader materialization and full library acceptance remain open. `some!` preserves nested optional presence; ordinary procedures initialize concrete runtime `undefined` locals and compute fully initialized materializable constants. the existing `ArrayList` uses nested generic inference, file-local helpers and @@ -184,7 +185,8 @@ test discovery and FFI remain outside these completed slices. - [x] inferred metadata-local execution checkpoint: inferred mutable type bindings and metadata array/record/tuple locals support constant-input execution without metadata inputs, results or local annotations. literal/helper results, checked field/index/optional projections, yielding blocks and conditional/fallible selection preserve owned copies, nominal leaves, whole-value replacement, loops, recursion and scoped cleanup through interpreter/native execution. inferred array elements remain readonly; ordinary local aliases and scalar metadata projections retain runtime parameters. ordinary fallible success/error snapshots use checked channel discrimination rather than assuming every payload carries a nominal type. imported private types, source/session disposal and repeat compilation preserve ownership. independent runtime parameters and metadata-local entry points are supported below. - [x] formatted-IO acceptance checkpoint: existing `std/io.print` parses constant formats and expands tuple fields through interpreter/native execution without replacement or adapted library code. escaped braces, all integer bases/boundaries, booleans, strings/arrays, sparse/aliased enums, nested scalar distinct values, runtime arguments and computed formats retain exact output; short writes, zero progress, invalid callback counts and write errors retain checked propagation. malformed formats, wrong tuple/count/type inputs and unsupported specifiers diagnose. native `f64` default/decimal/scientific formatting uses imported variadic `snprintf` at `-O0`/`-O2`, while interpreter FFI remains a demanded trap. character literals preserve Unicode code points and byte escapes; newline-separated conditional branches and package-local visibility match Bro. partial compile-time prefixes infer remaining arguments without overriding explicit values; pure type builtins and effect-free bound type helpers specialize guards. `rem!` preserves exact numeric types, signed-minimum remainder and checked zero divisors. immutable C byte-pointer coercions, borrowed pointer-to-array slices and optional presence retain mutability/nominal checks. inline iterables propagate constant demand through original parser calls, metadata indexing and borrowed initialization; guarded errors follow reached control flow, owned snapshots commit local mutation, contextual constant numeric locals preserve rigid boundaries, and runtime bodies/effects remain runtime. general type-valued execution, broader materialization and broader `meta` acceptance remain open. - [x] mixed metadata-local execution checkpoint: ordinary and generic procedures retain runtime scalar/pointer inputs and effects alongside erased explicit/inferred type, array and record locals. owned snapshots preserve sequential field/index/whole updates, saved copies, generated runtime storage, constant branches, bounded construction loops, static exits and scoped metadata cleanup. runtime branches/loops create independent metadata locals; optional/collection captures keep their runtime bindings. constant tagged/optional metadata selection specializes only the selected body. runtime-dependent metadata still requires fully constant-input execution, and runtime paths cannot mutate enclosing metadata snapshots. interpreter/native `-O0`/`-O2`, imported private nominal leaves, source/session disposal and repeat compilation retain ownership and diagnostics. mutable metadata borrowing and broader constant materialization remain open. - - [ ] remaining gate: mutable metadata borrowing, broader constant materialization (including address-bearing untagged unions, nonnull data pointers and mutable slices), plus remaining existing-library acceptance for broader `meta` operations. broader imported C-binding support may interleave with later gates. + - [x] mutable metadata slice-borrowing checkpoint: local type-valued array/slice views retain live aliases, frozen subview bounds and exact nominal leaves. mutable views update nested metadata fields/indices through immutable slice bindings, share helper parameters, initialize undefined backing and run scoped `defer`/`errdefer`; ordinary array/record copies remain independent. readonly results snapshot before return cleanup, readonly-only calls retain content-based specialization identity, and mutable borrowed calls never reuse cached effects. mixed runtime parameters/effects remain runtime; runtime-controlled metadata mutation, readonly reversal, expired places, incomplete owned snapshots and bounds/index failures diagnose. imported private leaves and repeat compilation/source disposal retain ownership through interpreter/native execution. metadata data pointers, pointer captures, mutable result materialization and sentinels remain open. + - [ ] remaining gate: metadata data-pointer borrowing and pointer captures, broader constant materialization (including address-bearing untagged unions, nonnull data pointers and mutable slices), plus remaining existing-library acceptance for broader `meta` operations. 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. - [ ] 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/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index 41e115a..98c6f85 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -1318,6 +1318,7 @@ readonly_type_slices_recompile_owned_private_nominal_payloads test { ` saved :: $(lib.make()) ` if (lib.Box(saved) != lib.Box(lib.make())) return bool ` if (lib.Box(saved) != lib.Box(lib.literal())) return bool + ` if (lib.Box(saved) != lib.Box(lib.borrowed())) return bool ` Selected :: lib.types()[0] ` if (Selected == Child or saved[0].child != Selected) return bool ` return lib.Box(saved) @@ -1357,6 +1358,16 @@ readonly_type_slices_recompile_owned_private_nominal_payloads test { ` ] ` return saved `} + `fill proc(values []mut Config) void { + ` for values |_, index| { values[index] = { child = Child, count = 2 } } + `} + `borrowed proc() []Config { + ` rows [2]mut Config := undefined + ` view :: rows[..] + ` fill(view) + ` defer view[0].child = bool + ` return view + `} `Box proc(input []Config) type { ` return struct { values [input[0].count]input[0].child, bias i16 = i16(input[1].count) } `} diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index a664e36..98bc32e 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -108,6 +108,9 @@ State :: struct { demanded_functions []mut u8 execution_functions []mut u8 execution_visited arraylist.ArrayList(ast.NodeId) + metadata_views arraylist.ArrayList(MetadataView) + metadata_view_indices hashmap.AutoHashMap(ast.NodeId, usize) + metadata_runtime_depth usize = 0 } #! elaborates templates into cached concrete declarations in the caller-owned `builder` arena. @@ -185,6 +188,8 @@ elaborate proc( demanded_functions = demanded_functions, execution_functions = execution_functions, execution_visited = arraylist.init(allocator), + metadata_views = arraylist.init(allocator), + metadata_view_indices = hashmap.init(allocator), } defer arraylist.deinit(&state.bindings) defer arraylist.deinit(&state.declarations) @@ -192,6 +197,8 @@ elaborate proc( defer hashmap.deinit(&state.cache) defer hashmap.deinit(&state.capture_types) defer arraylist.deinit(&state.execution_visited) + defer arraylist.deinit(&state.metadata_views) + defer hashmap.deinit(&state.metadata_view_indices) defer { for (state.instances.items) |instance| mem.free(allocator, instance.arguments) arraylist.deinit(&state.instances) @@ -1759,7 +1766,7 @@ expand_inline_match proc(state @mut State, id, subject_type ast.NodeId) void ! E mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)), first_owned = ast.NodeId(0), result_type = no_node, } - defer arraylist.deinit(&execution.locals) + defer type_execution_deinit(state, &execution) defer mem.free(state.allocator, execution.mapping) span :: ast.node_span(&state.tree, inline_arm) static_capture :: if (tagged) arm.tag_capture else arm.capture @@ -2583,6 +2590,18 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void 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 ((view.comptime or !metadata_result) and arguments[i].kind == .aggregate + and metadata_has_view(state, arguments[i].node)) { + wrapped :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, argument), + arguments[i].annotation, arguments[i].node) + try refresh(state) + owned :: try metadata_own(state, wrapped, argument) + if (owned == no_node or !try type_array_initialized(state, owned, argument)) { + try reject_evaluation(state, call) + return + } + arguments[i].node = ast.typed_constant(&state.tree, owned).value + } if (arguments[i].kind == .invalid) { try reject_evaluation(state, call) return @@ -2648,13 +2667,41 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void annotation :: try argument_annotation(state, function, view.type, arguments) try expand_expected(state, argument, annotation) } + borrowed := false + for arguments |argument| { + if (argument.kind != .invalid and metadata_has_view(state, argument.node)) borrowed = true + } + if (borrowed) { + readonly := true + for arguments |argument| { + if (argument.kind != .invalid and !try metadata_readonly_views(state, argument.node, argument.annotation)) + readonly = false + } + if (readonly) { + for arguments |@argument| { + if (argument.kind == .invalid or !metadata_has_view(state, argument.node)) continue + wrapped :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, argument.node), + argument.annotation, argument.node) + try refresh(state) + owned :: try metadata_own(state, wrapped, call) + if (owned == no_node or !try type_array_initialized(state, owned, call)) { + try reject_evaluation(state, call) + return + } + argument.node = ast.typed_constant(&state.tree, owned).value + } + borrowed = false + } + } demand_key :: if (demanded) usize(1) else usize(0) key := mix(usize(function), demand_key) for arguments |argument| { if (argument.kind != .invalid) key = mix(key, try value_hash(state, argument)) } head := no_index - if (hashmap.get(&state.cache, key)) |found| head = found + if (!borrowed) { + if (hashmap.get(&state.cache, key)) |found| head = found + } current := head instance := no_index while (current != no_index) { @@ -2688,7 +2735,9 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void return err } # cache heads are replaced without reallocating a second entry for the same hash. - if (head != no_index) { + if (borrowed) { + # live places carry effects and must never reuse a cached execution. + } else if (head != no_index) { for (state.cache.entries) |@entry| { if (entry.hash != 0 and entry.key == key) { entry.value = instance @@ -3640,6 +3689,7 @@ TypeExecution :: struct { mapping []mut ast.NodeId first_owned ast.NodeId returned ast.NodeId = no_node + view_identity ast.NodeId = no_node result_type ast.NodeId failed bool = false yielded ast.NodeId = no_node @@ -3649,6 +3699,8 @@ TypeExecution :: struct { cleanup_depth usize = 0 cleanup_loop_depth usize = 0 evaluate_values bool = false + # dispatch snapshots must retain match shape until ordinary arm validation. + resolve_views bool = true pending_flow TypeFlow = .next loop_depth usize = 0 inline_body ast.NodeId = no_node @@ -3659,6 +3711,442 @@ TypeExecution :: struct { residual_flow TypeFlow = .next } +# unique empty AST literals identify views in metadata_view_indices. owner teardown +# invalidates handles before its stack frame disappears; only owned leaves reach IR. +@hide:file +MetadataView :: struct { + owner @mut TypeExecution + owner_identity ast.NodeId = no_node + place ast.NodeId + start usize + count usize + active bool = true +} + +@hide:file +metadata_view_index proc(state @State, id ast.NodeId) usize { + if (id == no_node) return no_index + if (ast.node_class(&state.tree, id) != .expression) return no_index + marker :: if (ast.expr_kind(&state.tree, id) == .typed_constant) + ast.typed_constant(&state.tree, id).value else id + if (hashmap.get(&state.metadata_view_indices, marker)) |index| return index + return no_index +} + +@hide:file +metadata_has_view proc(state @State, id ast.NodeId) bool { + if (id == no_node or state.metadata_views.items.len == 0) return false + if (metadata_view_index(state, id) != no_index) return true + for (0..ast.child_count(&state.tree, id)) |i| { + if (metadata_has_view(state, ast.child(&state.tree, id, i))) return true + } + return false +} + +@hide:file +metadata_uses_views proc(state @State, execution @TypeExecution, id ast.NodeId) bool { + if (type_call_metadata_borrows(state, execution, id)) return true + for (execution.locals.items) |local| { + if (metadata_has_view(state, local.value) and metadata_reads(state, id, local.declaration, 0)) + return true + } + return false +} + +@hide:file +type_execution_deinit proc(state @mut State, execution @mut TypeExecution) void { + if (execution.view_identity != no_node) { + for (state.metadata_views.items) |@view| { + if (view.owner_identity == execution.view_identity) view.active = false + } + } + arraylist.deinit(&execution.locals) +} + +# handles refer to live type-control places, never to temporary interpreter storage. +@hide:file +metadata_view_read proc(state @mut State, index usize, at ast.NodeId) ast.NodeId ! Error { + view :: state.metadata_views.items[index] + if (view.active) { + root :: assignment_root(state, view.place) + for (view.owner.locals.items) |local| { + if (local.declaration != root) continue + result :: try type_collection_read(state, view.owner, view.place) + if (!try expand_type_matches(state, result)) return no_node + return result + } + } + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .comptime_value_escape, ast.node_span(&state.tree, at)) + return no_node +} + +@hide:file +metadata_view_snapshot proc(state @mut State, value, at ast.NodeId) ast.NodeId ! Error { + index :: metadata_view_index(state, value) + if (index == no_index) return value + view :: state.metadata_views.items[index] + annotation :: ast.typed_constant(&state.tree, value).type + span :: ast.node_span(&state.tree, at) + source :: try metadata_view_read(state, index, at) + if (source == no_node) return no_node + array :: ast.typed_constant(&state.tree, source).value + count :: ast.array_element_count(&state.tree, array) + if (view.start > count or view.count > count - view.start) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .invalid_slice_bounds, ast.node_span(&state.tree, at)) + return no_node + } + elements := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&elements) + for (view.start..(view.start + view.count)) |i| { + if (!try type_step(state, at)) return no_node + try arraylist.append(&elements, ast.array_element(&state.tree, array, i)) + } + literal :: try ast.add_array_literal(state.builder, span, elements.items) + result :: try ast.add_typed_constant(state.builder, span, annotation, literal) + try refresh(state) + return result +} + +# readonly boundary values own their leaves before cleanup invalidates local places. +@hide:file +metadata_own proc(state @mut State, input, at ast.NodeId) ast.NodeId ! Error { + value := input + if (!metadata_has_view(state, value)) return value + if (metadata_view_index(state, value) != no_index) { + if (ast.expr_kind(&state.tree, value) != .typed_constant) return no_node + type :: try canonical_type(state, ast.typed_constant(&state.tree, value).type) + if (ast.slice_type(&state.tree, type).mutable) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .comptime_value_escape, ast.node_span(&state.tree, at)) + return no_node + } + value = try metadata_view_snapshot(state, value, at) + if (value == no_node) return no_node + if (!metadata_has_view(state, value)) return value + } + mapping :: try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)) + defer mem.free(state.allocator, mapping) + mem.set(mapping, no_node) + for (0..ast.child_count(&state.tree, value)) |i| { + child :: ast.child(&state.tree, value, i) + owned :: if (metadata_has_view(state, child)) try metadata_own(state, child, at) else child + if (owned == no_node) return no_node + mapping[usize(child)] = owned + } + result :: try ast.clone_subtree(state.builder, value, mapping) + try refresh(state) + return result +} + +@hide:file +metadata_readonly_views proc(state @mut State, id, annotation ast.NodeId) bool ! Error { + if (!metadata_has_view(state, id)) return true + if (metadata_view_index(state, id) != no_index) { + type := annotation + if (ast.node_class(&state.tree, id) == .expression + and ast.expr_kind(&state.tree, id) == .typed_constant) + type = ast.typed_constant(&state.tree, id).type + type = try canonical_type(state, type) + return (type != no_node and ast.expr_kind(&state.tree, type) == .slice_type + and !ast.slice_type(&state.tree, type).mutable) + } + for (0..ast.child_count(&state.tree, id)) |i| { + if (!try metadata_readonly_views(state, ast.child(&state.tree, id, i), no_node)) return false + } + return true +} + +@hide:file +MetadataPlace :: struct { + owner @mut TypeExecution + place ast.NodeId + mutable bool +} + +@hide:file +metadata_place proc( + state @mut State, execution @mut TypeExecution, place ast.NodeId, +) MetadataPlace ! Error { + kind :: ast.expr_kind(&state.tree, place) + root :: assignment_root(state, place) + if (kind != .selection and kind != .index) { + return MetadataPlace{ owner = execution, place = place, + mutable = (root != no_node and ast.node_class(&state.tree, root) == .statement + and ast.statement_decl(&state.tree, root).mutability == .variable) } + } + parent :: if (kind == .index) ast.index_expr(&state.tree, place).base + else ast.selection_expr(&state.tree, place).base + base :: try type_collection_read(state, execution, parent) + if (!try expand_type_matches(state, base)) + return MetadataPlace{ owner = execution, place = no_node, mutable = false } + borrowed :: metadata_view_index(state, base) + if (kind == .index and borrowed != no_index) { + view :: state.metadata_views.items[borrowed] + if (try metadata_view_read(state, borrowed, place) == no_node) + return MetadataPlace{ owner = execution, place = no_node, mutable = false } + offset :: try type_array_index(state, ast.index_expr(&state.tree, place).index, view.count, false) + if (offset == no_index) + return MetadataPlace{ owner = execution, place = no_node, mutable = false } + span :: ast.node_span(&state.tree, place) + type :: try canonical_type(state, ast.typed_constant(&state.tree, base).type) + mutable :: ast.slice_type(&state.tree, type).mutable + index :: try ast.add_integer(state.builder, span, u64(view.start + offset)) + result :: try ast.add_index(state.builder, span, view.place, index) + try refresh(state) + return MetadataPlace{ owner = view.owner, place = result, mutable = mutable } + } + result := try metadata_place(state, execution, parent) + if (result.place == no_node) return result + if (kind == .index) { + type :: try canonical_type(state, try expression_type(state, base, 0)) + if (type == no_node or ast.expr_kind(&state.tree, type) != .array_type) { + result.mutable = false + } else result.mutable = result.mutable and ast.array_type(&state.tree, type).mutable + result.place = try ast.add_index(state.builder, ast.node_span(&state.tree, place), + result.place, ast.index_expr(&state.tree, place).index) + } else result.place = try ast.add_selection(state.builder, ast.node_span(&state.tree, place), + result.place, ast.selection_expr(&state.tree, place).member) + try refresh(state) + return result +} + +@hide:file +metadata_view_create proc( + state @mut State, execution @mut TypeExecution, id ast.NodeId, +) ast.NodeId ! Error { + view :: ast.slice_expr(&state.tree, id) + normalized :: try type_assignment_target(state, execution, view.base) + if (normalized == no_node) return no_node + place :: try type_collection_place(state, execution, normalized) + source := no_node + if (place != no_node) source = try type_collection_read(state, execution, place) + else source = try type_snapshot(state, execution, normalized) + if (source == no_node or !try expand_type_matches(state, source)) return no_node + annotation :: try expression_type(state, source, 0) + if (!type_record_storage(state, annotation, 0)) return no_node + if (view.sentinel != null) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .invalid_operand, ast.node_span(&state.tree, id)) + return no_node + } + type :: try canonical_type(state, annotation) + kind :: ast.expr_kind(&state.tree, type) + if (kind != .array_type and kind != .slice_type) return no_node + previous :: metadata_view_index(state, source) + if (place == no_node and previous == no_index) return no_node + base := MetadataView{ owner = execution, place = place, start = 0, count = 0 } + mutable := false + element := no_node + if (previous != no_index) { + base = state.metadata_views.items[previous] + mutable = ast.slice_type(&state.tree, type).mutable + element = ast.slice_type(&state.tree, type).element + } else if (kind == .array_type) { + descriptor :: ast.array_type(&state.tree, type) + storage :: try metadata_place(state, execution, place) + if (storage.place == no_node) return no_node + base.owner = storage.owner + base.place = storage.place + mutable = descriptor.mutable and storage.mutable + element = descriptor.element + base.count = ast.array_element_count(&state.tree, ast.typed_constant(&state.tree, source).value) + } else return no_node + start := usize(0) + end := base.count + if (view.start) |bound| { + snapshot :: try type_snapshot(state, execution, bound) + if (snapshot == no_node) return no_node + start = try type_array_index(state, snapshot, base.count, true) + } + if (start == no_index) return no_node + if (view.end) |bound| { + snapshot :: try type_snapshot(state, execution, bound) + if (snapshot == no_node) return no_node + end = try type_array_index(state, snapshot, base.count, true) + } + if (end == no_index) return no_node + if (start > end) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .invalid_slice_bounds, ast.node_span(&state.tree, id)) + return no_node + } + span :: ast.node_span(&state.tree, id) + slice :: try ast.add_slice_type(state.builder, span, element, mutable, null) + empty [0]ast.NodeId :: [] + marker :: try ast.add_array_literal(state.builder, span, empty[..]) + result :: try ast.add_typed_constant(state.builder, span, slice, marker) + try refresh(state) + if (base.owner.view_identity == no_node) base.owner.view_identity = marker + if (previous == no_index) base.owner_identity = base.owner.view_identity + base.start += start + base.count = end - start + index :: state.metadata_views.items.len + try arraylist.append(&state.metadata_views, base) + hashmap.put(&state.metadata_view_indices, marker, index) catch |err| { + match err { + .key_exists: unreachable + else: return err + } + } + return result +} + +@hide:file +metadata_resolve_views proc(state @mut State, id ast.NodeId) bool ! Error { + if (!metadata_has_view(state, id)) return true + if (ast.node_class(&state.tree, id) == .expression + and ast.expr_kind(&state.tree, id) == .typed_constant) return true + if (ast.node_class(&state.tree, id) == .expression + and ast.expr_kind(&state.tree, id) == .binary) { + binary :: ast.binary_expr(&state.tree, id) + if (binary.operator == .logical_and or binary.operator == .logical_or) { + if (!try metadata_resolve_views(state, binary.left)) return false + if (branch_constant(state, binary.left, 0)) { + value :: try evaluate_ir(state, binary.left, try builtin(state, id, .bool)) + if (value.kind != .boolean) return false + if (value.boolean == (binary.operator == .logical_or)) { + # inspect the skipped call's signature without executing its body. + right :: storage_expression_annotation(&state.tree, state.bindings.items, binary.right, 0) + if (!try same_type(state, right, try builtin(state, id, .bool))) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .invalid_operand, ast.node_span(&state.tree, binary.right)) + return false + } + result :: try ast.add_boolean(state.builder, ast.node_span(&state.tree, id), value.boolean) + try replace(state, id, result) + try refresh(state) + return true + } + return try metadata_resolve_views(state, binary.right) + } + state.metadata_runtime_depth += 1 + defer state.metadata_runtime_depth -= 1 + return try metadata_resolve_views(state, binary.right) + } + } + if (ast.node_class(&state.tree, id) == .expression + and ast.expr_kind(&state.tree, id) == .match_expr) { + subject :: ast.match_expr(&state.tree, id).subject + if (!try metadata_resolve_views(state, subject)) return false + if (try specialize_match(state, id, no_node, no_node)) { + if (binding(state, id).kind == .invalid) return false + return try metadata_resolve_views(state, id) + } + state.metadata_runtime_depth += 1 + defer state.metadata_runtime_depth -= 1 + for (0..ast.match_arm_count(&state.tree, id)) |i| { + if (!try metadata_resolve_views(state, ast.match_arm(&state.tree, id, i))) return false + } + return true + } + for (0..ast.child_count(&state.tree, id)) |i| { + if (!try metadata_resolve_views(state, ast.child(&state.tree, id, i))) return false + } + if (ast.node_class(&state.tree, id) != .expression) return true + if (ast.expr_kind(&state.tree, id) == .call) { + try expand(state, id) + if (binding(state, id).kind == .invalid) { + state.evaluation_diagnostic = binding(state, id).data.diagnostic + return false + } + } + return try type_array_projection(state, id) +} + +@hide:file +metadata_rebase_place proc( + state @mut State, execution @mut TypeExecution, target, first, replacement ast.NodeId, +) ast.NodeId ! Error { + if (target == first) return replacement + kind :: ast.expr_kind(&state.tree, target) + base :: if (kind == .index) ast.index_expr(&state.tree, target).base + else ast.selection_expr(&state.tree, target).base + rebased :: try metadata_rebase_place(state, execution, base, first, replacement) + result := no_node + span :: ast.node_span(&state.tree, target) + if (kind == .selection) + result = try ast.add_selection(state.builder, span, rebased, ast.selection_expr(&state.tree, target).member) + else { + index :: try type_snapshot(state, execution, ast.index_expr(&state.tree, target).index) + if (index == no_node) return no_node + frozen :: try type_execution_value(state, index, try builtin(state, index, .usize)) + if (frozen == no_node) return no_node + result = try ast.add_index(state.builder, span, rebased, frozen) + } + try refresh(state) + return result +} + +@hide:file +type_place_root proc(state @State, id ast.NodeId) ast.NodeId { + kind :: ast.expr_kind(&state.tree, id) + if (kind == .selection) return type_place_root(state, ast.selection_expr(&state.tree, id).base) + if (kind == .index) return type_place_root(state, ast.index_expr(&state.tree, id).base) + return id +} + +@hide:file +metadata_assignment proc( + state @mut State, execution @mut TypeExecution, target, input ast.NodeId, +) bool ! Error { + first := target + while (true) { + kind :: ast.expr_kind(&state.tree, first) + if (kind != .selection and kind != .index) return false + base :: if (kind == .index) ast.index_expr(&state.tree, first).base + else ast.selection_expr(&state.tree, first).base + if (kind == .index) { + value :: try type_snapshot(state, execution, base) + if (value == no_node or !try expand_type_matches(state, value)) return true + index :: metadata_view_index(state, value) + if (index != no_index) { + view :: state.metadata_views.items[index] + type :: try canonical_type(state, ast.typed_constant(&state.tree, value).type) + if (!ast.slice_type(&state.tree, type).mutable) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .invalid_index_assignment, ast.node_span(&state.tree, target)) + return true + } + if (try metadata_view_read(state, index, target) == no_node) return true + if (state.metadata_runtime_depth > 0) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .comptime_argument_required, ast.node_span(&state.tree, target)) + return true + } + snapshot :: try type_snapshot(state, execution, ast.index_expr(&state.tree, first).index) + if (snapshot == no_node) return true + offset :: try type_array_index(state, snapshot, view.count, false) + if (offset == no_index) return true + span :: ast.node_span(&state.tree, first) + frozen :: try ast.add_integer(state.builder, span, u64(view.start + offset)) + place :: try ast.add_index(state.builder, span, view.place, frozen) + try refresh(state) + rebased :: try metadata_rebase_place(state, execution, target, first, place) + if (rebased == no_node) return true + expression :: try type_snapshot(state, execution, input) + if (expression == no_node or !try expand_type_matches(state, expression)) return true + root :: assignment_root(state, view.place) + for (view.owner.locals.items) |local, i| { + if (local.declaration != root) continue + if (view.owner.residual != null and i < view.owner.residual_boundary) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .comptime_argument_required, ast.node_span(&state.tree, target)) + return true + } + updated :: try type_array_update(state, view.owner, rebased, + type_place_root(state, view.place), local.value, expression) + if (updated != no_node) view.owner.locals.items[i].value = updated + return true + } + return true + } + } + first = base + } +} + @hide:file TypeFlow :: enum { next, returned, yielded, break, continue, invalid } @@ -3900,7 +4388,7 @@ expand_runtime_metadata proc(state @mut State, function ast.NodeId) void ! Error mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)), first_owned = ast.NodeId(0), result_type = view.return_type, residual_body = view.body, } - defer arraylist.deinit(&execution.locals) + defer type_execution_deinit(state, &execution) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) body := try residual_block(state, &execution, view.body, 0) @@ -3933,7 +4421,7 @@ return_type_value proc(state @mut State, block, first_owned, result_type ast.Nod result_type = result_type, evaluate_values = true, } - defer arraylist.deinit(&execution.locals) + defer type_execution_deinit(state, &execution) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) flow :: try execute_type_block(state, &execution, block) @@ -3967,7 +4455,7 @@ evaluate_type_value_block proc(state @mut State, expression, annotation ast.Node result_type = no_node, evaluate_values = true, } - defer arraylist.deinit(&execution.locals) + defer type_execution_deinit(state, &execution) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) return try execute_type_value_block(state, &execution, ast.block_expr(&state.tree, expression), annotation) @@ -4079,42 +4567,76 @@ prepare_type_snapshot proc(state @mut State, execution @mut TypeExecution, id as return } } + if (execution.residual != null and !execution.evaluate_values + and ast.node_class(&state.tree, id) == .expression and metadata_uses_views(state, execution, id)) { + kind :: ast.expr_kind(&state.tree, id) + if (kind == .binary) { + binary :: ast.binary_expr(&state.tree, id) + if (binary.operator == .logical_and or binary.operator == .logical_or) { + left :: try type_snapshot(state, execution, binary.left) + if (left == no_node) return + known :: branch_constant(state, left, 0) + if (known) { + value :: try evaluate_ir(state, left, try builtin(state, id, .bool)) + if (value.kind != .boolean) { + execution.pending_flow = .invalid + return + } + if (value.boolean == (binary.operator == .logical_or)) { + right :: storage_expression_annotation(&state.tree, state.bindings.items, binary.right, 0) + if (!try same_type(state, right, try builtin(state, id, .bool))) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .invalid_operand, ast.node_span(&state.tree, binary.right)) + execution.pending_flow = .invalid + return + } + execution.mapping[usize(id)] = try ast.add_boolean(state.builder, + ast.node_span(&state.tree, id), value.boolean) + try refresh(state) + return + } + } + if (!known) state.metadata_runtime_depth += 1 + defer if (!known) state.metadata_runtime_depth -= 1 + right :: try type_snapshot(state, execution, binary.right) + execution.mapping[usize(binary.left)] = left + execution.mapping[usize(binary.right)] = right + if (right == no_node) execution.pending_flow = .invalid + return + } + } + if (kind == .match_expr) { + subject :: try type_snapshot(state, execution, ast.match_expr(&state.tree, id).subject) + if (subject == no_node) return + if (!branch_constant(state, subject, 0)) { + state.metadata_runtime_depth += 1 + defer state.metadata_runtime_depth -= 1 + for (0..ast.child_count(&state.tree, id)) |i| + try prepare_type_snapshot(state, execution, ast.child(&state.tree, id, i)) + return + } + } + } + if ((execution.evaluate_values or execution.residual != null) + and ast.node_class(&state.tree, id) == .expression and ast.expr_kind(&state.tree, id) == .slice) { + result :: try metadata_view_create(state, execution, id) + if (result != no_node) { + execution.mapping[usize(id)] = result + return + } + if (state.evaluation_diagnostic != diagnostics.no_diagnostic) { + execution.pending_flow = .invalid + return + } + } if (execution.evaluate_values and ast.node_class(&state.tree, id) == .expression) { kind :: ast.expr_kind(&state.tree, id) - if (kind == .call and type_call_borrows(state, execution, id)) { + if (kind == .call and type_call_borrows(state, execution, id) + and !type_call_metadata_borrows(state, execution, id)) { execution.mapping[usize(id)] = try type_borrow_call(state, execution, id) if (execution.mapping[usize(id)] == no_node) execution.pending_flow = .invalid return } - if (kind == .slice) { - base :: ast.slice_expr(&state.tree, id).base - root := base - while (true) { - root_kind :: ast.expr_kind(&state.tree, root) - if (root_kind == .selection) root = ast.selection_expr(&state.tree, root).base - else if (root_kind == .index) root = ast.index_expr(&state.tree, root).base - else if (root_kind == .unary and ast.unary_expr(&state.tree, root).operator == .unwrap) - root = ast.unary_expr(&state.tree, root).operand - else if (root_kind == .builtin_call - and mem.eql(ast.literal_text(&state.tree, ast.builtin_call(&state.tree, root).name), "field") - and ast.call_arg_count(&state.tree, root) == 2) - root = ast.call_arg(&state.tree, root, 0) - else break - } - place :: binding(state, root) - if (place.kind == .declaration - and ast.node_class(&state.tree, place.data.declaration) == .statement - and ast.statement_decl(&state.tree, place.data.declaration).mutability == .variable) { - annotation :: try expression_type(state, base, 0) - if (type_record_storage(state, annotation, 0) - and ast.expr_kind(&state.tree, annotation) == .array_type) { - state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, - .comptime_value_escape, ast.node_span(&state.tree, id)) - execution.pending_flow = .invalid - return - } - } - } if (kind == .block_expr) { execution.mapping[usize(id)] = try execute_type_value_block(state, execution, ast.block_expr(&state.tree, id), no_node) @@ -4141,6 +4663,19 @@ prepare_type_snapshot proc(state @mut State, execution @mut TypeExecution, id as } right := no_node if (value.boolean == (view.operator == .logical_or)) { + if (metadata_uses_views(state, execution, view.right)) { + annotation :: storage_expression_annotation(&state.tree, state.bindings.items, view.right, 0) + if (!try same_type(state, annotation, boolean)) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .invalid_operand, ast.node_span(&state.tree, view.right)) + execution.pending_flow = .invalid + return + } + execution.mapping[usize(id)] = try ast.add_boolean(state.builder, + ast.node_span(&state.tree, id), value.boolean) + try refresh(state) + return + } saved :: execution.evaluate_values execution.evaluate_values = false right = try type_snapshot(state, execution, view.right) @@ -4239,6 +4774,7 @@ type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId result :: try ast.clone_subtree(state.builder, id, execution.mapping) try refresh(state) try bind_type_snapshot(state, execution, id, start) + if (execution.resolve_views and !try metadata_resolve_views(state, result)) return no_node return result } @@ -4354,7 +4890,7 @@ specialize_type_catch proc(state @mut State, id, annotation, expected ast.NodeId first_owned = ast.NodeId(0), result_type = no_node, } - defer arraylist.deinit(&execution.locals) + defer type_execution_deinit(state, &execution) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) if (view.capture) |capture| { @@ -4461,6 +4997,9 @@ type_dispatch_snapshot proc(state @mut State, execution @mut TypeExecution, id a try expand_inline_match(state, id, subject_type) if (ast.expr_kind(&state.tree, id) != .match_expr) return no_node saved :: execution.evaluate_values + saved_views :: execution.resolve_views + execution.resolve_views = false + defer execution.resolve_views = saved_views execution.evaluate_values = false defer execution.evaluate_values = saved snapshot :: try type_snapshot(state, execution, id) @@ -4634,7 +5173,7 @@ expand_inline_for proc(state @mut State, id ast.NodeId) void ! Error { first_owned = ast.NodeId(0), result_type = no_node, inline_body = loop.body, inline_blocks = &blocks, } - defer arraylist.deinit(&execution.locals) + defer type_execution_deinit(state, &execution) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) for (0..usize(2)) |i| { @@ -4825,7 +5364,7 @@ type_match_body proc(state @mut State, id ast.NodeId, arm ast.MatchArmView) ast. first_owned = ast.NodeId(0), result_type = no_node, } - defer arraylist.deinit(&execution.locals) + defer type_execution_deinit(state, &execution) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) captured :: if (is_type(state, snapshot.type)) snapshot.value else value @@ -4923,7 +5462,12 @@ type_array_projection proc(state @mut State, id ast.NodeId) bool ! Error { return false } if (ast.expr_kind(&state.tree, base) != .typed_constant) return true - array :: ast.typed_constant(&state.tree, base) + array := ast.typed_constant(&state.tree, base) + if (metadata_view_index(state, base) != no_index) { + snapshot :: try metadata_view_snapshot(state, base, id) + if (snapshot == no_node) return false + array = ast.typed_constant(&state.tree, snapshot) + } type :: try canonical_type(state, array.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))) @@ -5184,8 +5728,8 @@ type_record_check proc(state @mut State, annotation ast.NodeId) bool ! Error { } if (ast.expr_kind(&state.tree, type) == .slice_type) { descriptor :: ast.slice_type(&state.tree, type) - if (descriptor.mutable or descriptor.sentinel != null) { - code diagnostics.Code :: if (descriptor.mutable) .comptime_value_escape else .invalid_operand + if (descriptor.sentinel != null) { + code diagnostics.Code :: .invalid_operand state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, ast.node_span(&state.tree, type)) return false } @@ -5555,8 +6099,7 @@ type_array_value proc(state @mut State, expression, annotation ast.NodeId) ast.N return result } -# readonly metadata views own their leaves. mutable backing needs borrowing semantics, -# not a snapshot that silently stops observing later element writes. +# local handles retain borrowing; owned readonly literals cross execution boundaries. @hide:file type_slice_value proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { if (state.type_execution_depth == 0) state.type_execution_remaining = 4096 @@ -5575,8 +6118,27 @@ type_slice_value proc(state @mut State, expression, annotation ast.NodeId) ast.N if (kind == .typed_constant) { actual :: ast.typed_constant(&state.tree, expression) if (try same_type(state, type, actual.type)) return expression + index :: metadata_view_index(state, expression) + if (index != no_index) { + actual_type :: try canonical_type(state, actual.type) + if (ast.expr_kind(&state.tree, actual_type) == .slice_type) { + source :: ast.slice_type(&state.tree, actual_type) + if ((!descriptor.mutable or source.mutable) + and try same_type(state, descriptor.element, source.element)) { + result :: try ast.add_typed_constant(state.builder, span, type, actual.value) + try refresh(state) + return result + } + } + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, span) + return no_node + } } source := expression + if (descriptor.mutable) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span) + return no_node + } start := usize(0) end := no_index if (kind == .slice) { @@ -6114,6 +6676,15 @@ type_call_borrows proc(state @State, execution @TypeExecution, call ast.NodeId) return false } +@hide:file +type_call_metadata_borrows proc(state @State, execution @TypeExecution, call ast.NodeId) bool { + for (execution.locals.items) |local| { + if (type_record_storage(state, local.annotation, 0) + and type_borrowed_place(state, call, local.declaration, false)) return true + } + return false +} + @hide:file TypeBorrow :: struct { index usize, previous ast.NodeId, temporary ast.NodeId } @@ -6431,6 +7002,8 @@ execute_type_statement_body proc(state @mut State, execution @mut TypeExecution, expression :: try type_snapshot_expected(state, execution, value, execution.result_type) if (expression == no_node) return .invalid execution.returned = try type_execution_value(state, expression, execution.result_type) + if (execution.returned != no_node) + execution.returned = try metadata_own(state, execution.returned, value) if (execution.returned != no_node and !try type_array_initialized(state, execution.returned, value)) return .invalid if (execution.returned != no_node) { @@ -6491,6 +7064,11 @@ execute_type_statement_body proc(state @mut State, execution @mut TypeExecution, mem.set(execution.mapping[previous..], no_node) return try execute_type_statement(state, execution, normalized) } + if (try metadata_assignment(state, execution, target, assignment.value)) { + if (state.evaluation_diagnostic != diagnostics.no_diagnostic or execution.pending_flow != .next) + return .invalid + return .next + } root := assignment.target indexed := true while (true) { @@ -6768,7 +7346,10 @@ execute_type_collection proc(state @mut State, execution @mut TypeExecution, sta collection :: try type_execution_value(state, snapshot, annotation) if (collection == no_node) return .invalid count := Value{} - if tuple { + borrowed :: metadata_view_index(state, collection) + if (borrowed != no_index) { + count = Value{ kind = .integer, magnitude = u64(state.metadata_views.items[borrowed].count) } + } else if tuple { count = Value{ kind = .integer, magnitude = u64(ast.record_field_count(&state.tree, annotation)) } } else if (try execution_array_storage(state, annotation) or try metadata_sequence(state, annotation)) { count = Value{ kind = .integer, diff --git a/tests/__pycache__/native.cpython-313.pyc b/tests/__pycache__/native.cpython-313.pyc index 358120f..2b4b30b 100644 Binary files a/tests/__pycache__/native.cpython-313.pyc and b/tests/__pycache__/native.cpython-313.pyc differ diff --git a/tests/native.py b/tests/native.py index cd48688..a698da1 100644 --- a/tests/native.py +++ b/tests/native.py @@ -3375,31 +3375,301 @@ CASES = { main proc() Storage() { return [40, 2] } """, b"[40, 2]\n", None, ), + "metadata_slices_live_aliases_saved_bounds_rebinding_and_array_copies": ( + """ + Storage proc() type { + backing [3]mut type := [i8, bool, u8] + copied :: backing + start := usize(1) + finish := usize(2) + view []mut type := backing[..] + saved :: view[start..finish] + shared :: saved + start = 0 + finish = 3 + saved[0] = i16 + if (backing[1] != i16 or copied[1] != bool) return bool + backing = [bool, i8, bool] + if (shared.len != 1 or shared[0] != i8) return bool + other [1]mut type := [u8] + view = other[..] + view[0] = i16 + shared[0] = i16 + if (other[0] != i16 or backing[1] != i16) return bool + return shared[0] + } + main proc() Storage() { return 42 } + """, b"42\n", None, + ), + "metadata_record_slices_borrow_helpers_nested_writes_and_uncached_effects": ( + """ + Info :: struct { child type, nested struct { children [2]mut type, count usize } } + update proc(items []mut Info) void { + items[0].child = i16 + items[0].nested.children[1] = i16 + items[0].nested.count += 1 + } + read proc(items []Info) usize { return items[0].nested.count } + Storage proc() type { + backing [1]mut Info := [{ child = bool, nested = { children = [i8, bool], count = 0 } }] + copied :: backing + view :: backing[..] + update(view) + if (read(view) != 1) return bool + backing[0] = copied[0] + update(view) + if (read(view) != 1) return bool + update(view) + if (read(view) != 2 or copied[0].nested.count != 0) return bool + if (view[0].child != i16 or view[0].nested.children[1] != i16) return bool + if (copied[0].child != bool or copied[0].nested.children[1] != bool) return bool + return [read(view)]view[0].child + } + main proc() Storage() { return [40, 2] } + """, b"[40, 2]\n", None, + ), + "metadata_slices_borrow_cleanup_initialization_and_return_snapshot": ( + """ + Error :: enum { failed } + initialize proc(items []mut type) void { + defer items[1] = i16 + items[0] = bool + } + fail proc(items []mut type) void ! Error { + errdefer items[0] = i16 + return .failed + } + types proc() []type { + backing [2]mut type := undefined + view :: backing[..] + initialize(view) + fail(view) catch {} + defer view[0] = bool + return view + } + main proc() [2]types()[0] { return [40, 2] } + """, b"[40, 2]\n", None, + ), + "metadata_slices_reject_immutable_backing_write": ( + """ + Storage proc() type { + backing [1]mut type :: [i16] + view :: backing[..] + view[0] = bool + return view[0] + } + main proc() Storage() { return 42 } + """, b"", b"writable", + ), + "metadata_slices_reject_readonly_array_to_mutable_view": ( + """ + Storage proc() type { + backing [1]type := [i16] + view []mut type :: backing[..] + view[0] = bool + return view[0] + } + main proc() Storage() { return 42 } + """, b"", b"incompatible", + ), + "metadata_slices_reject_incomplete_borrow_snapshot": ( + """ + types proc() []type { + backing [2]mut type := undefined + view :: backing[..] + view[0] = i16 + return view + } + main proc() types()[0] { return 42 } + """, b"", b"read before initialization", + ), + "metadata_slices_reject_runtime_branch_borrow_mutation": ( + """ + update proc(items []mut type) void { items[0] = i16 } + compute proc(take bool) usize { + backing [1]mut type := [i8] + view :: backing[..] + if (take) update(view) + return sizeof!(view[0]) + } + main proc() usize { return compute(true) } + """, b"", b"compile-time value", + ), + "metadata_slices_reject_mutable_view_index_type": ( + """ + Storage proc() type { + backing [1]mut type := [i16] + view :: backing[..] + view[u8(0)] = bool + return i16 + } + main proc() Storage() { return 42 } + """, b"", b"usize", + ), + "metadata_slices_reject_mutable_subview_write_at_length": ( + """ + Storage proc() type { + backing [2]mut type := [i16, bool] + view :: backing[1..2] + view[1] = i8 + return i16 + } + main proc() Storage() { return 42 } + """, b"", b"index out of bounds", + ), + "metadata_slices_nested_places_iteration_optional_aliases_and_empty_shapes": ( + """ + Info :: struct { children [2]mut type } + Choice :: union(enum) { types []mut type, empty void } + Storage proc() type { + first [1]mut Info := [{ children = [i8, bool] }] + second [1]mut Info := [{ children = [bool, u8] }] + rows := first[..] + children :: field!(rows[0], "children")[..] + rows = second[..] + selected ?[]mut type :: some!(children) + if selected |chosen| { chosen[0] = i16 } + choice Choice :: .types {children} + match choice { .types |captured|: captured[1] = i16, .empty: {} } + if (children[1] != i16) return bool + for children |T, index| { + if (index == 0) children[1] = T + if (index == 1 and T != i16) return bool + } + if (second[0].children[0] != bool) return bool + empty [0]mut type := [] + view :: empty[..] + for view |T| { return T } + if (view[..].len != 0) return bool + return first[0].children[1] + } + main proc() Storage() { return 42 } + """, b"42\n", None, + ), + "metadata_slices_local_fallible_captures_preserve_aliases": ( + """ + Failure :: struct { types []mut type } + Storage proc() type { + backing [1]mut type := [i8] + value void ! Failure :: Failure { types = backing[..] } + value catch |failure| { failure.types[0] = i16 } + return backing[0] + } + main proc() Storage() { return 42 } + """, b"42\n", None, + ), + "metadata_slices_reject_readonly_enclosing_array_place": ( + """ + Info :: struct { children [1]mut type } + Storage proc() type { + rows [1]Info := [{ children = [i8] }] + children :: rows[0].children[..] + children[0] = i16 + return rows[0].children[0] + } + main proc() Storage() { return 42 } + """, b"", b"writable", + ), + "metadata_slices_mixed_execution_and_readonly_generator_cache": ( + """ + Token proc(types []type) type { return struct {} } + update proc(items []mut type) void { items[0] = i16 } + compute proc(value i16) i16 { + backing [1]mut type := [i8] + update(backing[..]) + view :: backing[..] + if (Token(view) != Token([i16])) return -1 + local view[0] :: value + return local + } + main proc() i16 { return compute(42) } + """, b"42\n", None, + ), + "metadata_slices_reject_dangling_nested_scope": ( + """ + Storage proc() type { + backing [1]mut type := [i16] + view []type := backing[..] + { + inner [1]mut type := [bool] + view = inner[..] + } + return view[0] + } + main proc() Storage() { return 42 } + """, b"", b"compile-time result cannot escape evaluation as a source value", + ), + "metadata_slices_reject_runtime_short_circuit_mutation": ( + """ + update proc(items []mut type) bool { items[0] = i16 + return true } + compute proc(take bool) usize { + backing [1]mut type := [i8] + view :: backing[..] + if (take and update(view)) return sizeof!(view[0]) + return sizeof!(view[0]) + } + main proc() usize { return compute(false) } + """, b"", b"compile-time value", + ), + "metadata_slices_reject_runtime_conditional_expression_mutation": ( + """ + update proc(items []mut type) bool { items[0] = i16 + return true } + compute proc(take bool) usize { + backing [1]mut type := [i8] + view :: backing[..] + selected :: if (take) update(view) else false + if (selected) return sizeof!(view[0]) + return sizeof!(view[0]) + } + main proc() usize { return compute(false) } + """, b"", b"compile-time value", + ), + "metadata_slices_skip_short_circuited_borrowed_effects": ( + """ + update proc(items []mut type) bool { items[0] = i16 + return true } + Storage proc() type { + backing [1]mut type := [i8] + view :: backing[..] + if (false and update(view)) return bool + return view[0] + } + compute proc(value Storage()) i16 { + backing [1]mut type := [i8] + view :: backing[..] + if (true or update(view)) return i16(value) + return -1 + } + main proc() i16 { return compute(42) } + """, b"42\n", None, + ), "metadata_slices_reject_mutable_slice_annotation": ( """ types proc() []mut type { return [i16] } main proc() types()[0] { return 42 } """, b"", b"compile-time result cannot escape evaluation as a source value", ), - "metadata_slices_reject_mutable_array_backing": ( + "metadata_slices_snapshot_mutable_array_backing": ( """ types proc() []type { backing [1]mut type := [i16] return backing[..] } main proc() types()[0] { return 42 } - """, b"", b"compile-time result cannot escape evaluation as a source value", + """, b"42\n", None, ), - "metadata_slices_reject_mutable_binding_with_readonly_array_elements": ( + "metadata_slices_snapshot_mutable_binding_with_readonly_array_elements": ( """ types proc() []type { backing [1]type := [i16] return backing[..] } main proc() types()[0] { return 42 } - """, b"", b"compile-time result cannot escape evaluation as a source value", + """, b"42\n", None, ), - "metadata_record_slices_reject_mutable_array_backing": ( + "metadata_record_slices_snapshot_mutable_array_backing": ( """ Info :: struct { child type, count usize } fields proc() []Info { @@ -3407,7 +3677,7 @@ CASES = { return backing[..] } main proc() fields()[0].child { return 42 } - """, b"", b"compile-time result cannot escape evaluation as a source value", + """, b"42\n", None, ), "metadata_slices_reject_sentinel_annotation": ( """ @@ -11412,6 +11682,16 @@ def check_specialization_packages(temporary, executable): 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 } + @hide:file + Private :: distinct i16 + @hide:file + replace proc(items []mut type) void { items[0] = Private } + borrowed_types proc() []type { + backing [1]mut type := [bool] + replace(backing[..]) + return backing[..] + } + FromBorrow proc() type { return struct { value borrowed_types()[0] } } NominalConfig :: struct { child type } nominal_config proc() NominalConfig { return { child = struct { value i16 } } @@ -11426,11 +11706,15 @@ def check_specialization_packages(temporary, executable): 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 + borrowed lib.FromBorrow() :: { value = 40 } + same_borrowed lib.FromBorrow() :: borrowed + if (i16(same_borrowed.value) != 40) return 0 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)) + 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]] } } @@ -11564,7 +11848,7 @@ def check_specialization_packages(temporary, executable): interpreted = run(str(HONEY), "run", str(app)) assert interpreted.stderr.startswith(f"{sizing.resolve()}:4:".encode()), outcome(interpreted) assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted) - return 26 + return 27 def check_dependency_provenance(temporary, executable):