diff --git a/README.md b/README.md index 15bec00..459a0ec 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ type-producing bodies support mutable type and value locals, direct binding assi field/index assignments also update initialized, materializable aggregate locals in type-producing bodies, including nested record fields and mutable fixed-array elements. for example, `counts [2]mut usize := [0, 0]` followed by `counts[1] = 2` can generate `[counts[1]]i16` storage. each update uses ordinary checked IR and returns a new owned constant: earlier copies remain unchanged, while later loop iterations, generated lengths and defaults observe the update. ordinary mutability, index-type/bounds, field and exact value-type checks still apply, including immutable payload captures and narrow arithmetic overflow. imported producers and repeated compilation retain source diagnostics and nominal cache identity; pointer/slice-backed mutation and type-valued aggregate storage remain open. +type-producing bodies also iterate materializable fixed arrays and strings with immutable by-value element captures and optional `usize` indices: `for counts |count, index| { length = length + count + index }` can compute generated storage. nested loops, aggregate captures, empty collections, break/continue and early returns share the type-control execution bound. array places retain their entry-time indices and observe later element updates; temporary arrays and strings are evaluated once. ordinary constant-input procedures additionally support slice and pointer captures through checked IR, provided only materializable results escape. type-producing loop bodies still reject storage-bearing captures rather than retaining temporary interpreter addresses; type-valued elements and `inline for` 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. type-producing bodies also execute scalar/enum `match` statements and type-valued match expressions in initializers, assignments and returns, such as `return match wide { true: i16, false: i8 }`. selected expression arms may themselves be matches. an ordinary checked IR match returns the selected arm index, preserving subject/pattern typing, range boundaries, overlap, unreachable-arm and exhaustiveness diagnostics without checking unexecuted bodies. statement arms retain scoped locals, assignments, early returns and enclosing-loop break/continue; expression selection shares the type-control step bound. @@ -174,7 +176,9 @@ untagged `Bits :: union { integer u64, real f64 }` overlays its members in share `for 0..10 |i| { ... }` iterates an ascending, upper-exclusive integer range; `..=` includes the upper bound. a capture is required: `|i|` binds an immutable value with the bounds' exact integer type in the body scope, while `|_|` discards it. bounds resolve in the enclosing scope and are evaluated left-to-right once, so changing a bound variable in the body does not change the range. reversed ranges are empty; equal bounds produce zero iterations with `..` and one with `..=`. inclusive ranges terminate safely at the maximum of each integer type. -range bounds that are compound or unary expressions require their own parentheses, such as `(-2)..(limit + 1)`. a braceless body also requires parentheses around the entire iterable: `for (0..10) |i| total = total + i`. current execution supports direct integer ranges only; first-class range values, collection iteration, pointer captures and index captures remain unimplemented. [programs/range_loop.hon](programs/range_loop.hon) skips `5` while summing `0..10`; `./build/honey run programs/range_loop.hon` prints `40`. +range bounds that are compound or unary expressions require their own parentheses, such as `(-2)..(limit + 1)`. a braceless body also requires parentheses around the entire iterable: `for (0..10) |i| total = total + i`. first-class range values and range pointer/index captures remain unimplemented. [programs/range_loop.hon](programs/range_loop.hon) skips `5` while summing `0..10`; `./build/honey run programs/range_loop.hon` prints `40`. + +collection loops accept arrays, slices, strings and single pointers to arrays: `for values |value, index| { ... }`. element captures are immutable by-value copies with the exact element type; optional indices have type `usize`, and either capture may be discarded with `_`. `|@value|` borrows an element through a single pointer, writable only when the source storage permits mutation. the iterable and its length are evaluated once, then each iteration reads its current element: array lvalues are borrowed, temporary arrays are retained once, and slice rebinding does not change the active loop's view. empty collections execute no body, including arrays of zero-cell elements. collection loops share range-loop cleanup, break/continue and return handling through the interpreter and native backend. `break` exits the nearest enclosing `for` or `while`; `continue` restarts a `while` condition or advances a `for` range through its guarded update. both are operand-free statements permitted within loop bodies and nested conditional arms; labels and values are unsupported. controls outside loops produce keyword-spanned diagnostics, including in untaken or unreachable code, and trap only if reached. code after a transfer is still resolved and type-checked but does not execute on that path. body locals are reset on re-entry after `continue`; loop transfers do not guarantee a function return. both controls lower to existing IR jumps, without new interpreter instructions or loop frames. [programs/loop_control.hon](programs/loop_control.hon) skips an iteration and exits early; `./build/honey run programs/loop_control.hon` prints `12`. diff --git a/TODO.md b/TODO.md index 37339ac..55587c0 100644 --- a/TODO.md +++ b/TODO.md @@ -27,8 +27,11 @@ including owned by-value tagged payload captures in ordinary generic procedures. type-producing procedures also accept ordinary constant inputs, cached and erased like explicit compile-time arguments. mutable type/value locals, direct and aggregate field/index assignments, branches, -scalar/enum/tagged-union matching and bounded integer-range/while loops now generate types and storage. optional +scalar/enum/tagged-union matching and bounded integer-range/collection/while loops now generate types and storage. optional if/while captures and tagged payload matches select nested constructors, lengths, signatures and defaults; +array/slice/string/pointer-to-array collection loops support exact copy/pointer +captures and `usize` indices through both paths; type-producing bodies iterate +materializable arrays/strings by value without retaining interpreter addresses. general type-valued execution, reflection and generic library acceptance remain open. the driver retains frontend storage and source provenance through `@source/compilation`, with compilation-wide AST node identities. @@ -79,8 +82,9 @@ test discovery and FFI remain outside these completed slices. - [x] generic match-specialization checkpoint: side-effect-free constant scalar/enum/tagged-union subjects in concrete generic procedures and explicit `match $(subject)` expressions/statements select an arm before body specialization/checking. reuse ordinary checked IR for pattern types, ranges, overlap, exhaustiveness and every arm's capture validation; only the selected body is elaborated. exact by-value payload types/owned strings, runtime parameters, nested expression matches, cleanup, return/loop transfers, imported producers and repeatable compilation work through interpreter/native execution. ordinary call/runtime/mutable subjects stay runtime; contextual aggregates with possible runtime defaults are not implicitly evaluated. all bodies remain parsed/name-resolved; storage-bearing payloads retain the constant-materialization escape boundary. - [x] optional type-control checkpoint: type-producing bodies execute optional if/while guards with immutable by-value payload captures. ordinary checked IR validates guards; selected scalar/aggregate payloads retain exact types and owned strings across nested captures, generated storage/defaults, loop updates/transfers and early returns. absent optionals skip unselected bodies; invalid guards, capture mutation/shadowing, overflow and execution bounds retain diagnostics. imported producers, dependency provenance, nominal cache identity and repeatable compilation work through interpreter/native execution. null data/function pointers materialize correctly; nonnull storage-bearing payloads retain the constant-materialization escape boundary. - [x] aggregate type-update checkpoint: type-producing bodies update initialized, materializable aggregate locals through nested record fields and mutable fixed-array indices. ordinary checked IR validates and executes each assignment, retaining exact value types, mutability (including immutable payload captures), index types/bounds, unknown-field and narrow-overflow diagnostics. new owned constants preserve earlier by-value copies while later loops, generated storage and defaults observe updates. imported producers, dependency provenance, nominal cache identity and repeatable compilation work through interpreter/native execution. pointer/slice-backed mutation, uninitialized aggregate construction and type-valued aggregate storage remain open. - - [ ] remaining gate: general type-valued compile-time execution, inline match/loop expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), broader constant materialization (including untagged unions and storage-bearing values), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates. -- [ ] remaining source constructs: collection/index iteration, expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. + - [x] collection-iteration checkpoint: arrays, slices, strings and single pointers-to-arrays support immutable exact-type copy captures, storage-qualified pointer captures and optional `usize` indices through interpreter/native execution. iterables/bounds evaluate once, array lvalues remain borrowed and later element updates are visible; temporary arrays, aggregate copy isolation, zero-cell/empty collections, nested loops and cleanup across transfers retain ordinary semantics. ordinary constant-input procedures reuse the same checked IR; type-producing bodies iterate materializable arrays/strings with by-value captures, fixed entry-time place indices, generated storage, nominal cache identity, dependency diagnostics, shared execution bounds and repeatable compilation. storage-bearing type-control captures and type-valued elements retain their existing boundaries. + - [ ] remaining gate: general type-valued compile-time execution, inline match/loop expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), broader constant materialization (including untagged unions and storage-bearing values), plus existing-library acceptance. required expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates. +- [ ] remaining source constructs: expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. - [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library. - [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support. - [ ] self-hosting proof: bootstrap-built Honey builds the production compiler (excluding test-only imports); that executable rebuilds the compiler; the rebuilt compiler passes representative interpreter/native examples and CLI/error scenarios without Bro at runtime. diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index 4a7ad8a..bf77352 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -146,6 +146,18 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { ` bias i16 = config.bias + i16(original.counts[1]) ` } `} + `Collected proc() type { + ` counts [2]mut usize := [1, 1] + ` length usize := 0 + ` for counts |amount, index| { + ` counts[1] = 2 + ` length = length + amount + ` if (index == 0) continue + ` } + ` Element := i8 + ` for "ok" |byte| { if (byte == u8(107)) Element = i16 } + ` return [length]Element + `} `read_selected proc($choice Choice, value i16) i16 { ` match choice { ` .payload |detail|: { @@ -173,6 +185,11 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { ` if (optional[0] + optional[1] != 40) return 0 ` updated Updated() :: { values = optional } ` if (updated.bias != 42 or updated.values[1] != 21) return 0 + ` collected Collected() :: [19, 21, 2] + ` same_collected Collected() :: collected + ` total i16 := 0 + ` for same_collected |item| { total = total + item } + ` if (total != 42) return 0 ` payload :: packet.choices[0].payload ` if (payload.values[0] != 40 or payload.values[1] != 2 or payload.text.len != 2) return 0 ` match packet.choices[1] { .empty: {}, .payload: return 0 } diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index e00be70..de20963 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -176,7 +176,7 @@ messages :: $enummap.init(Code, []u8, { break_outside_loop = "break requires an enclosing loop", continue_outside_loop = "continue requires an enclosing loop", invalid_range_bound = "range bounds must have compatible integer types", - invalid_for_iterable = "for-loop iteration currently requires a direct integer range", + invalid_for_iterable = "for-loop iteration requires an array, slice, string, single pointer to an array, or direct integer range", range_value_not_supported = "first-class range values are not implemented", range_pointer_capture = "range loops do not support pointer captures", range_index_capture = "range loops do not support index captures", diff --git a/source/lower/lower.hon b/source/lower/lower.hon index fd8565a..0047a06 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -482,13 +482,17 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocErro } .for_stmt: { loop :: ast.for_stmt(state.tree, statement) + is_range :: ast.expr_kind(state.tree, loop.iterable) == .range family :: typecheck.type_of(state.checked, loop.iterable).range_element - type :: lower_type(state, typecheck.Type{ kind = family }) + type ir.Type :: if (is_range) lower_type(state, typecheck.Type{ kind = family }) else .usize slot :: try ir.add_local(state.module, ir.FunctionId(state.function), type, ir.no_array) state.storage[usize(statement)] = Storage{ kind = .local, index = u32(slot) } - if (loop.capture) |capture| state.storage[usize(capture)] = state.storage[usize(statement)] + if (loop.capture) |capture| { + if (is_range) state.storage[usize(capture)] = state.storage[usize(statement)] + else try predeclare_capture(state, capture) + } if (loop.index) |index| { - index_slot :: try ir.add_local(state.module, ir.FunctionId(state.function), .int, ir.no_array) + index_slot :: try ir.add_local(state.module, ir.FunctionId(state.function), .usize, ir.no_array) state.storage[usize(index)] = Storage{ kind = .local, index = u32(index_slot) } } try predeclare_locals(state, loop.body) @@ -705,8 +709,7 @@ lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagn lower_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { loop :: ast.for_stmt(state.tree, id) if (ast.expr_kind(state.tree, loop.iterable) != .range) { - _ = try lower_expression(state, loop.iterable) - if (!state.terminated) _ = trap_type(state, id) + try lower_collection_for(state, id) return } interval :: ast.range_expr(state.tree, loop.iterable) @@ -765,6 +768,78 @@ lower_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnos state.terminated = false } +@hide:file +lower_collection_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + loop :: ast.for_stmt(state.tree, id) + parts :: try slice_parts(state, loop.iterable) + if (state.terminated or trap_type(state, loop.iterable) or trap_type(state, id)) return + slot :: state.storage[usize(id)].index + zero :: try usize_constant(state, 0, loop.span) + _ = try emit(state, ir.Instruction{ op = .store_local, type = .usize, left = zero, slot = slot, span = loop.span }) + origin :: state.block + condition :: try add_block(state) + body :: try add_block(state) + update :: try add_block(state) + exit :: try add_block(state) + state.module.functions.items[state.function].blocks.items[usize(origin)].terminator = ir.Terminator{ + kind = .jump, target = condition, span = loop.span, + } + state.block = condition + current :: try emit(state, ir.Instruction{ op = .load_local, type = .usize, slot = slot, span = loop.span }) + condition_value :: try emit(state, ir.Instruction{ op = .less, type = .bool, left = current, right = parts.bound, span = loop.span }) + state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ + kind = .branch, value = condition_value, target = body, alternative = exit, span = loop.span, + } + previous_loop :: state.loop + state.loop = LoopTargets{ continue_target = update, exit = exit, cleanup_depth = state.cleanups.items.len } + defer state.loop = previous_loop + state.block = body + if (loop.capture) |capture| { + if (!trap_binding(state, capture) and !trap_type(state, capture)) { + runtime :: lower_type(state, parts.element) + shape :: try array_shape(state, parts.element) + pointer :: try emit(state, ir.Instruction{ + op = .pointer_offset, type = .pointer, left = parts.pointer, right = current, + pointee = runtime, array_type = shape, span = loop.span, + }) + value := pointer + if (!loop.pointer_capture) { + address :: try pointer_address(state, pointer, parts.element, loop.span) + value = try emit(state, ir.Instruction{ + op = .load_address, type = runtime, array_type = shape, left = address, span = loop.span, + }) + } + checked :: typecheck.type_of(state.checked, capture) + _ = try emit(state, ir.Instruction{ + op = .store_local, type = lower_type(state, checked), array_type = try array_shape(state, checked), + left = value, slot = state.storage[usize(capture)].index, span = loop.span, + }) + } + } + if (loop.index) |index| { + if (!state.terminated and !trap_binding(state, index) and !trap_type(state, index)) { + _ = try emit(state, ir.Instruction{ + op = .store_local, type = .usize, left = current, slot = state.storage[usize(index)].index, span = loop.span, + }) + } + } + if (!state.terminated) try lower_block(state, loop.body, true) + if (!state.terminated) state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ + kind = .jump, target = update, span = loop.span, + } + state.block = update + state.terminated = false + last :: try emit(state, ir.Instruction{ op = .load_local, type = .usize, slot = slot, span = loop.span }) + one :: try usize_constant(state, 1, loop.span) + next :: try emit(state, ir.Instruction{ op = .add, type = .usize, left = last, right = one, span = loop.span }) + _ = try emit(state, ir.Instruction{ op = .store_local, type = .usize, left = next, slot = slot, span = loop.span }) + state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ + kind = .jump, target = condition, span = loop.span, + } + state.block = exit + state.terminated = false +} + @hide:file lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { tree :: state.tree diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 640fc3e..26f6eb4 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -174,6 +174,7 @@ Cell :: struct { literal ast.NodeId = ast.NodeId(maxval!(u32)) producer ast.NodeId = ast.NodeId(maxval!(u32)) capture ast.NodeId = ast.NodeId(maxval!(u32)) + for_loop ast.NodeId = ast.NodeId(maxval!(u32)) record_checking bool = false record_pointer_depth usize = 0 nominal bool = false @@ -382,6 +383,7 @@ check_elaborated proc( } } # calls use the signature result before contextual literal inference. + try prepare_for_captures(&state, false) try prepare_optional_captures(&state, false) for (0..count) |i| { id :: ast.NodeId(i) @@ -553,7 +555,7 @@ check_elaborated proc( if (cells[i].role != .for_stmt) continue iterable :: ast.for_stmt(tree, ast.NodeId(i)).iterable actual :: types[usize(iterable)] - if (actual.kind != .invalid and actual.kind != .range) { + if (actual.kind != .invalid and actual.kind != .range and !collection_type(&state, actual)) { try fail(&state, iterable, .invalid_for_iterable, ast.node_span(tree, iterable)) } } @@ -1073,12 +1075,13 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b state.cells[usize(loop.iterable)].for_iterable = true try collect_expression(state, loop.iterable) try depend(state, loop.iterable, statement) - if (loop.pointer_capture) { + direct_range :: ast.expr_kind(tree, loop.iterable) == .range + if (direct_range and loop.pointer_capture) { state.cells[usize(statement)].type = invalid_type(try diagnostics.add( state.diagnostics, .range_pointer_capture, loop.span, )) } - if (loop.index_capture) { + if (direct_range and loop.index_capture) { diagnostic :: try diagnostics.add(state.diagnostics, .range_index_capture, loop.span) if (state.cells[usize(statement)].type.kind != .invalid) { state.cells[usize(statement)].type = invalid_type(diagnostic) @@ -1086,9 +1089,18 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b } if (loop.capture) |capture| { try collect_capture(state, capture, loop.body) + if (!direct_range) { + state.cells[usize(capture)].for_loop = statement + if (state.cells[usize(capture)].type.kind != .invalid) state.cells[usize(capture)].type = Type{} + } state.cells[usize(loop.iterable)].capture = capture } - if (loop.index) |index| try collect_capture(state, index, loop.body) + if (loop.index) |index| { + try collect_capture(state, index, loop.body) + if (!direct_range and state.cells[usize(index)].type.kind != .invalid) { + state.cells[usize(index)].type = Type{ kind = .usize } + } + } try collect_block(state, loop.body, return_annotation, true) try depend(state, loop.body, id) } @@ -1763,6 +1775,15 @@ indexable_type proc(state @State, type Type) bool { return (pointer.many and pointer.element.kind != .anyopaque) or pointer.element.kind == .array } +@hide:file +collection_type proc(state @State, type Type) bool { + if (type.nominal != ast.NodeId(maxval!(u32))) return false + if (type.kind == .array or type.kind == .slice or type.kind == .string) return true + if (type.kind != .pointer) return false + pointer :: state.pointers.items[usize(type.pointer_type)] + return !pointer.many and pointer.element.kind == .array and pointer.element.nominal == ast.NodeId(maxval!(u32)) +} + @hide:file addressable_target proc(state @mut State, id ast.NodeId) bool { kind :: ast.expr_kind(state.tree, id) @@ -2676,6 +2697,28 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All if (state.cells[index].type.kind == .invalid) return state.cells[index].type state.cells[index].visiting = true defer state.cells[index].visiting = false + if (state.cells[usize(id)].role == .loop_capture and state.cells[usize(id)].for_loop != ast.NodeId(maxval!(u32))) { + loop :: ast.for_stmt(state.tree, state.cells[usize(id)].for_loop) + actual :: try infer_shape(state, loop.iterable, defaults) + if (actual.kind == .invalid or state.types[usize(loop.iterable)].kind == .invalid) { + type :: if (actual.kind == .invalid) actual else state.types[usize(loop.iterable)] + seed(state, id, type) + return type + } + if (!collection_type(state, actual)) { + if (actual.kind != .none) { + try fail(state, loop.iterable, .invalid_for_iterable, ast.node_span(state.tree, loop.iterable)) + seed(state, id, state.types[usize(loop.iterable)]) + } + return group_type(state, id) + } + descriptor :: try sequence_type(state, loop.iterable, defaults) + type :: if (loop.pointer_capture) + try intern_pointer(state, PointerType{ element = descriptor.element, mutable = descriptor.mutable, many = false }) + else descriptor.element + seed(state, id, type) + return type + } if (state.cells[usize(id)].role == .expression) { kind :: ast.expr_kind(state.tree, id) if (declaration_reference(state.tree, id) and has_narrowing(state, id)) { @@ -2685,6 +2728,14 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All return narrowed } } + if (declaration_reference(state.tree, id)) { + binding :: sema.binding(state.resolution, id) + if (binding.kind == .declaration and state.cells[usize(binding.data.declaration)].role == .loop_capture) { + type :: try infer_shape(state, binding.data.declaration, defaults) + if (type.kind != .none) seed(state, id, type) + return group_type(state, id) + } + } if (kind == .try_expr or kind == .catch_expr) return try infer_fallible_expression(state, id, defaults) if (kind == .builtin_call) return try infer_builtin(state, id, defaults) if (kind == .slice) return try infer_slice(state, id, defaults) @@ -2937,6 +2988,15 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All return group_type(state, id) } +@hide:file +prepare_for_captures proc(state @mut State, defaults bool) void ! (mem.AllocError | diagnostics.CapacityError) { + for 0..(state.cells.len) |i| { + if (state.cells[i].role == .loop_capture and state.cells[i].for_loop != ast.NodeId(maxval!(u32))) { + _ = try infer_shape(state, ast.NodeId(i), defaults) + } + } +} + @hide:file prepare_optional_captures proc(state @mut State, defaults bool) void ! (mem.AllocError | diagnostics.CapacityError) { for 0..(state.cells.len) |i| { @@ -2965,6 +3025,7 @@ prepare_optional_captures proc(state @mut State, defaults bool) void ! (mem.Allo @hide:file prepare_shapes proc(state @mut State, assignment_values, defaults bool) void ! (mem.AllocError | diagnostics.CapacityError) { + try prepare_for_captures(state, defaults) try prepare_optional_captures(state, defaults) for 0..(state.cells.len) |i| { if (state.cells[i].role != .expression or state.cells[i].assignment_value != assignment_values) continue diff --git a/source/sema/check/check.test.hon b/source/sema/check/check.test.hon index 7d99cdb..bc2e8bd 100644 --- a/source/sema/check/check.test.hon +++ b/source/sema/check/check.test.hon @@ -1855,7 +1855,7 @@ range_values_nonrange_iterables_and_capture_modes_have_explicit_errors test { ` local :: 0..3 ` for 3 |_| { absent_scalar } ` for true |_| {} - ` for "text" |_| {} + ` for 2.5 |_| {} ` for scalar() |_| {} ` for stored |_| {} ` for 0..3 |@item| { item } diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index a688abe..0361836 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -1617,10 +1617,153 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat return .next } +# retain an array place, but freeze its indices at loop entry. snapshots then read +# the current owned root after body assignments without re-evaluating the place. +@hide:file +type_collection_place proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error { + resolved :: binding(state, id) + if (resolved.kind == .declaration) { + for (execution.locals.items) |local| { + if (local.declaration == resolved.data.declaration) return id + } + return no_node + } + kind :: ast.expr_kind(&state.tree, id) + if (kind != .selection and kind != .index) return no_node + base :: if (kind == .selection) ast.selection_expr(&state.tree, id).base else ast.index_expr(&state.tree, id).base + place :: try type_collection_place(state, execution, base) + if (place == no_node) return no_node + result := no_node + if (kind == .selection) { + result = try ast.add_selection(state.builder, ast.node_span(&state.tree, id), + place, ast.selection_expr(&state.tree, id).member) + } else { + index :: try type_snapshot(state, execution, ast.index_expr(&state.tree, id).index) + value :: try type_execution_value(state, index, try builtin(state, index, .usize)) + if (value == no_node) return no_node + result = try ast.add_index(state.builder, ast.node_span(&state.tree, id), place, value) + } + try refresh(state) + return result +} + +@hide:file +type_collection_read proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error { + if (binding(state, id).kind == .declaration) return try type_snapshot(state, execution, id) + kind :: ast.expr_kind(&state.tree, id) + base :: if (kind == .selection) ast.selection_expr(&state.tree, id).base else ast.index_expr(&state.tree, id).base + value :: try type_collection_read(state, execution, base) + result := no_node + if (kind == .selection) + result = try ast.add_selection(state.builder, ast.node_span(&state.tree, id), + value, ast.selection_expr(&state.tree, id).member) + else result = try ast.add_index(state.builder, ast.node_span(&state.tree, id), + value, ast.index_expr(&state.tree, id).index) + try refresh(state) + return result +} + +@hide:file +execute_type_collection proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error { + loop :: ast.for_stmt(&state.tree, statement) + span :: ast.node_span(&state.tree, statement) + if (loop.capture) |capture| { + if (binding(state, capture).kind == .invalid) { + state.evaluation_diagnostic = binding(state, capture).data.diagnostic + return .invalid + } + } + if (loop.index) |index| { + if (binding(state, index).kind == .invalid) { + state.evaluation_diagnostic = binding(state, index).data.diagnostic + return .invalid + } + } + snapshot :: try type_snapshot(state, execution, loop.iterable) + if (!try expand_type_matches(state, snapshot)) return .invalid + try expand(state, snapshot) + annotation :: try expression_type(state, snapshot, 0) + if (annotation == no_node) return .invalid + collection :: try type_execution_value(state, snapshot, annotation) + if (collection == no_node) return .invalid + # validate the loop contract even for empty collections, without executing its + # type-valued body. the synthetic body exits before reading any element. + stop :: try ast.add_break_stmt(state.builder, span) + stops [1]ast.NodeId :: [stop] + body :: try ast.add_block(state.builder, span, stops[..]) + selector :: try ast.add_for_stmt(state.builder, span, collection, loop.capture, loop.index, + loop.pointer_capture, loop.index_capture, body) + length := no_node + if (ast.expr_kind(&state.tree, annotation) == .array_type) length = ast.array_type(&state.tree, annotation).count? + else { + value :: try evaluate(state, collection) + if (value.kind == .string) { + text :: ast.literal_text(&state.tree, value.node) + cursor := usize(1) + count := u64(0) + while (cursor < text.len - 1) { + _ = ast.string_next(text, &cursor) catch { return .invalid } + count += 1 + } + length = try ast.add_integer(state.builder, span, count) + } else length = try ast.add_integer(state.builder, span, 0) + } + returned :: try ast.add_return_stmt(state.builder, span, length) + statements [2]ast.NodeId :: [selector, returned] + wrapper :: try ast.add_block(state.builder, span, statements[..]) + try refresh(state) + count :: try evaluate_ir_body(state, statement, wrapper, try builtin(state, statement, .usize)) + if (count.kind != .integer) return .invalid + if (loop.pointer_capture and loop.capture != null) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span) + return .invalid + } + place := no_node + if (ast.expr_kind(&state.tree, annotation) == .array_type) + place = try type_collection_place(state, execution, loop.iterable) + if (state.evaluation_diagnostic != diagnostics.no_diagnostic) return .invalid + saved :: execution.locals.items.len + defer execution.locals.items = execution.locals.items[..saved] + execution.loop_depth += 1 + defer execution.loop_depth -= 1 + for (0..usize(count.magnitude)) |i| { + if (!try type_step(state, statement)) return .invalid + execution.locals.items = execution.locals.items[..saved] + index :: try ast.add_integer(state.builder, span, u64(i)) + try refresh(state) + if (loop.capture) |capture| { + current := collection + if (place != no_node) current = try type_collection_read(state, execution, place) + element :: try ast.add_index(state.builder, span, current, index) + try refresh(state) + element_type :: try expression_type(state, element, 0) + value :: try type_execution_value(state, element, element_type) + if (value == no_node) return .invalid + try arraylist.append(&execution.locals, TypeLocal{ + declaration = capture, value = value, annotation = element_type, + }) + } + if (loop.index) |capture| { + type :: try builtin(state, capture, .usize) + value :: try type_execution_value(state, index, type) + if (value == no_node) return .invalid + try arraylist.append(&execution.locals, TypeLocal{ + declaration = capture, value = value, annotation = type, + }) + } + flow :: try execute_type_block(state, execution, loop.body) + if (flow == .returned or flow == .invalid) return flow + if (flow == .break) return .next + } + return .next +} + @hide:file execute_type_for proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error { loop :: ast.for_stmt(&state.tree, statement) - if (loop.pointer_capture or loop.index_capture or ast.expr_kind(&state.tree, loop.iterable) != .range) return .invalid + if (ast.expr_kind(&state.tree, loop.iterable) != .range) + return try execute_type_collection(state, execution, statement) + if (loop.pointer_capture or loop.index_capture) return .invalid bounds :: ast.range_expr(&state.tree, loop.iterable) start :: try type_snapshot(state, execution, bounds.start) end :: try type_snapshot(state, execution, bounds.end) diff --git a/tests/native.py b/tests/native.py index c8df855..c2e5dd7 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,200 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "collection_iteration_borrows_array_and_reads_each_element": ( + """ + main proc() [2]i16 { + values [3]mut i16 := [19, 1, 100] + total i16 := 0 + for values |value, index| { + if (index == 0) values[1] = 22 + if (index == 2) break + total = total + value + i16(index) + continue + } + return [total, values[1]] + } + """, b"[42, 22]\n", None, + ), + "collection_iteration_pointer_captures_slices_and_array_pointers": ( + """ + main proc() [3]i16 { + values [2]mut i16 := [19, 21] + for (&values) |@value, index| { value^ = value^ + i16(index) } + view []mut i16 :: values[..] + for view |@value| { value^ = value^ + 1 } + total i16 := 0 + for view |value| { total = total + value } + return [total - 1, values[0], values[1]] + } + """, b"[42, 20, 23]\n", None, + ), + "collection_iteration_evaluates_entry_once_and_retains_slice_bound": ( + """ + data proc(calls @mut i16) [2]i16 { + calls^ = calls^ + 1 + return [19, 22] + } + main proc() [3]i16 { + calls i16 := 0 + total i16 := 0 + for data(&calls) |value, index| { total = total + value + i16(index) } + values [2]mut i16 := [20, 22] + view []mut i16 := values[..] + sum i16 := 0 + for view |value| { + view = values[0..0] + sum = sum + value + } + return [total, calls, sum] + } + """, b"[42, 1, 42]\n", None, + ), + "collection_iteration_aggregate_copies_nested_and_zero_cell_shapes": ( + """ + Empty :: struct {} + Item :: struct { values [2]mut i16 } + main proc() [3]i16 { + items [2]mut Item := [{ values = [19, 21] }, { values = [0, 0] }] + total i16 := 0 + for items |item, index| { + copied := item + copied.values[0] = copied.values[0] + 1 + for copied.values |value| { total = total + value } + if (index == 0) items[1].values[1] = 0 + } + empty [0]i16 :: [] + for empty |_| { return [-1, -1, -1] } + cells [2]Empty :: [{}, {}] + count i16 := 0 + for cells |cell, index| { + saved Empty :: cell + count = count + i16(index) + 1 + } + return [total, items[0].values[0], count] + } + """, b"[42, 19, 3]\n", None, + ), + "collection_iteration_strings_and_cleanup_on_transfers": ( + """ + main proc() [2]i16 { + values [3]i16 :: [20, 22, 99] + total i16 := 0 + cleanups i16 := 0 + for values |value, index| { + defer cleanups = cleanups + 1 + if (index == 2) break + total = total + value + continue + } + for "a\\0b" |byte, index| { + if (index == 1 and byte != 0) return [-1, -1] + if (index == 2 and byte != u8(98)) return [-2, -2] + } + return [total, cleanups] + } + """, b"[42, 3]\n", None, + ), + "collection_iteration_ordinary_constant_procedure": ( + """ + compute proc() i16 { + values [2]mut i16 := [19, 21] + view []mut i16 :: values[..] + for view |@value| { value^ = value^ + 1 } + total i16 := 0 + for values |value| { total = total + value } + return total + } + main proc() i16 { return $(compute()) } + """, b"42\n", None, + ), + "type_execution_collection_iteration_observes_updates_and_freezes_places": ( + """ + Storage proc() type { + rows [2]mut [2]mut usize := [[1, 1], [9, 9]] + selected usize := 0 + length usize := 0 + for rows[selected] |value, index| { + selected = 1 + (rows[0])[1] = 2 + length = length + value + if (index == 0) continue + break + } + Result := i8 + for "a\\0b" |byte, index| { + if (byte == 0 and index == 1) Result = i16 + } + return struct { values [length]Result } + } + main proc() i16 { + value Storage() :: { values = [19, 21, 2] } + same Storage() :: value + return same.values[0] + same.values[1] + same.values[2] + } + """, b"42\n", None, + ), + "type_execution_collection_nested_aggregate_captures_and_empty_body": ( + """ + Storage proc() type { + items [2]struct { values [2]usize, text string } :: [ + { values = [1, 0], text = "a" }, { values = [0, 1], text = "b" }, + ] + length usize := 0 + for items |item| { + for item.values |value| { length = length + value } + } + empty [0]i16 :: [] + for empty |_| { return i8(128) } + for items |@_, index| { if (index == 1) break } + return [length]i16 + } + main proc() i16 { + values Storage() :: [20, 22] + return values[0] + values[1] + } + """, b"42\n", None, + ), + "collection_iteration_rejects_scalar_iterable": ( + "main proc() i16 { for 1.5 |_| { return 0 }\nreturn 42 }", + b"", b"for-loop iteration requires", + ), + "collection_iteration_rejects_capture_rebinding": ( + "main proc() i16 { values [1]i16 :: [1]\nfor values |value| { value = 42 }\nreturn 0 }", + b"", b"assignment requires a mutable local binding", + ), + "collection_iteration_rejects_index_rebinding": ( + "main proc() usize { values [1]i16 :: [1]\nfor values |_, index| { index = 42 }\nreturn 0 }", + b"", b"assignment requires a mutable local binding", + ), + "collection_iteration_rejects_readonly_array_pointer_mutation": ( + "main proc() i16 { values [1]mut i16 :: [1]\nfor values |@value| { value^ = 42 }\nreturn 0 }", + b"", b"mutable pointee", + ), + "collection_iteration_rejects_readonly_slice_pointer_mutation": ( + "main proc() i16 { values [1]mut i16 := [1]\nview []i16 :: values[..]\nfor view |@value| { value^ = 42 }\nreturn 0 }", + b"", b"mutable pointee", + ), + "type_execution_collection_iteration_preserves_element_type": ( + """ + Bad proc() type { + values [1]i8 :: [127] + for values |value| { next :: value + 1 } + return i16 + } + main proc() Bad() { return 0 } + """, b"", b"integer arithmetic overflow", + ), + "type_execution_collection_iteration_preserves_step_bound": ( + """ + Bad proc() type { + cells [1]i16 :: [0] + for cells |_| { while (true) {} } + return i16 + } + main proc() Bad() { return 0 } + """, b"", b"compile-time execution", + ), "type_execution_aggregate_updates_preserve_copies_and_generate_defaults": ( """ Config :: struct { values [2]mut i16, count usize, text string } @@ -4520,7 +4714,38 @@ 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 10 + sizing.write_text(textwrap.dedent(""" + Collected proc() type { + values [2]usize :: [1, 1] + length usize := 0 + for values |value, index| { length = length + value + index } + return [length]i16 + } + """).strip() + "\n") + source.write_text(textwrap.dedent(""" + lib :: import "../library" + main proc() i16 { + values lib.Collected() :: [19, 21, 2] + same lib.Collected() :: values + total i16 := 0 + for same |value| { total = total + value } + return total + } + """).strip() + "\n") + check(app, executable, (b"42\n", None)) + sizing.write_text(textwrap.dedent(""" + Collected proc() type { + values [1]i8 :: [127] + for values |value| { next :: value + 1 } + return i16 + } + """).strip() + "\n") + source.write_text('lib :: import "../library"\nmain proc() lib.Collected() { return 0 }\n') + check(app, executable, (b"", b"integer arithmetic overflow")) + 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 12 def check_dependency_provenance(temporary, executable):