diff --git a/README.md b/README.md index 01054d3..fa66f78 100644 --- a/README.md +++ b/README.md @@ -178,9 +178,9 @@ optionals use `?T`; values of `T` and `null` initialize optional storage, parame fallible values use explicit `T ! E` channels, where `E` is an enum, record or tagged union. success and error values initialize storage, arguments and returns; fallible values also work in arrays, records and callbacks. `void ! E` functions may fall through successfully. native unbacked enums and tagged unions compose with `A | B`, including grouped/imported error sets; compatible subsets widen without changing variant identities. immutable match subjects narrow within explicit/grouped arms and final `else` arms, allowing residual errors to propagate into smaller channels. -`try expression` evaluates once, extracts success or returns a compatible error from the enclosing fallible function. `expression catch [|err|] fallback` evaluates only the selected path; fallbacks may be expressions, terminal statement blocks or, for void success, blocks that fall through. inferred `T!` signatures and yielding catch blocks are not implemented. write `builtin!(...)` without a gap before `!`; in general expressions a spaced `T ! (E)` denotes a fallible type. +`try expression` evaluates once, extracts success or returns a compatible error from the enclosing fallible function. `expression catch [|err|] fallback` evaluates only the selected path; fallbacks may be expressions, yielding or terminal statement blocks or, for void success, blocks that fall through. inferred `T!` signatures are not implemented. write `builtin!(...)` without a gap before `!`; in general expressions a spaced `T ! (E)` denotes a fallible type. -`defer statement` and `errdefer [|err|] statement` register reached cleanup in the current block; braces introduce explicit nested scopes. cleanup runs LIFO when its scope exits, including return, break and continue; return values are evaluated and copied before cleanup. `errdefer` requires a fallible function and runs only on a language-error return while its scope remains active. cleanup cannot return, propagate with `try` or transfer control out of itself. source errors remain ordinary values: catch does not intercept diagnostic traps or compiler allocation/IO failures, and traps do not unwind source cleanup. a fallible `main` prints its success value normally; an unhandled error prints nothing and exits with status `1`, without creating a diagnostic. +`defer statement` and `errdefer [|err|] statement` register reached cleanup in the current block; braces introduce explicit nested scopes. cleanup runs LIFO when its scope exits, including return, yield, break and continue; returned and yielded values are evaluated and copied before cleanup. `errdefer` requires a fallible function and runs only on a language-error return while its scope remains active, not on a successful yield. cleanup cannot return, propagate with `try` or transfer control out of itself; a nested value block may yield locally. source errors remain ordinary values: catch does not intercept diagnostic traps or compiler allocation/IO failures, and traps do not unwind source cleanup. a fallible `main` prints its success value normally; an unhandled error prints nothing and exits with status `1`, without creating a diagnostic. `sizeof!(T)` and `alignof!(T)` report the supported target's native layout, including aggregate padding, slice descriptors and nullable pointers. `ptrcast!(value)` takes its full result pointer type from context; `ptrcast!(T, value)` states it explicitly. casts preserve the storage location and optional presence, permit byte reinterpretation, and cannot remove readonly qualification. `constcast!` changes pointer or slice mutability without changing the pointee type; it does not make genuinely readonly storage writable or extend its lifetime. `anyopaque` is a pointer-only pointee, not a storable or dereferenceable value. @@ -200,9 +200,20 @@ enums use `Kind :: enum { number, empty }`, qualified members such as `Kind.numb untagged `Bits :: union { integer u64, real f64 }` overlays its members in shared storage. keyed construction selects exactly one member; reading another member reinterprets the same bytes. tagged unions use `union(enum)` or `union(Kind)`; an explicit enum must contain every union variant. construct them with `Choice { number = 42 }`, contextual `.number{42}`, `.point{x = 40, y = 2}` for record payloads, or `.empty` for `void` payloads. field reads/writes validate the active tag. nested aggregates copy by value, including across calls. CLI union results print `{payload}` (or `{}` for a void payload); untagged results print the first member. -`if (guard) then_value else else_value` is a boolean value expression. `if value |payload| then_value else else_value` instead tests optional presence and binds the exact payload as an immutable by-value capture in the then expression; nested optionals preserve present-null payloads. the guard runs once and only the selected expression executes; `else` is required and binds to the nearest conditional. both runtime branches must fit the result context, including optional/fallible channels and aggregate copies. constant-input procedures, direct `$()` evaluation, generic branch specialization, explicit compile-time subject selection and generated type positions reuse checked match execution. boolean guards require parentheses unless they are calls or explicit `$()` expressions; optional captures also permit bare guards. parenthesize a conditional used as a match-arm value to distinguish it from an `if` statement. yielding value blocks remain unsupported. +`if (guard) then_value else else_value` is a boolean value expression. `if value |payload| then_value else else_value` instead tests optional presence and binds the exact payload as an immutable by-value capture in the then expression; nested optionals preserve present-null payloads. the guard runs once and only the selected expression executes; `else` is required and binds to the nearest conditional. both runtime branches must fit the result context, including optional/fallible channels and aggregate copies. constant-input procedures, direct `$()` evaluation, generic branch specialization, explicit compile-time subject selection and generated type positions reuse checked match execution. boolean guards require parentheses unless they are calls or explicit `$()` expressions; optional captures also permit bare guards. parenthesize a conditional used as a match-arm value to distinguish it from an `if` statement. -`match value { .number |n|: n + 2, .empty: 0 }` selects one arm after evaluating its subject once. matches support enums, tagged unions, composed error sets, numeric/boolean scalars, comma-separated patterns, numeric ranges, final `else` arms, arm-local payload captures, statement bodies and expression results. checking rejects missing coverage, duplicate/overlapping patterns, unreachable arms and incompatible captures/results. block arms can return or transfer loop control; yielding value blocks and pointer captures belong to later gates. +value blocks contain ordinary statements and produce a result with `yield expression`: + +```hon +answer i16 :: { + value i16 := 40 + yield value + 2 +} +``` + +`yield` exits the nearest enclosing value block, match arm or catch handler with a copied result. ordinary nested statement blocks and loops do not change its target; return, break and continue retain their usual targets. conditional branches also accept yielding blocks. result context reaches yielded expressions, including narrow integers, aggregates and optional/fallible channels. nonvoid fallthrough, incompatible yields and yields outside a value source diagnose; only reached invalid paths trap. constant evaluation and type-producing procedures support yielding blocks, generated storage/defaults and owned snapshots. empty, singleton and keyed record/tuple literals retain their existing syntax; statement-bearing braces select a value block. labeled yields and value-producing loops remain unsupported. + +`match value { .number |n|: n + 2, .empty: 0 }` selects one arm after evaluating its subject once. matches support enums, tagged unions, composed error sets, numeric/boolean scalars, comma-separated patterns, numeric ranges, final `else` arms, arm-local payload captures, statement bodies and expression results. checking rejects missing coverage, duplicate/overlapping patterns, unreachable arms and incompatible captures/results. block arms can yield results, return or transfer loop control; pointer captures belong to a later gate. a final `inline |tag|:` arm on an enum, or `inline |payload[, tag]|:` on a tagged union, expands into one ordinary arm for each variant not covered earlier. dispatch remains runtime and evaluates the subject once. enum captures and optional union tags are immutable compile-time enum values; payloads are immutable by-value captures with each variant's exact type, including a `void` value for empty variants. either capture may be discarded with `_`. static tag matches/conditions select variant-specific operations before checking, including generic calls, nested inline arms and type-producing procedure bodies. ordinary payloads remain runtime values, and return/break/continue/deferred cleanup retain ordinary match semantics. redundant/nonfinal inline arms, invalid captures and scalar/untagged subjects are diagnosed. expansion shares the 4096-step type-control bound. diff --git a/TODO.md b/TODO.md index 88e50fb..87cab21 100644 --- a/TODO.md +++ b/TODO.md @@ -57,8 +57,10 @@ the existing `ArrayList` uses nested generic inference, file-local helpers and the real imported C allocator through native execution; interpreter checks use non-FFI allocators and cover zero-sized storage, growth, failure and cleanup. boolean and optional-capture `if ... else` expressions reuse checked matches for -runtime/constant values, contextual optional/fallible results and generated types; -yielding blocks remain open. +runtime/constant values, contextual optional/fallible results and generated types. +yielding value blocks, match arms and catch handlers copy results before scoped +cleanup and preserve enclosing function/loop transfers through both execution paths; +compile-time blocks also generate types, storage and defaults. the driver retains frontend storage and source provenance through `@source/compilation`, with compilation-wide AST node identities. file commands retain single-file behavior; directory commands compile the @@ -125,10 +127,11 @@ test discovery and FFI remain outside these completed slices. - [x] type-metadata reflection checkpoint: `typeinfo!(T)` uses the actual `@std/meta` ABI to classify supported types and expose array child/length, immediate distinct backing, record names/tuple/layout and enum/record fields through interpreter/native execution. readonly owned metadata collections retain exact nominal/type leaves, ordinal indices, composed enum identity, checked projections, by-value iteration/copies and compile-time procedure boundaries. ordinary/inline matches select runtime bodies; reflected fields drive `field!`, generics and generated storage/defaults. arity/type/import/ABI failures, match/capture validation, bounds, readonly updates, runtime escape rejection, imported provenance, shared construction bounds and repeatable compilation retain diagnostics and ownership. existing `meta.EnumFieldStruct` and `enummap.EnumMap` generated storage are verified without replacement library code; full `enummap.init/get` is covered below, while general type-valued execution and broader constant materialization remain open. - [x] enum-map acceptance checkpoint: existing `enummap.init/get` run through interpreter/native execution with sparse/aliased/composed enum keys, absent entries and owned aggregate values. contextual `.tag` and aggregate arguments reuse key/value types inferred from rigid generated-map arguments; unresolved generics and rigid conflicts still diagnose. `some!` constructs exact, single-evaluation optional payloads and preserves nested present-null values through compile-time results, cache keys and generated defaults. concrete mutable runtime locals initialize from `undefined`, including nested record/array updates, pointer access and by-value copies; ordinary compile-time procedures materialize fully initialized storage and reject incomplete snapshots. native undefined storage is poison, not zeroed or per-leaf checked. `get` now explicitly returns `null` on lookup fallthrough, preserving the ordinary nonvoid return contract. - [x] array-list acceptance checkpoint: existing `std/arraylist` and `std/mem` initialize, reserve/grow, append/pop/clear and release ordinary/aggregate/zero-sized storage without replacement library code. nested generic inference canonicalizes generated types through pointers and derives exact slice `.ptr` types; rigid conflicts remain diagnostic. `minval!`/`maxval!` preserve integer types and checked `divtrunc!` traps on zero and signed overflow through interpreter/native execution. file-local helpers retain real `@hide:file` visibility, newline `else` preserves match default arms, and repeated `_` parameters remain sinks. generated C records, nominal opaque pointers and foreign callbacks load from existing `ffi/c`; unused unsupported signatures neither diagnose nor emit external symbols, while demanded unsupported ABI/types diagnose. interpreter fixed-capacity allocator checks cover failure without losing storage and deferred error cleanup; native `mem.c_allocator` checks cover aggregate growth and over-aligned allocation/reallocation at `-O0`/`-O2`. arbitrary interpreter FFI, variadic/aggregate C calls and broader library acceptance remain open. - - [x] boolean conditional-expression checkpoint: `if (guard) then_value else else_value` reuses canonical matches through interpreter/native execution. guards evaluate once and only the selected expression executes; nested conditionals bind `else` to the nearest conditional. exact result types, aggregate copy isolation, contextual optional/fallible channels, direct `$()` values, constant-input procedures, generic branch selection and nested generated type positions retain existing checked execution. missing `else`, nonboolean guards and incompatible runtime branch types diagnose. optional captures are covered below; yielding value blocks, compound updates and bitwise operators remain open before hash-map acceptance. - - [x] optional conditional-expression checkpoint: `if value |payload| then_value else else_value` reuses canonical presence matches through interpreter/native execution. single-evaluation guards and lazy branches preserve exact immutable by-value captures, nested optional presence, aggregate isolation and runtime pointer/callback/slice/zero-sized payloads. contextual optional/fallible results, constant-input procedures, generated types/defaults and explicit compile-time subject selection retain checked execution and owned payload snapshots. nonoptional guards, capture scope/shadowing, malformed captures, missing `else`, incompatible branches and compile-time overflow retain diagnostics. yielding value blocks and broader constant materialization remain open. + - [x] boolean conditional-expression checkpoint: `if (guard) then_value else else_value` reuses canonical matches through interpreter/native execution. guards evaluate once and only the selected expression executes; nested conditionals bind `else` to the nearest conditional. exact result types, aggregate copy isolation, contextual optional/fallible channels, direct `$()` values, constant-input procedures, generic branch selection and nested generated type positions retain existing checked execution. missing `else`, nonboolean guards and incompatible runtime branch types diagnose. optional captures and yielding value blocks are covered below; compound updates and bitwise operators remain open before hash-map acceptance. + - [x] optional conditional-expression checkpoint: `if value |payload| then_value else else_value` reuses canonical presence matches through interpreter/native execution. single-evaluation guards and lazy branches preserve exact immutable by-value captures, nested optional presence, aggregate isolation and runtime pointer/callback/slice/zero-sized payloads. contextual optional/fallible results, constant-input procedures, generated types/defaults and explicit compile-time subject selection retain checked execution and owned payload snapshots. nonoptional guards, capture scope/shadowing, malformed captures, missing `else`, incompatible branches and compile-time overflow retain diagnostics. yielding value blocks are covered below; broader constant materialization remains open. + - [x] yielding-block checkpoint: value blocks and `yield expression` produce exact/contextual results through interpreter/native execution, including conditional branches, match arms and catch handlers. nested value sources select the nearest target; ordinary blocks/loops preserve it. yielded aggregates snapshot before LIFO cleanup, successful yields skip `errdefer`, and return/error/break/continue retain enclosing targets. ordinary constants, generic branch specialization and type-producing blocks generate owned values, types, storage and defaults. missing operands/results, incompatible yields, capture mutation and outward cleanup transfers retain diagnostics and demand-driven traps; imported provenance and repeatable compilation are verified. labeled yields and value-producing loops remain outside this checkpoint. - [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including untagged unions and storage-bearing values), plus full existing-library acceptance for hash maps, broader `meta` operations and `io.print`. required expression-valued control flow, compound updates, formatting parsing and broader imported C-binding support may interleave with later gates. -- [ ] remaining source constructs: yielding value blocks and `yield`, compound updates, bitwise operators and multiline strings. boolean and optional-capture conditional expressions are covered above. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. +- [ ] remaining source constructs: compound updates, bitwise operators and multiline strings. boolean and optional-capture conditional expressions, yielding value blocks and `yield` 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. - [ ] 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/ast/ast.hon b/source/ast/ast.hon index 4255f66..219750b 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -8,11 +8,13 @@ #! * parameters -> [['$'] identifier {',' ['$'] identifier} type {',' ['$'] identifier {',' ['$'] identifier} type} [',']]; '$' marks each compile-time parameter independently. #! '$(' expr ')' is an explicit compile-time expression with primary-expression precedence. #! value expressions also accept 'if' guard ['|' identifier '|'] expr 'else' expr, canonicalized to matches; else is required. +#! bare braces containing statement syntax form a value block; contextual record literals keep their initializer grammar. +#! 'yield' requires an expression on the same line and targets the nearest enclosing value construct, not a statement block. #! * block .... -> '{' {stmt} '}' #! * arm ...... -> block | stmt (single statements become canonical blocks) #! * match_arm -> (expr {',' expr} ['|' identifier '|'] | 'else') ':' (block | stmt | expr); arms are comma/newline separated. #! enum members and tagged-union variants accept keyword names; member names do not enter lexical scopes. -#! * stmt ..... -> decl | expr | block | ('defer' | 'errdefer' ['|' identifier '|']) arm | expr '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr ['|' identifier '|'] arm ['else' arm] | 'while' expr ['|' identifier '|'] arm | ['inline'] 'for' expr '|' ['@'] identifier [',' identifier] '|' arm +#! * stmt ..... -> decl | expr | block | ('defer' | 'errdefer' ['|' identifier '|']) arm | expr '=' expr | 'return' [expr] | 'yield' expr | 'break' | 'continue' | 'if' expr ['|' identifier '|'] arm ['else' arm] | 'while' expr ['|' identifier '|'] arm | ['inline'] 'for' expr '|' ['@'] identifier [',' identifier] '|' arm #! braceless guards/iterables require parentheses unless they are calls, explicit comptime expressions or optional captures; else binds to the nearest if. #! precedence (tightest first): calls/indices/selections/dereference/unwrap, unary -/!/&/try higher than multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges/catch. type composition | binds tighter than fallible !; pointer/optional/array prefixes bind to type atoms. compound range endpoints require their own parentheses. #! binary operators associate left; unary operators associate right. @@ -82,6 +84,7 @@ ExprKind :: enum { comptime_expr literal_void literal_undefined + block_expr } DeclKind :: enum { @@ -96,6 +99,7 @@ StmtKind :: enum { expression assignment_stmt return_stmt + yield_stmt break_stmt continue_stmt if_stmt @@ -190,6 +194,8 @@ NodeKind :: enum { literal_float_value literal_void literal_undefined + expr_block + stmt_yield } @hide:file @@ -207,7 +213,7 @@ NodeData :: union { @hide:file foreign_type_flag u8 :: 0b0010 @hide:file variadic_flag u8 :: 0b0100 @hide:file opaque_flag u8 :: 0b1000 -@hide:file optional_match_flag u8 :: 0b0001_0000 +@hide:file optional_match_flag u8 :: 0b00010000 @hide:file Node :: struct { @@ -956,6 +962,13 @@ add_block_stmt proc(builder @mut Builder, body NodeId) NodeId ! mem.AllocError { }, builder_span(builder, body)) } +add_block_expr proc(builder @mut Builder, span Span, body NodeId) NodeId ! mem.AllocError { + return try add_node(builder, Node{ + kind = .expr_block, + main = NodeData{ node_id = body }, + }, span) +} + add_return_stmt proc(builder @mut Builder, span Span, value ?NodeId) NodeId ! mem.AllocError { raw_value :: if (value) |id| id else no_node return try add_node(builder, Node{ @@ -964,6 +977,13 @@ add_return_stmt proc(builder @mut Builder, span Span, value ?NodeId) NodeId ! me }, span) } +add_yield_stmt proc(builder @mut Builder, span Span, value NodeId) NodeId ! mem.AllocError { + return try add_node(builder, Node{ + kind = .stmt_yield, + main = NodeData{ node_id = value }, + }, span) +} + add_break_stmt proc(builder @mut Builder, span Span) NodeId ! mem.AllocError { return try add_node(builder, Node{ kind = .stmt_break }, span) } @@ -1316,6 +1336,7 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind { .stmt_decl: .declaration .stmt_expr: .expression .stmt_return: .return_stmt + .stmt_yield: .yield_stmt .stmt_break: .break_stmt .stmt_continue: .continue_stmt .stmt_if: .if_stmt @@ -1352,6 +1373,12 @@ statement_block proc(tree @Tree, id NodeId) NodeId { return node.main.node_id } +block_expr proc(tree @Tree, id NodeId) NodeId { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_block) + return node.main.node_id +} + return_stmt proc(tree @Tree, id NodeId) ReturnView { node :: get_node(&tree.storage, id) debug.assert(node.kind == .stmt_return) @@ -1361,6 +1388,15 @@ return_stmt proc(tree @Tree, id NodeId) ReturnView { } } +yield_stmt proc(tree @Tree, id NodeId) ReturnView { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .stmt_yield) + return ReturnView{ + span = get_span(&tree.storage, id), + value = node.main.node_id, + } +} + if_stmt proc(tree @Tree, id NodeId) IfView { node :: get_node(&tree.storage, id) debug.assert(node.kind == .stmt_if) @@ -1543,6 +1579,7 @@ expression_kind proc(kind NodeKind) ExprKind { .expr_catch: .catch_expr .expr_typed_constant: .typed_constant .expr_comptime: .comptime_expr + .expr_block: .block_expr .literal_float_value: .literal_float .literal_void: .literal_void .literal_undefined: .literal_undefined @@ -1954,7 +1991,7 @@ node_layout proc(node Node) NodeLayout { layout.direct_children = 6 .expr_pointer_type: layout.direct_children = 4 .expr_optional_type, .expr_distinct_type, .expr_alias, .expr_union_tag_type, .expr_enum_literal, - .expr_try, .expr_comptime: layout.direct_children = 1 + .expr_try, .expr_comptime, .expr_block: layout.direct_children = 1 .expr_call, .expr_builtin_call, .expr_function_type, .expr_array_literal, .expr_record_type, .expr_record_literal, .expr_enum_type, .expr_match, .block, .package: { @@ -2003,7 +2040,7 @@ node_layout proc(node Node) NodeLayout { layout.extra_count = 3 layout.extra_children = 7 } - .stmt_expr, .stmt_return, .stmt_block, .stmt_defer, .stmt_errdefer: { + .stmt_expr, .stmt_return, .stmt_yield, .stmt_block, .stmt_defer, .stmt_errdefer: { layout.class = .statement layout.direct_children = 1 if (node.kind == .stmt_defer or node.kind == .stmt_errdefer) diff --git a/source/ast/renderer/renderer.hon b/source/ast/renderer/renderer.hon index f678726..4c252ad 100644 --- a/source/ast/renderer/renderer.hon +++ b/source/ast/renderer/renderer.hon @@ -72,6 +72,12 @@ render_block proc(renderer @mut Renderer, tree @ast.Tree, block ast.NodeId, dept } try append_text(renderer, "\n") } + .yield_stmt: { + try append_text(renderer, "yield ") + value :: ast.yield_stmt(tree, statement).value? + try append_text(renderer, ast.source_text(tree, ast.node_span(tree, value))) + try append_text(renderer, "\n") + } .if_stmt: { conditional :: ast.if_stmt(tree, statement) try append_text(renderer, "if ") diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index 658180f..fb64f26 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -801,3 +801,49 @@ file_local_declarations_are_isolated_from_siblings_and_import_selection test { } } } + +yielding_blocks_recompile_owned_constants_and_generated_types test { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + modules [2]mut ir.Module := undefined + { + session := init(mem.c_allocator) + defer deinit(&session) + root :: try add_package(&session) + bytes := try owned_text( + `Choose proc(wide bool) type { + ` return { Selected := i8 + ` if (wide) Selected = i16 + ` yield Selected } + `} + `Box proc(wide bool) type { + ` return struct { value Choose(wide) = { yield 42 } } + `} + `compute proc() i16 { + ` return { value i16 := 40 + ` defer value = 0 + ` yield value + 2 } + `} + `main proc() i16 { + ` value Box(true) :: {} + ` constant :: $(compute()) + ` return { yield if (constant == value.value) constant else -1 } + `} + , + ) + defer arraylist.deinit(&bytes) + _ = try add_source(&session, root, "yielding.hon", &bytes, &diagnostic_store) + modules[0] = try compile(&session, root, &diagnostic_store) + errdefer ir.deinit(&modules[0]) + modules[1] = try compile(&session, root, &diagnostic_store) + } + defer ir.deinit(&modules[0]) + defer ir.deinit(&modules[1]) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + for modules |module| { + outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store) + defer interpreter.deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(42), outcome.value.data.integer) + } +} diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index 1dc9451..5d8a2a3 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -37,6 +37,9 @@ Code :: enum { cannot_infer_type missing_return return_type_mismatch + yield_outside_block + missing_yield + yield_type_mismatch condition_not_bool invalid_assignment_target break_outside_loop @@ -181,6 +184,9 @@ messages :: $enummap.init(Code, []u8, { cannot_infer_type = "cannot infer a type without a type anchor", missing_return = "nonvoid function can finish without returning a value", return_type_mismatch = "return does not match the function result type", + yield_outside_block = "yield requires an enclosing value block", + missing_yield = "nonvoid value block can finish without yielding a value", + yield_type_mismatch = "yield does not match the value block result type", condition_not_bool = "condition must have type bool", invalid_assignment_target = "assignment requires a mutable local binding", break_outside_loop = "break requires an enclosing loop", @@ -253,7 +259,7 @@ messages :: $enummap.init(Code, []u8, { incompatible_propagated_error = "propagated errors are not a subset of the function error channel", catch_type_mismatch = "catch handler must produce the success type or terminate", errdefer_outside_fallible = "errdefer requires an enclosing fallible function", - cleanup_control_flow = "cleanup cannot escape with return, break, continue, or try", + cleanup_control_flow = "cleanup cannot transfer control out of itself", comptime_argument_required = "generic argument must be a compile-time value", cannot_infer_generic = "cannot infer consistent compile-time arguments from runtime arguments", specialization_cycle = "compile-time specialization depends on its unfinished result", diff --git a/source/lexer/scan.hon b/source/lexer/scan.hon index 1f804c7..3604801 100644 --- a/source/lexer/scan.hon +++ b/source/lexer/scan.hon @@ -25,6 +25,7 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([ { "null", .null }, { "undefined", .undefined }, { "return", .return }, + { "yield", .yield }, { "break", .break }, { "continue", .continue }, { "try", .try }, diff --git a/source/lexer/scan.test.hon b/source/lexer/scan.test.hon index 14f04f4..093324a 100644 --- a/source/lexer/scan.test.hon +++ b/source/lexer/scan.test.hon @@ -275,3 +275,18 @@ scans_import_keywords_and_preserves_source_in_tokens_and_diagnostics test { try testing.expect_equal(state.source_id, diagnostic.span.source) } } + +reserves_yield_without_splitting_identifiers test { + pool := strpool.init(mem.c_allocator) + defer strpool.deinit(&pool) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + state := init(mem.c_allocator) + defer deinit(&state) + try scan(&state, "yield yielding yield_1 .yield", &pool, &diagnostic_store) + expected [6]TokenKind :: [.yield, .ident, .ident, .dot, .yield, .eof] + try testing.expect_equal(expected.len, state.tokens.items.len) + for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind) + try testing.expect_equal("yield", strpool.get_str(&pool, state.tokens.items[0].data.str_id)?) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) +} diff --git a/source/lexer/token.hon b/source/lexer/token.hon index 189a2de..439bcdf 100644 --- a/source/lexer/token.hon +++ b/source/lexer/token.hon @@ -23,6 +23,7 @@ TokenKind :: enum { match inline proc, return + yield break, continue try, catch defer, errdefer diff --git a/source/lower/lower.hon b/source/lower/lower.hon index 7f28c1b..54d77d9 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -24,6 +24,15 @@ LoopTargets :: struct { cleanup_depth usize } +@hide:file +ValueTarget :: struct { + join ir.BlockId + slot u32 + type typecheck.Type + cleanup_depth usize + reached bool = false +} + @hide:file State :: struct { tree @ast.Tree @@ -40,6 +49,7 @@ State :: struct { block ir.BlockId = ir.BlockId(0) terminated bool = false loop ?LoopTargets = null + value_target ?@mut ValueTarget = null return_type typecheck.Type = typecheck.Type{} fallthrough_block ?ast.NodeId = null fallthrough_diagnostic ?diagnostics.DiagnosticId = null @@ -412,6 +422,7 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di state.block = ir.BlockId(0) state.terminated = false state.loop = null + state.value_target = null state.fallthrough_block = declaration.body state.fallthrough_diagnostic = typecheck.fallthrough_diagnostic(state.checked, id) state.return_type = typecheck.type_of(state.checked, declaration.return_type) @@ -559,8 +570,53 @@ lower_block proc(state @mut State, block ast.NodeId, fresh_scope bool) void ! (m if (!state.terminated) try lower_cleanups(state, cleanup_depth, ir.no_value) } +@hide:file +lower_value_block proc(state @mut State, block, owner ast.NodeId, type typecheck.Type) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + span :: ast.node_span(state.tree, block) + runtime :: lower_type(state, type) + shape :: try array_shape(state, type) + slot := u32(0) + if (runtime != .void) slot = u32(try ir.add_local(state.module, ir.FunctionId(state.function), runtime, shape)) + join :: try add_block(state) + reached :: try lower_value_body(state, block, owner, type, slot, join) + state.block = join + state.terminated = !reached + if (!reached) state.module.functions.items[state.function].blocks.items[usize(join)].terminator = ir.Terminator{ kind = .unreachable } + if (!reached or runtime == .void) return ir.no_value + return try emit(state, ir.Instruction{ op = .load_local, type = runtime, array_type = shape, slot = slot, span = span }) +} + +@hide:file +lower_value_body proc(state @mut State, block, owner ast.NodeId, type typecheck.Type, slot u32, join ir.BlockId) bool ! (mem.AllocError | diagnostics.CapacityError) { + if (trap_type(state, block)) return false + target := ValueTarget{ join = join, slot = slot, type = type, cleanup_depth = state.cleanups.items.len } + previous_target :: state.value_target + state.value_target = &target + defer state.value_target = previous_target + previous_fallthrough :: state.fallthrough_block + previous_diagnostic :: state.fallthrough_diagnostic + state.fallthrough_block = block + state.fallthrough_diagnostic = typecheck.fallthrough_diagnostic(state.checked, owner) + defer state.fallthrough_block = previous_fallthrough + defer state.fallthrough_diagnostic = previous_diagnostic + try predeclare_locals(state, block) + try lower_block(state, block, true) + if (!state.terminated) { + # checking permits an implicit result only for void blocks. + state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ + kind = .jump, target = join, span = ast.node_span(state.tree, block), + } + target.reached = true + state.terminated = true + } + return target.reached +} + @hide:file lower_cleanups proc(state @mut State, depth usize, error_value ir.ValueId) void ! (mem.AllocError | diagnostics.CapacityError) { + previous_target :: state.value_target + state.value_target = null + defer state.value_target = previous_target index := state.cleanups.items.len while (index > depth) { index -= 1 @@ -932,6 +988,23 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d span = declaration.span, }) } + .yield_stmt: { + statement :: ast.yield_stmt(tree, id) + if (state.value_target) |target| { + value :: try lower_as(state, statement.value?, target.type) + if (state.terminated or trap_type(state, id)) return + if (lower_type(state, target.type) != .void) _ = try emit(state, ir.Instruction{ + op = .store_local, left = value, slot = target.slot, span = statement.span, + }) + try lower_cleanups(state, target.cleanup_depth, ir.no_value) + if (state.terminated) return + state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ + kind = .jump, target = target.join, span = statement.span, + } + target.reached = true + state.terminated = true + } else if (!trap_type(state, id)) unreachable + } .return_stmt: { statement :: ast.return_stmt(tree, id) value := ir.no_value @@ -1067,7 +1140,6 @@ lower_match proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | local :: try ir.add_local(state.module, ir.FunctionId(state.function), lower_type(state, capture_type), try array_shape(state, capture_type)) state.storage[usize(capture)] = Storage{ kind = .local, index = u32(local) } } - if (!arm_view.expression) try predeclare_locals(state, arm_view.body) patterns :: ast.match_pattern_count(tree, arm) if (arm_view.catch_all) { state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ @@ -1119,7 +1191,10 @@ lower_match proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | if (!state.terminated and runtime != .void) _ = try emit(state, ir.Instruction{ op = .store_local, left = value, slot = result_slot, span = span, }) - } else try lower_block(state, arm_view.body, true) + } else { + reached :: try lower_value_body(state, arm_view.body, arm_view.body, type, result_slot, join) + if (reached) falls = true + } if (!state.terminated) { falls = true state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ @@ -1602,8 +1677,7 @@ lower_catch proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | } if (!trap_type(state, expression.handler)) { if (expression.handler_is_block) { - try predeclare_locals(state, expression.handler) - try lower_block(state, expression.handler, true) + _ = try lower_value_body(state, expression.handler, expression.handler, type, slot, join) } else { fallback :: try lower_as(state, expression.handler, type) if (!state.terminated and runtime != .void) _ = try emit(state, ir.Instruction{ @@ -1934,6 +2008,10 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr .enum_literal: { if (!trap_type(state, id)) unreachable } + .block_expr: { + if (trap_type(state, id)) return ir.no_value + return try lower_value_block(state, ast.block_expr(tree, id), id, typecheck.type_of(state.checked, id)) + } .match_expr: return try lower_match(state, id) .try_expr: return try lower_try(state, id) .catch_expr: return try lower_catch(state, id) diff --git a/source/parser/parse.hon b/source/parser/parse.hon index 612d02e..88b69d9 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -321,6 +321,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError .while: return try parse_while(state, allow_else) .for, .inline: return try parse_for(state, allow_else) .return: return try parse_return(state, allow_else) + .yield: return try parse_yield(state, allow_else) .break, .continue: return try parse_loop_control(state, allow_else) .defer, .errdefer: return try parse_cleanup(state, allow_else) .open_curly: return try ast.add_block_stmt(state.builder, try parse_block(state)) @@ -385,6 +386,31 @@ parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { ) } +@hide:file +parse_yield proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { + keyword :: try consume(state, .yield) + value :: if ( + next_is(state, .newline) or next_is(state, .close_curly) or next_is(state, .eof) + or (state.match_arm_body and next_is(state, .comma)) + or (allow_else and next_is(state, .else)) + ) { + diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, keyword.span) + yield try ast.add_invalid_expr(state.builder, keyword.span, diagnostic) + } else { + yield try parse_expr(state) + } + if ( + !next_is(state, .newline) and !next_is(state, .close_curly) and !next_is(state, .eof) + and !(allow_else and next_is(state, .else)) + and !(state.match_arm_body and next_is(state, .comma)) + ) return .unexpected_token + return try ast.add_yield_stmt(state.builder, ast.Span{ + start = keyword.span.start, + end = state.tokens[state.next_token - 1].span.end, + source = keyword.span.source, + }, value) +} + @hide:file parse_loop_control proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { keyword :: current(state) @@ -925,7 +951,7 @@ starts_type proc(kind lexer.TokenKind) bool { @hide:file member_name proc(kind lexer.TokenKind) bool { return match kind { - .ident, .if, .else, .for, .inline, .while, .proc, .return, .break, .continue, + .ident, .if, .else, .for, .inline, .while, .proc, .return, .yield, .break, .continue, .true, .false, .and, .or, .mut, .struct, .c_struct, .opaque, .enum, .union, .distinct, .alias, .match, .import, .test, .c_func, .null, .undefined, .try, .catch, .defer, .errdefer: true else: false @@ -1158,7 +1184,7 @@ parse_match_arm proc(state @mut State) ast.NodeId ! ParseError { state.match_arm_body = true defer state.match_arm_body = previous statement_body :: match current(state).kind { - .open_curly, .return, .break, .continue, .if, .while, .for, .inline, .defer, .errdefer: true + .open_curly, .return, .yield, .break, .continue, .if, .while, .for, .inline, .defer, .errdefer: true else: false } body ast.NodeId := undefined @@ -1701,6 +1727,41 @@ named_binding_starts proc(state @State) bool { ) } +@hide:file +brace_starts_block proc(state @State) bool { + first := state.next_token + 1 + while (first < state.tokens.len and state.tokens[first].kind == .newline) first += 1 + # empty records and braces without statement markers retain record syntax. + if (first >= state.tokens.len or state.tokens[first].kind == .close_curly) return false + depth := usize(0) + for (state.next_token)..(state.tokens.len) |index| { + kind :: state.tokens[index].kind + if (kind == .eof) return false + if (depth > 0 + and (state.tokens[first].kind == .if or state.tokens[first].kind == .open_curly) + and (kind == .yield or kind == .return or kind == .defer or kind == .errdefer) + and state.tokens[index - 1].kind != .dot + and !(index + 1 < state.tokens.len and state.tokens[index + 1].kind == .equal)) return true + if (depth == 1) { + if (kind == .double_colon or kind == .colon_equal) return true + previous :: if (index > first) state.tokens[index - 1].kind else lexer.TokenKind.open_curly + if (previous != .dot and !(index + 1 < state.tokens.len and state.tokens[index + 1].kind == .equal)) { + match kind { + .yield, .return, .break, .continue, .while, .for, .inline, .defer, .errdefer: return true + else: {} + } + } + } + if (kind == .open_curly or kind == .open_paren or kind == .open_bracket) depth += 1 + if (kind == .close_curly or kind == .close_paren or kind == .close_bracket) { + if (depth == 0) return false + depth -= 1 + if (depth == 0) return false + } + } + return false +} + @hide:file parse_primary proc(state @mut State) ast.NodeId ! ParseError { token :: current(state) @@ -1736,6 +1797,10 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError { }, member_id) } .open_curly: { + if (state.allow_record_literals and brace_starts_block(state)) { + body :: try parse_block(state) + return try ast.add_block_expr(state.builder, ast.builder_span(state.builder, body), body) + } if (state.allow_record_literals) return try parse_record_literal(state, null, token.span.start) diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, token.span) return try ast.add_invalid_expr(state.builder, token.span, diagnostic) @@ -1808,7 +1873,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError { @hide:file is_expression_boundary proc(kind lexer.TokenKind) bool { return match kind { - .close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .while, .for, .inline, .defer, .errdefer, .eof: true + .close_paren, .newline, .close_curly, .open_curly, .else, .return, .yield, .if, .while, .for, .inline, .defer, .errdefer, .eof: true else: false } } diff --git a/source/parser/parse.test.hon b/source/parser/parse.test.hon index 51be906..11ef796 100644 --- a/source/parser/parse.test.hon +++ b/source/parser/parse.test.hon @@ -2040,3 +2040,205 @@ keeps_match_default_arm_after_newline_single_statement_if test { try testing.expect(branch.else_block == null) try testing.expect(ast.match_arm_view(&tree, ast.match_arm(&tree, expression, 1)).catch_all) } + +parses_value_blocks_and_clones_yield_children test { + strings := strpool.init(mem.c_allocator) + defer strpool.deinit(&strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `run proc(flag bool) int { + ` result :: { + ` local :: 3 + ` { if (flag) yield local } + ` yield { yield 4 } + ` } + ` return if (flag) { yield result } else { yield 5 } + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &strings, &diagnostic_store) + builder := ast.init_builder(mem.c_allocator, source) + defer ast.deinit_builder(&builder) + root :: try parse_source(&builder, scan_state.tokens.items, &diagnostic_store) + original :: ast.view(&builder, root) + mapping :: try mem.alloc(ast.NodeId, mem.c_allocator, ast.node_count(&original)) + defer mem.free(mem.c_allocator, mapping) + mem.set(mapping, maxval!(ast.NodeId)) + cloned :: try ast.clone_subtree(&builder, root, mapping) + tree :: ast.view(&builder, root) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + roots [2]ast.NodeId :: [root, cloned] + for (roots) |package| { + function :: ast.function_decl(&tree, ast.child(&tree, package, 0)) + value :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 0)).initializer + try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, value)) + try testing.expect_equal(ast.NodeClass.expression, ast.node_class(&tree, value)) + block :: ast.block_expr(&tree, value) + try testing.expect_equal(3, ast.block_statement_count(&tree, block)) + ordinary :: ast.statement_block(&tree, ast.block_statement(&tree, block, 1)) + branch :: ast.if_stmt(&tree, ast.block_statement(&tree, ordinary, 0)) + first :: ast.block_statement(&tree, branch.then_block, 0) + try testing.expect_equal(ast.StmtKind.yield_stmt, ast.stmt_kind(&tree, first)) + try testing.expect_equal(ast.NodeClass.statement, ast.node_class(&tree, first)) + try testing.expect_equal("yield local", ast.source_text(&tree, ast.yield_stmt(&tree, first).span)) + last :: ast.yield_stmt(&tree, ast.block_statement(&tree, block, 2)) + try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, last.value?)) + nested :: ast.block_statement(&tree, ast.block_expr(&tree, last.value?), 0) + try testing.expect_equal(ast.ExprKind.literal_int, ast.expr_kind(&tree, ast.yield_stmt(&tree, nested).value?)) + conditional :: ast.return_stmt(&tree, ast.block_statement(&tree, function.body, 1)).value? + for (0..ast.match_arm_count(&tree, conditional)) |index| { + arm :: ast.match_arm_view(&tree, ast.match_arm(&tree, conditional, index)) + try testing.expect(arm.expression) + try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, arm.body)) + } + } + first_function :: ast.function_decl(&tree, ast.child(&tree, root, 0)) + first_value :: ast.statement_decl(&tree, ast.block_statement(&tree, first_function.body, 0)).initializer + copied_value :: mapping[usize(first_value)] + try testing.expect(copied_value != first_value) + try testing.expect_equal(mapping[usize(ast.block_expr(&tree, first_value))], ast.block_expr(&tree, copied_value)) +} + +parses_value_blocks_starting_with_named_assignments test { + strings := strpool.init(mem.c_allocator) + defer strpool.deinit(&strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `main proc() i16 { + ` value ?i16 :: 42 + ` return if value |payload| { payload = 7 + ` yield payload + ` } else 0 + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &strings, &diagnostic_store) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + function :: ast.function_decl(&tree, ast.package_decl(&tree, 0)) + conditional :: ast.return_stmt(&tree, ast.block_statement(&tree, function.body, 1)).value? + arm :: ast.match_arm_view(&tree, ast.match_arm(&tree, conditional, 0)) + try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, arm.body)) + block :: ast.block_expr(&tree, arm.body) + try testing.expect_equal(2, ast.block_statement_count(&tree, block)) + first :: ast.block_statement(&tree, block, 0) + try testing.expect_equal(ast.StmtKind.assignment_stmt, ast.stmt_kind(&tree, first)) + assignment :: ast.assignment_stmt(&tree, first) + try testing.expect_equal("payload", ast.source_text(&tree, ast.node_span(&tree, assignment.target))) + try testing.expect_equal("7", ast.source_text(&tree, ast.node_span(&tree, assignment.value))) + last :: ast.block_statement(&tree, block, 1) + try testing.expect_equal(ast.StmtKind.yield_stmt, ast.stmt_kind(&tree, last)) + try testing.expect_equal("payload", ast.source_text(&tree, ast.node_span(&tree, ast.yield_stmt(&tree, last).value?))) +} + +preserves_record_literals_alongside_value_blocks test { + strings := strpool.init(mem.c_allocator) + defer strpool.deinit(&strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `empty :: {} + `single :: {1} + `pair :: {1, 2} + `keyed :: { value = { yield 3 } } + `keyword :: { yield = 4 } + `choose :: if (true) { value = 7 } else { value = 8 } + `conditional :: {if (true) 1 else 2} + `block :: { if (true) yield 9 } + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &strings, &diagnostic_store) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + try testing.expect_equal(8, ast.package_decl_count(&tree)) + for (usize(0)..usize(5)) |index| { + value :: ast.statement_decl(&tree, ast.package_decl(&tree, index)).initializer + try testing.expect_equal(ast.ExprKind.record_literal, ast.expr_kind(&tree, value)) + } + choose :: ast.statement_decl(&tree, ast.package_decl(&tree, 5)).initializer + for (usize(0)..usize(2)) |index| { + arm :: ast.match_arm_view(&tree, ast.match_arm(&tree, choose, index)) + try testing.expect_equal(ast.ExprKind.record_literal, ast.expr_kind(&tree, arm.body)) + } + conditional :: ast.statement_decl(&tree, ast.package_decl(&tree, 6)).initializer + try testing.expect_equal(ast.ExprKind.record_literal, ast.expr_kind(&tree, conditional)) + block :: ast.statement_decl(&tree, ast.package_decl(&tree, 7)).initializer + try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, block)) +} + +requires_yield_values_and_recovers_following_statements test { + strings := strpool.init(mem.c_allocator) + defer strpool.deinit(&strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `run proc() int { + ` value :: { + ` yield + ` after :: 2 + ` if (true) yield else yield 3 + ` yield 4 5 + ` yield after + ` } + ` return value + `} + `tail :: 7 + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &strings, &diagnostic_store) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(3, diagnostics.count(&diagnostic_store)) + try testing.expect_equal(2, ast.package_decl_count(&tree)) + function :: ast.function_decl(&tree, ast.package_decl(&tree, 0)) + value :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 0)).initializer + block :: ast.block_expr(&tree, value) + try testing.expect_equal(5, ast.block_statement_count(&tree, block)) + missing :: ast.yield_stmt(&tree, ast.block_statement(&tree, block, 0)) + try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, missing.value?)) + try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, block, 1))) + try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, ast.block_statement(&tree, block, 3))) + last :: ast.yield_stmt(&tree, ast.block_statement(&tree, block, 4)) + try testing.expect_equal("after", ast.source_text(&tree, ast.node_span(&tree, last.value?))) +} + +keeps_yielding_match_and_catch_bodies_as_raw_blocks test { + strings := strpool.init(mem.c_allocator) + defer strpool.deinit(&strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `run proc() int { + ` selected :: match 1 { 1: { yield 2 }, else: yield 3 } + ` recovered :: selected catch |err| { yield err } + ` return use({ yield recovered }, $({ yield 4 })) + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &strings, &diagnostic_store) + tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store) + defer ast.deinit(&tree) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + function :: ast.function_decl(&tree, ast.package_decl(&tree, 0)) + selected :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 0)).initializer + for (usize(0)..usize(2)) |index| { + arm :: ast.match_arm_view(&tree, ast.match_arm(&tree, selected, index)) + try testing.expect(!arm.expression) + try testing.expect_equal(ast.NodeClass.block, ast.node_class(&tree, arm.body)) + try testing.expect_equal(ast.StmtKind.yield_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, arm.body, 0))) + } + recovered :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 1)).initializer + handler :: ast.catch_expr(&tree, recovered) + try testing.expect(handler.handler_is_block) + try testing.expect_equal(ast.NodeClass.block, ast.node_class(&tree, handler.handler)) + try testing.expect_equal(ast.StmtKind.yield_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, handler.handler, 0))) + call :: ast.return_stmt(&tree, ast.block_statement(&tree, function.body, 2)).value? + try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, ast.call_arg(&tree, call, 0))) + constant :: ast.call_arg(&tree, call, 1) + try testing.expect_equal(ast.ExprKind.comptime_expr, ast.expr_kind(&tree, constant)) + try testing.expect_equal(ast.ExprKind.block_expr, ast.expr_kind(&tree, ast.comptime_operand(&tree, constant))) +} diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 76709b0..9342a6d 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -122,7 +122,7 @@ Storage :: struct { allocator mem.Allocator } -#! owns node-indexed types, aggregate descriptors, initializer mappings, and function fallthrough obligations; release with deinit. +#! owns node-indexed types, aggregate descriptors, initializer mappings, and function/value-block fallthrough obligations; release with deinit. #! borrows the caller-owned AST for signature comparisons; diagnostic IDs refer to the caller-owned store. Result :: struct { storage Storage } @@ -140,6 +140,8 @@ Role :: enum { invalid, body, return_stmt, + yield_stmt, + value_body, if_stmt, assignment_stmt, while_stmt, @@ -186,6 +188,9 @@ Cell :: struct { enclosing_arm ast.NodeId = ast.NodeId(maxval!(u32)) match_owner ast.NodeId = ast.NodeId(maxval!(u32)) narrowed_type Type = Type{} + yield_owner ast.NodeId = ast.NodeId(maxval!(u32)) + first_yield ast.NodeId = ast.NodeId(maxval!(u32)) + next_yield ast.NodeId = ast.NodeId(maxval!(u32)) } @hide:file @@ -226,6 +231,7 @@ State :: struct { collecting_assignment bool = false has_assignments bool = false pointer_depth usize = 0 + yield_owner ast.NodeId = ast.NodeId(maxval!(u32)) } #! borrows `resolution` and `diagnostic_store` during checking; Result retains the AST borrow for signature comparisons. @@ -599,6 +605,28 @@ check_elaborated proc( } } } + for 0..count |i| { + id :: ast.NodeId(i) + explicit :: cells[i].role == .expression and ast.expr_kind(tree, id) == .block_expr + if (!explicit and cells[i].role != .value_body) continue + result :: types[i] + if (result.kind == .none or result.kind == .void or result.kind == .invalid) continue + body :: if (explicit) ast.block_expr(tree, id) else id + if (!block_falls_through(&state, body)) continue + diagnostic :: try diagnostics.add(diagnostic_store, .missing_yield, ast.node_span(tree, body)) + hashmap.put(&fallthroughs, id, diagnostic) catch |err| { + match err { + .key_exists: unreachable + else: return err + } + } + if (explicit) hashmap.put(&fallthroughs, body, diagnostic) catch |err| { + match err { + .key_exists: unreachable + else: return err + } + } + } return Result{ storage = Storage{ tree = tree, types = types, @@ -622,10 +650,10 @@ type_of proc(result @Result, id ast.NodeId) Type { return result.storage.types[usize(id)] } -#! returns the original diagnostic for a function's nonvoid fallthrough, independent of body errors. -#! null means the function has no fallthrough obligation; lowering still owns trap placement. -fallthrough_diagnostic proc(result @Result, function ast.NodeId) ?diagnostics.DiagnosticId { - return hashmap.get(&result.storage.fallthroughs, function) +#! returns the original diagnostic for a function or value block's nonvoid fallthrough. +#! null means there is no fallthrough obligation; lowering still owns trap placement. +fallthrough_diagnostic proc(result @Result, owner ast.NodeId) ?diagnostics.DiagnosticId { + return hashmap.get(&result.storage.fallthroughs, owner) } array_type_count proc(result @Result) usize { @@ -824,6 +852,8 @@ join proc(state @mut State, left, right ast.NodeId) void { @hide:file depend proc(state @mut State, source, dependent ast.NodeId) void ! mem.AllocError { + # a value block keeps its result type when only an unexecuted path is invalid. + if (state.cells[usize(dependent)].role == .value_body) return next :: state.cells[usize(source)].first_edge try arraylist.append(&state.edges, Edge{ dependent = dependent, next = next }) state.cells[usize(source)].first_edge = state.edges.items.len - 1 @@ -1035,6 +1065,17 @@ collect_capture proc(state @mut State, id, body ast.NodeId) void ! mem.AllocErro try depend(state, id, body) } +@hide:file +collect_value_block proc(state @mut State, body, owner ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + saved_owner :: state.yield_owner + state.yield_owner = owner + defer state.yield_owner = saved_owner + # both explicit and implicit bodies preserve errors on their executed paths. + state.cells[usize(body)].role = .value_body + state.cells[usize(body)].assignment_value = state.collecting_assignment + try collect_block(state, body, state.return_annotation, state.in_loop) +} + @hide:file collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop bool) void ! (mem.AllocError | diagnostics.CapacityError) { saved_annotation :: state.return_annotation @@ -1046,7 +1087,7 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b state.in_loop = saved_loop } tree :: state.tree - state.cells[usize(id)].role = .body + if (state.cells[usize(id)].role == .none) state.cells[usize(id)].role = .body for (0..ast.block_statement_count(tree, id)) |i| { statement :: ast.block_statement(tree, id, i) match ast.stmt_kind(tree, statement) { @@ -1067,6 +1108,23 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b try depend(state, value, statement) } } + .yield_stmt: { + state.cells[usize(statement)].role = .yield_stmt + state.cells[usize(statement)].type = Type{ kind = .void } + owner :: state.yield_owner + state.cells[usize(statement)].yield_owner = owner + if (owner == ast.NodeId(maxval!(u32))) { + code :: if (state.in_cleanup) diagnostics.Code.cleanup_control_flow else diagnostics.Code.yield_outside_block + try fail(state, statement, code, ast.node_span(tree, statement)) + } else { + state.cells[usize(statement)].next_yield = state.cells[usize(owner)].first_yield + state.cells[usize(owner)].first_yield = statement + } + if (ast.yield_stmt(tree, statement).value) |value| { + try collect_expression(state, value) + try depend(state, value, statement) + } + } .if_stmt: { conditional :: ast.if_stmt(tree, statement) state.cells[usize(statement)].role = .if_stmt @@ -1165,7 +1223,7 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b cleanup :: ast.cleanup_stmt(tree, statement) state.cells[usize(statement)].role = .cleanup state.cells[usize(statement)].type = Type{ kind = .void } - result :: state.types[usize(return_annotation)] + result :: if (return_annotation == ast.NodeId(maxval!(u32))) Type{} else state.types[usize(return_annotation)] if (ast.stmt_kind(tree, statement) == .errdefer_stmt and result.kind != .fallible and result.kind != .invalid) { state.cells[usize(statement)].type = invalid_type(try diagnostics.add( state.diagnostics, .errdefer_outside_fallible, cleanup.span, @@ -1177,9 +1235,12 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b state.arrays.items[usize(result.array_type)].error_type else result } saved_cleanup :: state.in_cleanup + saved_owner :: state.yield_owner + state.yield_owner = ast.NodeId(maxval!(u32)) state.in_cleanup = true try collect_block(state, cleanup.body, return_annotation, false) state.in_cleanup = saved_cleanup + state.yield_owner = saved_owner } .break_stmt, .continue_stmt: { state.cells[usize(statement)].role = .loop_control @@ -1198,7 +1259,7 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b .invalid: collect_invalid(state, statement) } source :: if (ast.stmt_kind(tree, statement) == .expression) ast.statement_expr(tree, statement) else statement - try depend(state, source, id) + if (state.cells[usize(id)].role != .value_body) try depend(state, source, id) } } @@ -1212,6 +1273,9 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError state.cells[usize(id)].in_cleanup = state.in_cleanup state.cells[usize(id)].narrowing_arm = state.narrowing_arm match ast.expr_kind(tree, id) { + .block_expr: { + try collect_value_block(state, ast.block_expr(tree, id), id) + } .comptime_expr: try fail(state, id, .comptime_argument_required, ast.node_span(tree, id)) .typed_constant: { value :: ast.typed_constant(tree, id) @@ -1235,7 +1299,8 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError state.cells[usize(capture)].type = Type{} } if (expression.handler_is_block) { - try collect_block(state, expression.handler, state.return_annotation, state.in_loop) + try collect_value_block(state, expression.handler, expression.handler) + try depend(state, expression.handler, id) } else try collect_expression(state, expression.handler) } .literal_int: { @@ -1615,11 +1680,20 @@ demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Cod return } } + if ( + state.cells[usize(id)].role == .value_body + or (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .block_expr) + ) { + state.cells[index].expected = expected + try demand_yields(state, id, expected, code) + seed(state, id, expected) + return + } if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .match_expr) { state.cells[index].expected = expected for 0..ast.match_arm_count(state.tree, id) |i| { arm :: ast.match_arm_view(state.tree, ast.match_arm(state.tree, id, i)) - if (arm.expression) try demand(state, arm.body, expected, code) + try demand(state, arm.body, expected, code) } return } @@ -1981,9 +2055,23 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost try validate_match(state, id) return } + if (state.cells[usize(id)].role == .yield_stmt) { + owner :: state.cells[usize(id)].yield_owner + if (owner == ast.NodeId(maxval!(u32)) or state.types[usize(id)].kind == .invalid) return + expected :: state.types[usize(owner)] + if (expected.kind == .none or expected.kind == .invalid) return + if (ast.yield_stmt(tree, id).value) |value| { + actual :: state.types[usize(value)] + if (actual.kind != .invalid and !assignable_value(state, value, actual, expected)) { + try fail(state, value, .yield_type_mismatch, ast.node_span(tree, value)) + } + } + return + } match state.cells[usize(id)].role { .declaration: { declaration :: ast.statement_decl(tree, id) + if (!expression_falls_through(state, declaration.initializer)) return if (declaration.type == null and state.types[usize(declaration.initializer)].kind == .null) { try fail(state, declaration.initializer, .cannot_infer_type, ast.node_span(tree, declaration.initializer)) } @@ -2013,6 +2101,10 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost } .return_stmt: { statement :: ast.return_stmt(tree, id) + if (state.cells[usize(id)].return_annotation == ast.NodeId(maxval!(u32))) { + try fail(state, id, .return_type_mismatch, statement.span) + return + } expected :: state.types[usize(state.cells[usize(id)].return_annotation)] if (expected.kind == .invalid) return if (state.types[usize(id)].kind == .invalid) return @@ -2194,38 +2286,85 @@ validate_call proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | dia } +@hide:file +BlockFlow :: struct { + falls bool = true + yields bool = false +} + @hide:file block_falls_through proc(state @State, id ast.NodeId) bool { + return block_flow(state, id).falls +} + +@hide:file +value_block_completes proc(state @State, id ast.NodeId) bool { + flow :: block_flow(state, id) + return flow.falls or flow.yields +} + +@hide:file +expression_falls_through proc(state @State, id ast.NodeId) bool { tree :: state.tree + match ast.expr_kind(tree, id) { + .block_expr: return value_block_completes(state, ast.block_expr(tree, id)) + .match_expr: { + if (!state.exhaustive[usize(id)]) return true + for 0..ast.match_arm_count(tree, id) |i| { + arm :: ast.match_arm_view(tree, ast.match_arm(tree, id, i)) + if (arm.expression) { + if (expression_falls_through(state, arm.body)) return true + } else if (value_block_completes(state, arm.body)) return true + } + return false + } + else: return true + } +} + +@hide:file +block_flow proc(state @State, id ast.NodeId) BlockFlow { + tree :: state.tree + result := BlockFlow{} for (0..ast.block_statement_count(tree, id)) |i| { statement :: ast.block_statement(tree, id, i) kind :: ast.stmt_kind(tree, statement) source :: if (kind == .expression) ast.statement_expr(tree, statement) else statement - if (state.types[usize(source)].kind == .invalid) return false + if (state.types[usize(source)].kind == .invalid) { + result.falls = false + return result + } match kind { - .return_stmt, .break_stmt, .continue_stmt, .invalid: return false - .block_stmt: if (!block_falls_through(state, ast.statement_block(tree, statement))) return false + .yield_stmt: { + result.yields = true + result.falls = false + } + .return_stmt, .break_stmt, .continue_stmt, .invalid: result.falls = false + .block_stmt: { + nested :: block_flow(state, ast.statement_block(tree, statement)) + result.yields = result.yields or nested.yields + result.falls = nested.falls + } .if_stmt: { conditional :: ast.if_stmt(tree, statement) - then_falls :: block_falls_through(state, conditional.then_block) + then_flow :: block_flow(state, conditional.then_block) + result.yields = result.yields or then_flow.yields if (conditional.else_block) |other| { - if (!then_falls and !block_falls_through(state, other)) return false - } - } - .expression: { - if (ast.expr_kind(tree, source) == .match_expr and state.exhaustive[usize(source)]) { - falls := false - for 0..ast.match_arm_count(tree, source) |j| { - arm :: ast.match_arm_view(tree, ast.match_arm(tree, source, j)) - if (arm.expression or block_falls_through(state, arm.body)) falls = true - } - if (!falls) return false + else_flow :: block_flow(state, other) + result.yields = result.yields or else_flow.yields + result.falls = then_flow.falls or else_flow.falls } } + .while_stmt: result.yields = result.yields or block_flow(state, ast.while_stmt(tree, statement).body).yields + .for_stmt: result.yields = result.yields or block_flow(state, ast.for_stmt(tree, statement).body).yields + .expression: result.falls = expression_falls_through(state, source) + .declaration: result.falls = expression_falls_through(state, ast.statement_decl(tree, statement).initializer) + .assignment_stmt: result.falls = expression_falls_through(state, ast.assignment_stmt(tree, statement).value) else: {} } + if (!result.falls) return result } - return true + return result } @hide:file @@ -2802,6 +2941,37 @@ unify_elements proc(state @mut State, left, right ast.NodeId) void ! (mem.AllocE if (right_type.kind == .array or right_type.kind == .record) try demand(state, left, right_type, .type_mismatch) } +@hide:file +demand_yields proc(state @mut State, owner ast.NodeId, expected Type, code diagnostics.Code) void ! (mem.AllocError | diagnostics.CapacityError) { + yield_id := state.cells[usize(owner)].first_yield + while (yield_id != ast.NodeId(maxval!(u32))) { + if (ast.yield_stmt(state.tree, yield_id).value) |value| try demand(state, value, expected, code) + yield_id = state.cells[usize(yield_id)].next_yield + } +} + +@hide:file +infer_value_block proc(state @mut State, owner ast.NodeId, defaults bool) Type ! (mem.AllocError | diagnostics.CapacityError) { + expected := state.cells[root(state, usize(owner))].expected + yield_id := state.cells[usize(owner)].first_yield + while (yield_id != ast.NodeId(maxval!(u32))) { + if (ast.yield_stmt(state.tree, yield_id).value) |value| { + if (expected.kind != .none) try demand(state, value, expected, .yield_type_mismatch) + actual :: try infer_shape(state, value, defaults) + if (expected.kind == .none and actual.kind != .none and actual.kind != .invalid) expected = actual + } + yield_id = state.cells[usize(yield_id)].next_yield + } + if (expected.kind == .none and defaults and state.cells[usize(owner)].first_yield == ast.NodeId(maxval!(u32))) { + expected = Type{ kind = .void } + } + if (expected.kind != .none) { + try demand_yields(state, owner, expected, .yield_type_mismatch) + seed(state, owner, expected) + } + return group_type(state, owner) +} + @hide:file infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.AllocError | diagnostics.CapacityError) { index :: root(state, usize(id)) @@ -2809,6 +2979,10 @@ 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 == .value_body + or (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .block_expr) + ) return try infer_value_block(state, id, defaults) 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) @@ -3186,7 +3360,7 @@ apply_contexts proc(state @mut State, assignment_values, early bool) void ! (mem view :: ast.statement_decl(tree, id) if (view.type) |annotation| try context_demand(state, view.initializer, state.types[usize(annotation)], .type_mismatch, early) } - if (!assignment_values and state.cells[i].role == .return_stmt) { + if (!assignment_values and state.cells[i].role == .return_stmt and state.cells[i].return_annotation != ast.NodeId(maxval!(u32))) { if (ast.return_stmt(tree, id).value) |value| { try context_demand(state, value, state.types[usize(state.cells[i].return_annotation)], .return_type_mismatch, early) } @@ -4130,7 +4304,7 @@ collect_match proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | dia if (arm.expression) { try collect_expression(state, arm.body) } else { - try collect_block(state, arm.body, state.return_annotation, state.in_loop) + try collect_value_block(state, arm.body, arm.body) } state.narrowing_arm = saved_arm } @@ -4173,7 +4347,6 @@ infer_match proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All result :: root(state, usize(id)) expected := state.cells[result].expected has_block := false - has_value := false for 0..ast.match_arm_count(state.tree, id) |i| { arm_id :: ast.match_arm(state.tree, id, i) arm :: ast.match_arm_view(state.tree, arm_id) @@ -4221,20 +4394,16 @@ infer_match proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All } } } - if (arm.expression) { - has_value = true + if (arm.expression or state.cells[usize(arm.body)].first_yield != ast.NodeId(maxval!(u32))) { actual :: try infer_shape(state, arm.body, defaults) if (expected.kind == .none and actual.kind != .none and actual.kind != .invalid) expected = actual } else has_block = true } - if (has_block and !has_value) expected = Type{ kind = .void } - if (has_block and has_value and defaults) { - try fail(state, id, .type_mismatch, ast.node_span(state.tree, id)) - } + if (expected.kind == .none and has_block and defaults) expected = Type{ kind = .void } if (expected.kind != .none) { for 0..ast.match_arm_count(state.tree, id) |i| { arm :: ast.match_arm_view(state.tree, ast.match_arm(state.tree, id, i)) - if (arm.expression) try demand(state, arm.body, expected, .type_mismatch) + try demand(state, arm.body, expected, .type_mismatch) } seed(state, id, expected) } @@ -4685,10 +4854,8 @@ infer_fallible_expression proc(state @mut State, id ast.NodeId, defaults bool) T if (catching) { expression :: ast.catch_expr(state.tree, id) if (expression.capture) |capture| seed_capture(state, capture, descriptor.error_type) - if (!expression.handler_is_block) { - try demand(state, expression.handler, descriptor.element, .catch_type_mismatch) - _ = try infer_shape(state, expression.handler, defaults) - } + try demand(state, expression.handler, descriptor.element, .catch_type_mismatch) + _ = try infer_shape(state, expression.handler, defaults) } return group_type(state, id) } @@ -4706,9 +4873,7 @@ validate_fallible_expression proc(state @mut State, id ast.NodeId) void ! (mem.A descriptor :: state.arrays.items[usize(source.array_type)] if (catching) { expression :: ast.catch_expr(state.tree, id) - if (expression.handler_is_block) { - if (descriptor.element.kind != .void and block_falls_through(state, expression.handler)) try fail(state, expression.handler, .catch_type_mismatch, ast.node_span(state.tree, expression.handler)) - } else { + if (!expression.handler_is_block) { actual :: state.types[usize(expression.handler)] if (actual.kind != .invalid and !assignable_value(state, expression.handler, actual, descriptor.element)) try fail(state, expression.handler, .catch_type_mismatch, ast.node_span(state.tree, expression.handler)) } diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index 32aef60..dcdbf71 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -411,6 +411,7 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me .return_stmt: { if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, scope, value) } + .yield_stmt: try resolve_expr(state, scope, ast.yield_stmt(tree, statement).value?) .break_stmt, .continue_stmt: {} .block_stmt: try resolve_arm(state, scope, ast.statement_block(tree, statement)) .defer_stmt, .errdefer_stmt: { @@ -539,6 +540,7 @@ resolve_expr proc( tree :: state.tree match ast.expr_kind(tree, id) { .comptime_expr: try resolve_expr(state, scope, ast.comptime_operand(tree, id)) + .block_expr: try resolve_arm(state, scope, ast.block_expr(tree, id)) .typed_constant: { value :: ast.typed_constant(tree, id) try resolve_expr(state, scope, value.type) diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 6b66630..ada4b76 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -98,6 +98,7 @@ State :: struct { evaluation_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic type_execution_depth usize = 0 type_execution_remaining usize = 0 + value_expansion_depth usize = 0 } #! elaborates templates into cached concrete declarations in the caller-owned `builder` arena. @@ -121,7 +122,9 @@ elaborate proc( for (0..ast.node_count(&original)) |i| { id :: ast.NodeId(i) class :: ast.node_class(&original, id) - if (class == .expression and ast.expr_kind(&original, id) == .comptime_expr) needed = true + if (class == .expression + and (ast.expr_kind(&original, id) == .comptime_expr or ast.expr_kind(&original, id) == .block_expr)) + needed = true if (class == .statement and ast.stmt_kind(&original, id) == .for_stmt and ast.for_stmt(&original, id).is_inline) needed = true if (class == .match_arm and ast.match_arm_view(&original, id).is_inline) needed = true @@ -237,6 +240,42 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { if (state.expanded.items[usize(id)] != 0) return state.expanded.items[usize(id)] = 1 class :: ast.node_class(&state.tree, id) + if (class == .expression and ast.expr_kind(&state.tree, id) == .block_expr) { + body :: ast.block_expr(&state.tree, id) + annotation :: try value_block_type(state, body, 0) + if (is_type(state, annotation) or type_record_storage(state, annotation, 0)) { + selected :: try evaluate_type_value_block(state, id, annotation) + if (selected == no_node) { + try reject_evaluation(state, id) + return + } + try replace(state, id, selected) + try refresh(state) + state.bindings.items[usize(id)] = binding(state, selected) + return + } + state.value_expansion_depth += 1 + defer state.value_expansion_depth -= 1 + try expand(state, body) + return + } + if (class == .block and state.value_expansion_depth > 0) { + statements := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&statements) + count :: ast.block_statement_count(&state.tree, id) + for (0..count) |i| { + statement :: ast.block_statement(&state.tree, id, i) + try expand(state, statement) + try arraylist.append(&statements, statement) + if (value_block_terminal(state, statement)) break + } + if (statements.items.len != count) { + replacement :: try ast.add_block(state.builder, ast.node_span(&state.tree, id), statements.items) + try replace(state, id, replacement) + try refresh(state) + } + return + } if (class == .statement and ast.stmt_kind(&state.tree, id) == .for_stmt and ast.for_stmt(&state.tree, id).is_inline) { try expand_inline_for(state, id) @@ -437,6 +476,29 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { } } +@hide:file +value_block_terminal proc(state @State, id ast.NodeId) bool { + class :: ast.node_class(&state.tree, id) + if (class == .block) { + count :: ast.block_statement_count(&state.tree, id) + return count > 0 and value_block_terminal(state, ast.block_statement(&state.tree, id, count - 1)) + } + if (class != .statement) return false + match ast.stmt_kind(&state.tree, id) { + .yield_stmt, .return_stmt, .break_stmt, .continue_stmt: return true + .block_stmt: return value_block_terminal(state, ast.statement_block(&state.tree, id)) + .if_stmt: { + view :: ast.if_stmt(&state.tree, id) + return ( + view.else_block != null + and value_block_terminal(state, view.then_block) + and value_block_terminal(state, view.else_block?) + ) + } + else: return false + } +} + # reflection values are owned AST snapshots. only ordinary leaves and selected # runtime bodies enter IR; metadata slices never retain interpreter addresses. @hide:file @@ -1369,16 +1431,18 @@ specialize_match proc(state @mut State, id, statement ast.NodeId) bool ! Error { return true } arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, id, index)) - selected :: try type_match_body(state, id, arm) - if (selected == no_node or (statement == no_node and !arm.expression)) { + selected := try type_match_body(state, id, arm) + if (selected != no_node and !arm.expression) { + selected = try ast.add_block_expr(state.builder, ast.node_span(&state.tree, selected), selected) + try refresh(state) + } + if (selected == no_node) { try reject_evaluation(state, id) return true } try expand(state, selected) if (statement != no_node) { - replacement := no_node - if (arm.expression) replacement = try ast.add_expression_stmt(state.builder, selected) - else replacement = try ast.add_block_stmt(state.builder, selected) + replacement :: try ast.add_expression_stmt(state.builder, selected) try replace(state, statement, replacement) } else { resolved :: binding(state, selected) @@ -1891,7 +1955,7 @@ contextual_literal proc(state @State, id ast.NodeId) bool { kind :: ast.expr_kind(&state.tree, id) if (kind == .builtin_call) return mem.eql(ast.literal_text(&state.tree, ast.builtin_call(&state.tree, id).name), "some") - return kind == .record_literal or kind == .array_literal or kind == .enum_literal + return kind == .record_literal or kind == .array_literal or kind == .enum_literal or kind == .block_expr } @hide:file @@ -1943,7 +2007,9 @@ evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Erro type :: try canonical_type(state, annotation) if (type != no_node and ast.expr_kind(&state.tree, type) == .array_type and !try type_array_initialized(state, id, id)) return Value{} - if (type != no_node and (contextual_literal(state, id) or ast.expr_kind(&state.tree, type) == .fallible_type)) + if (type != no_node and (contextual_literal(state, id) + or ast.expr_kind(&state.tree, id) == .block_expr + or ast.expr_kind(&state.tree, type) == .fallible_type)) return try evaluate_ir(state, id, type) } return try evaluate(state, id) @@ -2264,13 +2330,21 @@ TypeExecution :: struct { first_owned ast.NodeId returned ast.NodeId = no_node result_type ast.NodeId + yielded ast.NodeId = no_node + yield_type ast.NodeId = no_node + yield_depth usize = 0 + cleanup_yield_depth usize = 0 + cleanup_depth usize = 0 + cleanup_loop_depth usize = 0 + evaluate_values bool = false + pending_flow TypeFlow = .next loop_depth usize = 0 inline_body ast.NodeId = no_node inline_blocks ?@mut arraylist.ArrayList(ast.NodeId) = null } @hide:file -TypeFlow :: enum { next, returned, break, continue, invalid } +TypeFlow :: enum { next, returned, yielded, break, continue, invalid } # type values cannot enter runtime IR. execute their control flow here, substituting # owned constants into snapshots; ordinary value operations still use checked IR. @@ -2283,6 +2357,7 @@ return_type_value proc(state @mut State, block, first_owned, result_type ast.Nod mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)), first_owned = first_owned, result_type = result_type, + evaluate_values = true, } defer arraylist.deinit(&execution.locals) defer mem.free(state.allocator, execution.mapping) @@ -2291,6 +2366,56 @@ return_type_value proc(state @mut State, block, first_owned, result_type ast.Nod return execution.returned } +@hide:file +evaluate_type_value_block proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { + if (state.type_execution_depth == 0) state.type_execution_remaining = 4096 + state.type_execution_depth += 1 + defer state.type_execution_depth -= 1 + execution := TypeExecution{ + locals = arraylist.init(state.allocator), + mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)), + first_owned = ast.NodeId(0), + result_type = no_node, + evaluate_values = true, + } + defer arraylist.deinit(&execution.locals) + 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) +} + +@hide:file +execute_type_value_block proc( + state @mut State, execution @mut TypeExecution, block, annotation ast.NodeId, +) ast.NodeId ! Error { + saved_value :: execution.yielded + saved_type :: execution.yield_type + execution.yielded = no_node + execution.yield_type = annotation + execution.yield_depth += 1 + defer { + execution.yielded = saved_value + execution.yield_type = saved_type + execution.yield_depth -= 1 + } + flow :: try execute_type_block(state, execution, block) + if (flow == .yielded) return execution.yielded + if (flow != .next) execution.pending_flow = flow + else { + inferred :: if (annotation == no_node) try value_block_type(state, block, 0) else annotation + result :: binding(state, inferred) + if (result.kind == .builtin and result.data.builtin == .void) { + value :: try ast.add_literal(state.builder, .void, ast.node_span(&state.tree, block)) + try refresh(state) + return value + } + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .missing_yield, ast.node_span(&state.tree, block)) + execution.pending_flow = .invalid + } + return no_node +} + @hide:file type_step proc(state @mut State, at ast.NodeId) bool ! Error { if (state.type_execution_remaining == 0) { @@ -2303,8 +2428,8 @@ type_step proc(state @mut State, at ast.NodeId) bool ! Error { } @hide:file -prepare_type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) void { - if (usize(id) >= execution.mapping.len) return +prepare_type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) void ! Error { + if (id == no_node or usize(id) >= execution.mapping.len or execution.pending_flow != .next) return # substituted arguments belong to the caller, including nominal type identity. if (id < execution.first_owned) { execution.mapping[usize(id)] = id @@ -2320,8 +2445,73 @@ prepare_type_snapshot proc(state @mut State, execution @mut TypeExecution, id as } } } + if (execution.evaluate_values and ast.node_class(&state.tree, id) == .field) { + field :: ast.field(&state.tree, id) + if (field.type != null and field.value != null) { + annotation :: try type_snapshot(state, execution, field.type?) + if (annotation == no_node) return + value :: try type_snapshot_expected(state, execution, field.value?, annotation) + execution.mapping[usize(field.value?)] = value + if (value == no_node and execution.pending_flow == .next) 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 == .block_expr) { + execution.mapping[usize(id)] = try execute_type_value_block(state, execution, + ast.block_expr(&state.tree, id), no_node) + if (execution.mapping[usize(id)] == no_node and execution.pending_flow == .next) + execution.pending_flow = .invalid + return + } + if (kind == .match_expr) { + execution.mapping[usize(id)] = try type_snapshot_match(state, execution, id, no_node) + if (execution.mapping[usize(id)] == no_node and execution.pending_flow == .next) + execution.pending_flow = .invalid + return + } + if (kind == .binary) { + view :: ast.binary_expr(&state.tree, id) + if (view.operator == .logical_and or view.operator == .logical_or) { + boolean :: try builtin(state, id, .bool) + left :: try type_snapshot_expected(state, execution, view.left, boolean) + if (left == no_node) return + value :: try evaluate_ir(state, left, boolean) + if (value.kind != .boolean) { + execution.pending_flow = .invalid + return + } + right := no_node + if (value.boolean == (view.operator == .logical_or)) { + saved :: execution.evaluate_values + execution.evaluate_values = false + right = try type_snapshot(state, execution, view.right) + if (right != no_node and !try skip_type_value_blocks(state, right)) { + execution.pending_flow = .invalid + return + } + execution.evaluate_values = saved + } else right = try type_snapshot_expected(state, execution, view.right, boolean) + if (right == no_node and execution.pending_flow == .next) { + execution.pending_flow = .invalid + return + } + # retain the operator and skipped operand for ordinary type validation. + execution.mapping[usize(view.left)] = left + execution.mapping[usize(view.right)] = right + return + } + } + if (kind == .catch_expr) { + execution.mapping[usize(id)] = try type_snapshot_catch(state, execution, id, no_node) + if (execution.mapping[usize(id)] == no_node and execution.pending_flow == .next) + execution.pending_flow = .invalid + return + } + } for (0..ast.child_count(&state.tree, id)) |i| - prepare_type_snapshot(state, execution, ast.child(&state.tree, id, i)) + try prepare_type_snapshot(state, execution, ast.child(&state.tree, id, i)) } @hide:file @@ -2348,9 +2538,40 @@ bind_type_snapshot proc(state @mut State, execution @TypeExecution, id, start as try bind_type_snapshot(state, execution, ast.child(&state.tree, id, i), start) } +@hide:file +skip_type_value_blocks proc(state @mut State, id ast.NodeId) bool ! Error { + if (ast.node_class(&state.tree, id) == .expression and ast.expr_kind(&state.tree, id) == .block_expr) { + annotation :: try expression_type(state, id, 0) + if (annotation == no_node) return false + span :: ast.node_span(&state.tree, id) + # skipped type-control bodies retain only their result type for checked IR. + placeholder :: try ast.add_literal(state.builder, .undefined, span) + local :: try ast.add_statement_decl(state.builder, span, span, strpool.no_str, + annotation, .variable, placeholder) + try refresh(state) + yielded :: try ast.add_yield_stmt(state.builder, span, try reference(state, id, local)) + statements [2]ast.NodeId :: [local, yielded] + body :: try ast.add_block(state.builder, span, statements[..]) + replacement :: try ast.add_block_expr(state.builder, span, body) + ast.replace_node_from_source(state.builder, id, replacement) + try refresh(state) + return true + } + for (0..ast.child_count(&state.tree, id)) |i| + if (!try skip_type_value_blocks(state, ast.child(&state.tree, id, i))) return false + return true +} + @hide:file type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error { - prepare_type_snapshot(state, execution, id) + if (id == no_node or execution.pending_flow != .next) return no_node + if (execution.mapping.len < ast.node_count(&state.tree)) { + previous :: execution.mapping.len + execution.mapping = try mem.realloc(state.allocator, execution.mapping, ast.node_count(&state.tree)) + mem.set(execution.mapping[previous..], no_node) + } + try prepare_type_snapshot(state, execution, id) + if (execution.pending_flow != .next) return no_node start :: ast.NodeId(ast.node_count(&state.tree)) result :: try ast.clone_subtree(state.builder, id, execution.mapping) try refresh(state) @@ -2358,6 +2579,123 @@ type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId return result } +@hide:file +type_snapshot_expected proc( + state @mut State, execution @mut TypeExecution, id, annotation ast.NodeId, +) ast.NodeId ! Error { + if (id == no_node or execution.pending_flow != .next) return no_node + if (execution.evaluate_values and ast.expr_kind(&state.tree, id) == .block_expr) + return try execute_type_value_block(state, execution, ast.block_expr(&state.tree, id), annotation) + if (execution.evaluate_values and ast.expr_kind(&state.tree, id) == .match_expr) + return try type_snapshot_match(state, execution, id, annotation) + if (execution.evaluate_values and ast.expr_kind(&state.tree, id) == .catch_expr) + return try type_snapshot_catch(state, execution, id, annotation) + return try type_snapshot(state, execution, id) +} + +@hide:file +type_snapshot_catch proc( + state @mut State, execution @mut TypeExecution, id, annotation ast.NodeId, +) ast.NodeId ! Error { + view :: ast.catch_expr(&state.tree, id) + operand :: try type_snapshot(state, execution, view.operand) + if (operand == no_node) return no_node + operand_type :: try expression_type(state, operand, 0) + if (operand_type == no_node or ast.expr_kind(&state.tree, operand_type) != .fallible_type) return no_node + channels :: ast.binary_expr(&state.tree, operand_type) + owned :: try type_execution_value(state, operand, operand_type) + if (owned == no_node) return no_node + span :: ast.node_span(&state.tree, id) + boolean :: try builtin(state, id, .bool) + yes :: try ast.add_boolean(state.builder, span, true) + no :: try ast.add_boolean(state.builder, span, false) + failed :: try ast.add_return_stmt(state.builder, span, yes) + failure_statements [1]ast.NodeId :: [failed] + handler :: try ast.add_block(state.builder, span, failure_statements[..]) + caught :: try ast.add_catch_expr(state.builder, span, owned, null, handler, true) + discarded :: try ast.add_expression_stmt(state.builder, caught) + success :: try ast.add_return_stmt(state.builder, span, no) + statements [2]ast.NodeId :: [discarded, success] + body :: try ast.add_block(state.builder, span, statements[..]) + try refresh(state) + guard :: try evaluate_ir_body(state, id, body, boolean) + if (guard.kind != .boolean) return no_node + payload :: ast.typed_constant(&state.tree, owned).value + if (!guard.boolean) return try type_execution_value(state, payload, channels.left) + saved :: execution.locals.items.len + defer execution.locals.items = execution.locals.items[..saved] + if (view.capture) |capture| { + error :: try type_execution_value(state, payload, channels.right) + if (error == no_node) return no_node + try arraylist.append(&execution.locals, TypeLocal{ + declaration = capture, value = error, annotation = channels.right, + }) + } + result_type := channels.left + if (annotation != no_node and !is_type(state, annotation)) { + contextual :: try canonical_type(state, annotation) + if (contextual != no_node) { + kind :: ast.expr_kind(&state.tree, contextual) + if (kind == .optional_type + and try same_type(state, ast.type_operand(&state.tree, contextual), channels.left)) + result_type = annotation + if (kind == .fallible_type + and try same_type(state, ast.binary_expr(&state.tree, contextual).left, channels.left)) + result_type = annotation + } + } + if (view.handler_is_block) + return try execute_type_value_block(state, execution, view.handler, result_type) + selected :: try type_snapshot_expected(state, execution, view.handler, result_type) + return try type_execution_value(state, selected, result_type) +} + +@hide:file +type_dispatch_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error { + subject :: try type_snapshot(state, execution, ast.match_expr(&state.tree, id).subject) + if (subject == no_node) return no_node + saved :: execution.evaluate_values + execution.evaluate_values = false + defer execution.evaluate_values = saved + snapshot :: try type_snapshot(state, execution, id) + if (snapshot == no_node) return no_node + arms := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&arms) + for (0..ast.match_arm_count(&state.tree, snapshot)) |i| + try arraylist.append(&arms, ast.match_arm(&state.tree, snapshot, i)) + optional_capture :: ast.match_expr(&state.tree, id).optional_capture + result :: try ast.add_match(state.builder, ast.node_span(&state.tree, id), subject, arms.items) + if (optional_capture) ast.set_match_optional(state.builder, result) + try refresh(state) + return result +} + +@hide:file +type_snapshot_match proc( + state @mut State, execution @mut TypeExecution, id, annotation ast.NodeId, +) ast.NodeId ! Error { + snapshot :: try type_dispatch_snapshot(state, execution, id) + if (snapshot == no_node) return no_node + index :: try type_match_arm(state, snapshot) + if (index == no_index) return no_node + arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, id, index)) + saved :: execution.locals.items.len + defer execution.locals.items = execution.locals.items[..saved] + if (arm.capture) |capture| { + value :: try type_match_capture(state, snapshot) + if (value == no_node) return no_node + constant :: ast.typed_constant(&state.tree, value) + payload :: if (is_type(state, constant.type)) constant.value else value + try arraylist.append(&execution.locals, TypeLocal{ + declaration = capture, + value = payload, + annotation = constant.type, + }) + } + if (!arm.expression) return try execute_type_value_block(state, execution, arm.body, annotation) + return try type_snapshot_expected(state, execution, arm.body, annotation) +} + @hide:file nominal_type_value proc(state @State, id ast.NodeId) bool { kind :: ast.expr_kind(&state.tree, id) @@ -2386,9 +2724,14 @@ inline_statement proc(state @mut State, id ast.NodeId) TypeFlow ! Error { kind :: ast.stmt_kind(&state.tree, id) if (kind == .break_stmt) return .break if (kind == .continue_stmt) return .continue + if (kind == .yield_stmt or kind == .return_stmt) { + try expand(state, id) + return if (kind == .yield_stmt) TypeFlow.yielded else TypeFlow.returned + } if (kind == .block_stmt) return try inline_block(state, ast.statement_block(&state.tree, id)) selected := no_node + value_source := false if (kind == .if_stmt) { conditional :: ast.if_stmt(&state.tree, id) explicit :: ast.expr_kind(&state.tree, conditional.condition) == .comptime_expr @@ -2414,8 +2757,10 @@ inline_statement proc(state @mut State, id ast.NodeId) TypeFlow ! Error { arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, index)) body :: try type_match_body(state, expression, arm) if (body == no_node) return .invalid - if (!arm.expression) selected = body - else { + if (!arm.expression) { + selected = body + value_source = true + } else { statement :: try ast.add_expression_stmt(state.builder, body) statements [1]ast.NodeId :: [statement] selected = try ast.add_block(state.builder, ast.node_span(&state.tree, id), statements[..]) @@ -2425,8 +2770,13 @@ inline_statement proc(state @mut State, id ast.NodeId) TypeFlow ! Error { } if (selected != no_node) { try refresh(state) - flow :: try inline_block(state, selected) - replacement :: try ast.add_block_stmt(state.builder, selected) + flow := try inline_block(state, selected) + replacement := no_node + if (value_source) { + value :: try ast.add_block_expr(state.builder, ast.node_span(&state.tree, selected), selected) + replacement = try ast.add_expression_stmt(state.builder, value) + if (flow == .yielded) flow = .next + } else replacement = try ast.add_block_stmt(state.builder, selected) try replace(state, id, replacement) try refresh(state) return flow @@ -2490,7 +2840,8 @@ expand_inline_for proc(state @mut State, id ast.NodeId) void ! Error { flow := TypeFlow.invalid if (state.evaluation_diagnostic == diagnostics.no_diagnostic) flow = try execute_type_for(state, &execution, id) - if (flow != .next or state.evaluation_diagnostic != diagnostics.no_diagnostic) { + if ((flow != .next and flow != .yielded and flow != .returned) + or state.evaluation_diagnostic != diagnostics.no_diagnostic) { if (state.evaluation_diagnostic == diagnostics.no_diagnostic) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_argument_required, loop.span) invalid :: try ast.add_invalid_stmt(state.builder, loop.span, state.evaluation_diagnostic) @@ -2664,23 +3015,33 @@ type_match_body proc(state @mut State, id ast.NodeId, arm ast.MatchArmView) ast. @hide:file type_value_expression proc(state @mut State, input ast.NodeId) ast.NodeId ! Error { + if (input == no_node) return no_node expression := input while (ast.expr_kind(&state.tree, expression) == .match_expr) { if (!try type_step(state, expression)) return no_node index :: try type_match_arm(state, expression) if (index == no_index) return no_node arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, index)) - if (!arm.expression) return no_node expression = try type_match_body(state, expression, arm) + if (!arm.expression and expression != no_node) { + expression = try ast.add_block_expr(state.builder, ast.node_span(&state.tree, expression), expression) + try refresh(state) + } if (expression == no_node) return no_node } + if (expression != no_node and ast.expr_kind(&state.tree, expression) == .block_expr) { + annotation :: try value_block_type(state, ast.block_expr(&state.tree, expression), 0) + if (annotation != no_node and (is_type(state, annotation) or type_record_storage(state, annotation, 0))) + return try evaluate_type_value_block(state, expression, annotation) + } return expression } -# normalize only selected expression arms before ordinary expansion can visit them. +# normalize selected value sources before ordinary expansion can visit their bodies. # nominal results enter here after their instance declaration is cached. @hide:file expand_type_matches proc(state @mut State, id ast.NodeId) bool ! Error { + if (id == no_node) return false if (ast.node_class(&state.tree, id) == .expression and ast.expr_kind(&state.tree, id) == .match_expr) { selected :: try type_value_expression(state, id) if (selected == no_node) return false @@ -3258,7 +3619,10 @@ type_array_update proc( @hide:file type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.NodeId ! Error { - if (annotation == no_node) return no_node + if (input == no_node or annotation == no_node) return no_node + if (ast.expr_kind(&state.tree, input) == .block_expr + and (is_type(state, annotation) or type_record_storage(state, annotation, 0))) + return try evaluate_type_value_block(state, input, annotation) expression :: try type_value_expression(state, input) if (expression == no_node) return no_node if (!try expand_type_matches(state, annotation)) return no_node @@ -3403,6 +3767,7 @@ type_assignment_value proc( local :: execution.locals.items[index] span :: ast.node_span(&state.tree, statement) value :: try type_snapshot(state, execution, assignment.value) + if (value == no_node) return no_node mutability :: if (ast.node_class(&state.tree, local.declaration) == .statement) ast.statement_decl(&state.tree, local.declaration).mutability else ast.Mutability.constant temporary :: try ast.add_statement_decl(state.builder, span, span, strpool.no_str, @@ -3432,6 +3797,7 @@ type_assignment_value proc( execute_type_block proc(state @mut State, execution @mut TypeExecution, block ast.NodeId) TypeFlow ! Error { if (block == execution.inline_body) { snapshot :: try type_snapshot(state, execution, block) + if (snapshot == no_node) return .invalid flow :: try inline_block(state, snapshot) if (flow == .invalid) return flow try arraylist.append(execution.inline_blocks?, snapshot) @@ -3439,13 +3805,33 @@ execute_type_block proc(state @mut State, execution @mut TypeExecution, block as } saved :: execution.locals.items.len defer execution.locals.items = execution.locals.items[..saved] + reached := usize(0) + flow := TypeFlow.next for (0..ast.block_statement_count(&state.tree, block)) |i| { statement :: ast.block_statement(&state.tree, block, i) if (!try type_step(state, statement)) return .invalid - flow :: try execute_type_statement(state, execution, statement) - if (flow != .next) return flow + reached = i + 1 + flow = try execute_type_statement(state, execution, statement) + if (flow != .next) break } - return .next + if (flow == .invalid) return flow + # yielded/returned values already own their snapshots before scope cleanup runs. + while (reached > 0) { + reached -= 1 + statement :: ast.block_statement(&state.tree, block, reached) + if (ast.stmt_kind(&state.tree, statement) != .defer_stmt) continue + saved_depth :: execution.cleanup_yield_depth + saved_loop :: execution.cleanup_loop_depth + execution.cleanup_loop_depth = execution.loop_depth + execution.cleanup_yield_depth = execution.yield_depth + execution.cleanup_depth += 1 + cleanup :: try execute_type_block(state, execution, ast.cleanup_stmt(&state.tree, statement).body) + execution.cleanup_depth -= 1 + execution.cleanup_yield_depth = saved_depth + execution.cleanup_loop_depth = saved_loop + if (cleanup != .next) return .invalid + } + return flow } # substitute selector names without snapshotting away the assignment's root place. @@ -3462,6 +3848,7 @@ type_assignment_target proc(state @mut State, execution @mut TypeExecution, id a base :: try type_assignment_target(state, execution, ast.call_arg(&state.tree, id, 0)) if (base == no_node) return no_node argument :: try type_snapshot(state, execution, ast.call_arg(&state.tree, id, 1)) + if (argument == no_node) return no_node arguments [2]ast.NodeId :: [base, argument] result :: try ast.add_builtin_call(state.builder, ast.node_span(&state.tree, id), name, arguments[..]) try refresh(state) @@ -3485,10 +3872,56 @@ type_assignment_target proc(state @mut State, execution @mut TypeExecution, id a @hide:file execute_type_statement proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error { + if (execution.cleanup_depth > 0) { + kind :: ast.stmt_kind(&state.tree, statement) + if (kind == .return_stmt + or ((kind == .break_stmt or kind == .continue_stmt) and execution.loop_depth <= execution.cleanup_loop_depth)) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .cleanup_control_flow, ast.node_span(&state.tree, statement)) + return .invalid + } + } + flow :: try execute_type_statement_body(state, execution, statement) + if (execution.pending_flow == .next) return flow + pending :: execution.pending_flow + execution.pending_flow = .next + return pending +} + +@hide:file +execute_type_statement_body proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error { match ast.stmt_kind(&state.tree, statement) { + .defer_stmt: {} + .errdefer_stmt: { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .errdefer_outside_fallible, ast.node_span(&state.tree, statement)) + return .invalid + } + .yield_stmt: { + if (execution.yield_depth == 0) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .yield_outside_block, ast.node_span(&state.tree, statement)) + return .invalid + } + if (execution.cleanup_depth > 0 and execution.yield_depth <= execution.cleanup_yield_depth) { + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .cleanup_control_flow, ast.node_span(&state.tree, statement)) + return .invalid + } + value :: ast.yield_stmt(&state.tree, statement).value? + expression :: try type_snapshot_expected(state, execution, value, execution.yield_type) + if (expression == no_node) return .invalid + annotation := execution.yield_type + if (annotation == no_node) annotation = try expression_type(state, expression, 0) + execution.yielded = try type_execution_value(state, expression, annotation) + if (execution.yielded == no_node + or !try type_array_initialized(state, execution.yielded, value)) return .invalid + return .yielded + } .return_stmt: { if (ast.return_stmt(&state.tree, statement).value) |value| { - expression :: try type_snapshot(state, execution, value) + 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 and !try type_array_initialized(state, execution.returned, value)) return .invalid @@ -3498,13 +3931,14 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat } .declaration: { declaration :: ast.statement_decl(&state.tree, statement) - snapshot :: try type_snapshot(state, execution, declaration.initializer) - expression :: try type_value_expression(state, snapshot) - if (expression == no_node) return .invalid annotation := no_node if (declaration.type) |type| annotation = try type_snapshot(state, execution, type) - else if (nominal_type_value(state, expression)) annotation = try builtin(state, expression, .type) - else { + snapshot :: try type_snapshot_expected(state, execution, declaration.initializer, annotation) + expression :: try type_value_expression(state, snapshot) + if (expression == no_node) return .invalid + if (annotation == no_node and nominal_type_value(state, expression)) + annotation = try builtin(state, expression, .type) + else if (annotation == no_node) { if (!try expand_type_matches(state, expression)) return .invalid try expand(state, expression) annotation = try expression_type(state, expression, 0) @@ -3588,7 +4022,8 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat execution.locals.items[index].value = value return .next } - expression :: try type_snapshot(state, execution, assignment.value) + expression :: try type_snapshot_expected(state, execution, + assignment.value, execution.locals.items[index].annotation) value :: try type_execution_value(state, expression, execution.locals.items[index].annotation) if (value == no_node) return .invalid execution.locals.items[index].value = value @@ -3621,6 +4056,7 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat if (!try type_step(state, condition)) return .invalid if (!invariant or value.kind == .invalid) { expression :: try type_snapshot(state, execution, condition) + if (expression == no_node) return .invalid if (capture) |name| value = try type_capture_guard(state, expression, name) else { boolean :: try builtin(state, expression, .bool) @@ -3635,6 +4071,7 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat execution.locals.items = execution.locals.items[..saved] if (capture) |name| { expression :: try type_snapshot(state, execution, condition) + if (expression == no_node) return .invalid unwrapped :: try ast.add_unary(state.builder, ast.node_span(&state.tree, condition), .unwrap, expression) try refresh(state) @@ -3646,18 +4083,25 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat }) } flow :: try execute_type_block(state, execution, body) - if (!looping or flow == .returned or flow == .invalid) return flow + if (!looping or flow == .returned or flow == .yielded or flow == .invalid) return flow if (flow == .break) return .next } } .expression: { expression :: ast.statement_expr(&state.tree, statement) - if (ast.expr_kind(&state.tree, expression) != .match_expr) return .invalid + if (ast.expr_kind(&state.tree, expression) != .match_expr) { + snapshot :: try type_snapshot(state, execution, expression) + if (snapshot == no_node) return .invalid + annotation :: try expression_type(state, snapshot, 0) + if (try type_execution_value(state, snapshot, annotation) == no_node) return .invalid + return .next + } subject_type := no_node arms :: ast.match_arm_count(&state.tree, expression) if (arms > 0 and ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, arms - 1)).is_inline) { subject :: try type_snapshot(state, execution, ast.match_expr(&state.tree, expression).subject) + if (subject == no_node) return .invalid subject_type = try expression_type(state, subject, 0) } before :: ast.node_count(&state.tree) @@ -3668,7 +4112,8 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat execution.mapping = try mem.realloc(state.allocator, execution.mapping, ast.node_count(&state.tree)) mem.set(execution.mapping[previous..], no_node) } - snapshot :: try type_snapshot(state, execution, expression) + snapshot :: try type_dispatch_snapshot(state, execution, expression) + if (snapshot == no_node) return .invalid index :: try type_match_arm(state, snapshot) if (index == no_index) return .invalid arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, index)) @@ -3684,7 +4129,10 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat annotation = constant.type, }) } - if (!arm.expression) return try execute_type_block(state, execution, arm.body) + if (!arm.expression) { + if (try execute_type_value_block(state, execution, arm.body, no_node) == no_node) return .invalid + return .next + } body :: try type_snapshot(state, execution, arm.body) value :: try type_value_expression(state, body) if (value == no_node) return .invalid @@ -3729,6 +4177,7 @@ type_collection_place proc(state @mut State, execution @mut TypeExecution, id as place, ast.selection_expr(&state.tree, id).member) } else { index :: try type_snapshot(state, execution, ast.index_expr(&state.tree, id).index) + if (index == no_node) return no_node 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) @@ -3863,7 +4312,7 @@ execute_type_collection proc(state @mut State, execution @mut TypeExecution, sta }) } flow :: try execute_type_block(state, execution, loop.body) - if (flow == .returned or flow == .invalid) return flow + if (flow == .returned or flow == .yielded or flow == .invalid) return flow if (flow == .break) return .next } return .next @@ -3878,6 +4327,7 @@ execute_type_for proc(state @mut State, execution @mut TypeExecution, statement bounds :: ast.range_expr(&state.tree, loop.iterable) start :: try type_snapshot(state, execution, bounds.start) end :: try type_snapshot(state, execution, bounds.end) + if (start == no_node or end == no_node) return .invalid typed_bound :: if (flexible(state, start)) end else start annotation :: try expression_type(state, typed_bound, 0) if (annotation == no_node or !try integer_type(state, annotation)) return .invalid @@ -3902,7 +4352,7 @@ execute_type_for proc(state @mut State, execution @mut TypeExecution, statement if (!keep.boolean) return .next if (loop.capture != null) execution.locals.items[saved].value = current flow :: try execute_type_block(state, execution, loop.body) - if (flow == .returned or flow == .invalid) return flow + if (flow == .returned or flow == .yielded or flow == .invalid) return flow if (flow == .break) return .next if (try value_equal(state, try evaluate(state, current), try evaluate(state, limit))) return .next one :: try ast.add_integer(state.builder, span, 1) @@ -3920,6 +4370,7 @@ mix proc(left, right usize) usize { @hide:file evaluate proc(state @mut State, id ast.NodeId) Value ! Error { + if (id == no_node) return Value{} if (state.depth >= 128) return Value{} state.depth += 1 defer state.depth -= 1 @@ -4001,7 +4452,29 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error { } .binary: return try evaluate_binary(state, id) .call: return try evaluate_call(state, id) - .record_literal, .array_literal, .selection, .index, .match_expr, .builtin_call, .catch_expr: + .block_expr: { + annotation :: try expression_type(state, id, 0) + if (annotation == no_node) return Value{} + if (is_type(state, annotation) or type_record_storage(state, annotation, 0)) { + result :: try evaluate_type_value_block(state, id, annotation) + if (result == no_node) return Value{} + return try evaluate_expected(state, result, annotation) + } + return try evaluate_ir(state, id, annotation) + } + .match_expr: { + annotation :: try expression_type(state, id, 0) + if (annotation != no_node and (is_type(state, annotation) or type_record_storage(state, annotation, 0))) { + if (state.type_execution_depth == 0) state.type_execution_remaining = 4096 + state.type_execution_depth += 1 + defer state.type_execution_depth -= 1 + selected :: try type_value_expression(state, id) + if (selected == no_node or selected == id) return Value{} + return try evaluate_expected(state, selected, annotation) + } + return try evaluate_ir(state, id, annotation) + } + .record_literal, .array_literal, .selection, .index, .builtin_call, .catch_expr: return try evaluate_ir(state, id, try expression_type(state, id, 0)) else: return Value{} } @@ -4322,8 +4795,39 @@ type_hash_context proc(state @mut State, id ast.NodeId, metadata bool) usize ! E } } +@hide:file +value_block_yield proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! Error { + if (depth >= 128) return no_node + class :: ast.node_class(&state.tree, id) + if (class != .block and class != .statement) return no_node + if (class == .statement) { + kind :: ast.stmt_kind(&state.tree, id) + if (kind == .yield_stmt) + return ast.yield_stmt(&state.tree, id).value? + if (kind != .block_stmt and kind != .if_stmt and kind != .while_stmt and kind != .for_stmt) + return no_node + } + result := no_node + for (0..ast.child_count(&state.tree, id)) |i| { + child :: ast.child(&state.tree, id, i) + value :: try value_block_yield(state, child, depth + 1) + if (value == no_node or try expression_type(state, value, depth + 1) == no_node) continue + result = value + if (!flexible(state, value)) return result + } + return result +} + +@hide:file +value_block_type proc(state @mut State, block ast.NodeId, depth usize) ast.NodeId ! Error { + value :: try value_block_yield(state, block, depth) + return if (value != no_node) try expression_type(state, value, depth + 1) + else try builtin(state, block, .void) +} + @hide:file expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! Error { + if (id == no_node) return no_node if (depth >= 128) return no_node try expand(state, id) resolved :: binding(state, id) @@ -4367,15 +4871,16 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! .literal_bool: return try builtin(state, id, .bool) .literal_string: return try builtin(state, id, .string) .literal_void: return try builtin(state, id, .void) + .block_expr: return try value_block_type(state, ast.block_expr(&state.tree, id), depth + 1) .match_expr: { result := no_node for (0..ast.match_arm_count(&state.tree, id)) |i| { arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, id, i)) - if (!arm.expression) return no_node - annotation :: try expression_type(state, arm.body, depth + 1) + annotation :: if (arm.expression) try expression_type(state, arm.body, depth + 1) + else try value_block_type(state, arm.body, depth + 1) if (annotation == no_node) continue result = annotation - if (!flexible(state, arm.body)) return result + if (!arm.expression or !flexible(state, arm.body)) return result } return result } @@ -4394,8 +4899,10 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! } return no_node } - .catch_expr: { - type :: try expression_type(state, ast.catch_expr(&state.tree, id).operand, depth + 1) + .catch_expr, .try_expr: { + operand :: if (kind == .catch_expr) ast.catch_expr(&state.tree, id).operand + else ast.type_operand(&state.tree, id) + type :: try expression_type(state, operand, depth + 1) if (type == no_node or ast.expr_kind(&state.tree, type) != .fallible_type) return no_node return try canonical_type(state, ast.binary_expr(&state.tree, type).left) } diff --git a/tests/native.py b/tests/native.py index 3d771b6..24911bf 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,322 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "yielding_blocks_nest_and_copy_before_scoped_cleanup": ( + """ + Item :: struct { values [1]mut i16 } + main proc() [3]i16 { + trace i16 := 0 + source Item := { values = [40] } + saved Item :: { + defer trace = trace * 10 + 1 + inner i16 :: { + defer trace = trace * 10 + 2 + yield 2 + } + { + defer source.values[0] = 0 + source.values[0] = source.values[0] + inner + yield source + } + trace = -1 + } + return [saved.values[0], source.values[0], trace] + } + """, b"[42, 0, 21]\n", None, + ), + "yielding_blocks_preserve_loop_and_function_transfers": ( + """ + choose proc(flag bool) i16 { + value i16 :: { + if (flag) return 7 + yield 42 + } + return value + } + main proc() [3]i16 { + total i16 := 0 + cleanups i16 := 0 + for (0..5) |index| { + value i16 :: { + defer cleanups = cleanups + 1 + if (index == 0) continue + if (index == 3) break + yield i16(index) + } + total = total + value + } + return [total, cleanups, choose(true) + choose(false)] + } + """, b"[3, 4, 49]\n", None, + ), + "yielding_blocks_leave_inner_loops_on_yield": ( + """ + main proc() [2]i16 { + cleanups i16 := 0 + value i16 :: { + for (0..3) |index| { + defer cleanups = cleanups + 1 + if (index == 0) continue + yield 42 + } + yield 0 + } + return [value, cleanups] + } + """, b"[42, 2]\n", None, + ), + "yielding_match_catch_and_conditional_blocks_are_lazy": ( + """ + Error :: enum { failed } + fail proc(flag bool) i16 ! Error { + if (flag) return .failed + return 40 + } + choose proc(flag bool) i16 { + return match flag { + true: { value :: fail(false) catch { yield 0 } + yield value + 2 }, + false: { yield fail(true) catch { yield 7 } }, + } + } + main proc() [3]i16 { + return [choose(true), choose(false), + if (true) { yield 42 } else { yield divtrunc!(1, 0) }] + } + """, b"[42, 7, 42]\n", None, + ), + "yielding_blocks_contextual_optional_fallible_and_aggregate_values": ( + """ + Error :: enum { failed } + Item :: struct { value i16 } + optional proc(flag bool) ?i16 { + return { if (flag) yield 42 + yield null } + } + fallible proc(flag bool) i16 ! Error { + return { if (flag) yield 42 + yield .failed } + } + item proc(flag bool) Item { + return if (flag) { yield { value = 42 } } else { value = 7 } + } + main proc() [4]i16 { + if (optional(false) != null) return [0, 0, 0, 0] + return [optional(true)?, fallible(true) catch 0, + fallible(false) catch { yield 7 }, item(true).value] + } + """, b"[42, 42, 7, 42]\n", None, + ), + "yielding_blocks_propagate_errors_and_run_only_reached_cleanup": ( + """ + Error :: enum { failed } + fail proc() i16 ! Error { return .failed } + compute proc(trace @mut i16, flag bool) i16 ! Error { + value i16 :: { + defer trace^ = trace^ * 10 + 1 + errdefer trace^ = trace^ * 10 + 2 + if (flag) yield 42 + yield try fail() + } + return value + } + main proc() [4]i16 { + success i16 := 0 + failure i16 := 0 + left :: compute(&success, true) catch 0 + right :: compute(&failure, false) catch 7 + return [left, right, success, failure] + } + """, b"[42, 7, 1, 21]\n", None, + ), + "yielding_blocks_preserve_narrow_types_and_compile_time_values": ( + """ + exact proc(value i8) i16 { return i16(value) } + compute proc() i8 { + return { + total i8 := 0 + for (i8(0)..i8(3)) |index| total = total + index + yield total + 39 + } + } + main proc() [3]i16 { + return [exact({ yield i8(42) }), i16($(compute())), + i16($({ value i8 :: 7 + yield value }))] + } + """, b"[42, 42, 7]\n", None, + ), + "yielding_blocks_generate_types_defaults_and_specialized_bodies": ( + """ + Choose proc(wide bool) type { + return { + Selected := i8 + if (wide) Selected = i16 + yield Selected + } + } + Box proc(wide bool) type { + count usize :: { yield if (wide) 2 else 1 } + return struct { + values [count]Choose(wide) + bias i16 = { yield 7 } + } + } + select proc($flag bool, value i16) i16 { + return { if (flag) yield value + yield compile_error!("unselected") } + } + main proc() [3]i16 { + value Box(true) :: { values = [40, 2] } + return [select(true, value.values[0] + value.values[1]), + value.bias, i16(value.values.len)] + } + """, b"[42, 7, 2]\n", None, + ), + "yielding_blocks_keep_match_and_catch_capture_scopes": ( + """ + Choice :: union(enum) { value i8, empty void } + Error :: union(enum) { failed i8 } + fail proc() i16 ! Error { return .failed{7} } + choose proc(value Choice) i16 { + return match value { + .value |payload|: { yield i16(payload) }, + .empty: { yield fail() catch |error| { + yield match error { + .failed |code|: { yield i16(code) }, + } + } }, + } + } + main proc() [2]i16 { return [choose(.value{42}), choose(.empty)] } + """, b"[42, 7]\n", None, + ), + "yielding_blocks_allow_local_yields_inside_cleanup": ( + """ + main proc() i16 { + value i16 := 0 + { + defer value = { yield 42 } + } + return value + } + """, b"42\n", None, + ), + "yielding_blocks_require_yield_operands": ( + "main proc() i16 { return { yield } }", + b"", b"unexpected token", + ), + "yielding_blocks_preserve_yield_capture_immutability": ( + """ + main proc() i16 { + value ?i16 :: 42 + return if value |payload| { payload = 7 + yield payload } else 0 + } + """, b"", b"assignment requires a mutable local binding", + ), + "yielding_blocks_reject_yield_outside_value_sources": ( + "main proc() i16 { yield 42 }", + b"", b"yield requires an enclosing value block", + ), + "yielding_blocks_check_fallthrough_without_trapping_yielded_paths": ( + """ + choose proc(flag bool) i16 { + return { if (flag) yield 42 } + } + main proc() i16 { return choose(true) } + """, b"42\n", b"nonvoid value block can finish without yielding a value", + ), + "yielding_blocks_trap_reached_missing_value_paths": ( + """ + choose proc(flag bool) i16 { + return { if (flag) yield 42 } + } + main proc() i16 { return choose(false) } + """, b"", b"nonvoid value block can finish without yielding a value", + ), + "yielding_blocks_reject_incompatible_runtime_yields": ( + """ + choose proc(flag bool) i16 { + return { if (flag) yield i16(42) + yield u8(7) } + } + main proc() i16 { return choose(true) } + """, b"42\n", b"yield does not match the value block result type", + ), + "yielding_blocks_reject_outward_cleanup_yields": ( + """ + main proc() i16 { + return { defer { yield 7 } + yield 42 } + } + """, b"", b"cleanup cannot", + ), + "yielding_blocks_preserve_type_control_mutation_captures_and_cleanup": ( + """ + Choose proc(input ?bool) type { + Selected := i8 + Element :: { + Selected = i8 + defer Selected = i16 + for (0..3) |index| { + if (index == 1) { + yield if input |flag| { yield if (flag) bool else i8 } + else { yield i16 } + } + } + yield i8 + } + return struct { flag Element, value Selected } + } + main proc() i16 { + value Choose(true) :: { flag = true, value = 42 } + return if (value.flag) value.value else 0 + } + """, b"42\n", None, + ), + "yielding_blocks_preserve_runtime_aliases_callbacks_and_void_results": ( + """ + add proc(value i16) i16 { return value + 2 } + noop proc() void {} + nothing proc() void { return { yield noop() } } + main proc() [2]i16 { + values [1]mut i16 := [40] + pointer @mut i16 :: { yield &values[0] } + view []mut i16 :: { yield values[..] } + callback @proc(value i16) i16 :: { yield add } + pointer^ = callback(view[0]) + ({ yield nothing() }) + return [values[0], view[0]] + } + """, b"[42, 42]\n", None, + ), + "yielding_blocks_keep_compile_time_logical_guards_lazy": ( + """ + Choose proc() type { + Selected := i8 + if (false and { Selected = i16 + yield true }) Selected = bool + if (true or { Selected = bool + yield false }) {} + return Selected + } + exact proc(value i8) i16 { return i16(value) } + main proc() i16 { + value Choose() :: 42 + return exact(value) + } + """, b"42\n", None, + ), + "yielding_blocks_check_skipped_compile_time_guard_result_types": ( + """ + Bad proc() type { + if (false and { yield 1 }) return bool + return i16 + } + main proc() Bad() { return 42 } + """, b"", b"operator requires compatible operand types", + ), "conditional_expressions_evaluate_guard_once_and_only_selected_arm": ( """ next proc(calls @mut i16, value bool) bool { calls^ = calls^ + 1 @@ -7880,6 +8196,20 @@ def check_dependency_provenance(temporary, executable): fail proc() int { return dependency.fail() } """).strip() + "\n") cases = [ + (""" + fail proc() int { + return { + yield 9223372036854775807 + 1 + } + } + """, 5, 15, False), + (""" + fail proc() int { + return { + yield missing + } + } + """, 5, 15, True), (""" fail proc() int { return `