diff --git a/README.md b/README.md index d97e1ae..c81b631 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,8 @@ 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. 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 and generated type positions reuse checked match execution. guards require parentheses unless they are calls or explicit `$()` expressions. parenthesize a conditional used as a match-arm value to distinguish it from an `if` statement. optional payload captures and yielding value blocks 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 return or transfer loop control; yielding value blocks and pointer captures belong to later gates. 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 0e205e4..1e2b894 100644 --- a/TODO.md +++ b/TODO.md @@ -56,6 +56,8 @@ runtime `undefined` locals and compute fully initialized materializable constant 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 `if ... else` expressions reuse checked matches for runtime/constant values, +contextual optional/fallible results and generated types; yielding blocks remain open. 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 @@ -122,8 +124,9 @@ 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, yielding value blocks, compound updates and bitwise operators remain open before hash-map acceptance. - [ ] 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: expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. +- [ ] remaining source constructs: optional-capture conditional expressions, yielding value blocks and `yield`, compound updates, bitwise operators and multiline strings. boolean conditional expressions 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 097faa3..e425738 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -7,12 +7,13 @@ #! * decl ..... -> identifier [type] binding expr | identifier 'c_func' '(' parameters ')' type | ['test'] [identifier '::'] 'import' string #! * parameters -> [['$'] identifier {',' ['$'] identifier} type {',' ['$'] identifier {',' ['$'] identifier} type} [',']]; '$' marks each compile-time parameter independently. #! '$(' expr ')' is an explicit compile-time expression with primary-expression precedence. +#! boolean value expressions also accept 'if' guard expr 'else' expr, canonicalized to matches; else is required. #! * 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 -#! braceless guards/iterables require parentheses unless they are calls or optional captures; else binds to the nearest if. +#! 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. diff --git a/source/parser/parse.hon b/source/parser/parse.hon index d16ef56..bc4c0ae 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -461,6 +461,35 @@ parse_if proc(state @mut State) ast.NodeId ! ParseError { return id } +@hide:file +parse_if_expr proc(state @mut State) ast.NodeId ! ParseError { + keyword :: try consume(state, .if) + condition :: try parse_guard(state) + then_value :: try parse_expr(state) + skip_newlines(state) + if (!next_is(state, .else) or next_n_is(state, 1, .colon)) return .unexpected_token + state.next_token += 1 + skip_newlines(state) + else_value :: try parse_expr(state) + span :: ast.Span{ + start = keyword.span.start, + end = ast.builder_span(state.builder, else_value).end, + source = keyword.span.source, + } + # canonical boolean matches share inference, specialization and branch execution. + patterns [1]ast.NodeId :: [try ast.add_boolean(state.builder, keyword.span, true)] + then_arm :: try ast.add_match_arm( + state.builder, ast.builder_span(state.builder, then_value), + patterns[..], null, then_value, true, false, + ) + else_arm :: try ast.add_match_arm( + state.builder, ast.builder_span(state.builder, else_value), + &[], null, else_value, true, true, + ) + arms [2]ast.NodeId :: [then_arm, else_arm] + return try ast.add_match(state.builder, span, condition, arms[..]) +} + @hide:file parse_while proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { keyword :: try consume(state, .while) @@ -640,6 +669,7 @@ parse_guard proc(state @mut State) ast.NodeId ! ParseError { and !next_is(state, .pipe) and !parenthesized_guard(state, condition_start) and ast.builder_expr_kind(state.builder, condition) != .call + and ast.builder_expr_kind(state.builder, condition) != .comptime_expr and ast.builder_expr_kind(state.builder, condition) != .invalid ) { diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span) @@ -828,6 +858,7 @@ parse_type_atom proc(state @mut State) ast.NodeId ! ParseError { if next_is(state, .enum) return try parse_enum_type(state) if next_is(state, .union) return try parse_union_type(state) if next_is(state, .match) return try parse_match(state) + if next_is(state, .if) return try parse_if_expr(state) if (next_is(state, .distinct) or next_is(state, .alias)) return try parse_type_wrapper(state) token :: try consume(state, .ident) type := try ast.add_identifier(state.builder, token.span, token.data.str_id) @@ -883,6 +914,7 @@ starts_type proc(kind lexer.TokenKind) bool { return ( kind == .ident or kind == .open_bracket or kind == .struct or kind == .c_struct or kind == .opaque or kind == .enum or kind == .union or kind == .distinct or kind == .alias or kind == .match + or kind == .if or kind == .at or kind == .star or kind == .question or kind == .open_paren or kind == .proc or kind == .c_func ) } @@ -1658,6 +1690,7 @@ named_binding_starts proc(state @State) bool { next_is(state, .ident) and (next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal) or next_n_is(state, 1, .ident) or next_n_is(state, 1, .proc) or next_n_is(state, 1, .match) + or next_n_is(state, 1, .if) or next_n_is(state, 1, .at) or next_n_is(state, 1, .question) or (next_n_is(state, 1, .open_paren) and pointer_binding_starts(state)) @@ -1675,6 +1708,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError { .union: return try parse_union_type(state) .distinct, .alias: return try parse_type_wrapper(state) .match: return try parse_match(state) + .if: return try parse_if_expr(state) .dollar: { state.next_token += 1 _ = try consume(state, .open_paren) diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 451dd3e..20ddabb 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -1615,6 +1615,14 @@ demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Cod 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) + } + return + } if (expected.kind == .fallible) { descriptor :: state.arrays.items[usize(expected.array_type)] actual := group_type(state, id) @@ -1691,14 +1699,6 @@ demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Cod state.pointers.items[usize(expected.pointer_type)].element, code) 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) - } - return - } literal_node :: state.cells[index].literal if ( expected.nominal != ast.NodeId(maxval!(u32)) diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 15c610d..006fb05 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -4349,6 +4349,18 @@ 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) + .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) + if (annotation == no_node) continue + result = annotation + if (!flexible(state, arm.body)) return result + } + return result + } .builtin_call: { name :: ast.literal_text(&state.tree, ast.builtin_call(&state.tree, id).name) count :: ast.call_arg_count(&state.tree, id) diff --git a/tests/native.py b/tests/native.py index b7bff14..2dc449a 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,105 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "conditional_expressions_evaluate_guard_once_and_only_selected_arm": ( + """ + next proc(calls @mut i16, value bool) bool { calls^ = calls^ + 1 + return value } + main proc() [3]i16 { + calls i16 := 0 + first i16 :: if next(&calls, true) 42 else divtrunc!(1, 0) + second i16 :: if next(&calls, false) divtrunc!(1, 0) else 7 + return [first, second, calls] + } + """, b"[42, 7, 2]\n", None, + ), + "conditional_expressions_nest_bind_else_and_preserve_precedence": ( + """ + choose proc(a, b bool) i16 { return if (a) if (b) 1 else 40 else 3 + 4 } + main proc() [3]i16 { + return [choose(true, false) + 2, choose(false, true), + 2 * (if (false) 10 else 21)] + } + """, b"[42, 7, 42]\n", None, + ), + "conditional_expressions_preserve_aggregate_copy_isolation": ( + """ + Item :: struct { values [1]mut i16 } + choose proc(flag bool, left, right Item) Item { return if (flag) left else right } + main proc() i16 { + left Item := { values = [42] } + right Item :: { values = [7] } + saved :: choose(true, left, right) + left.values[0] = 0 + return saved.values[0] + } + """, b"42\n", None, + ), + "conditional_expressions_select_generated_types_and_specialized_bodies": ( + """ + Choose proc(wide bool) type { return if (wide) i16 else i8 } + length proc(flag bool) usize { return if (flag) 2 else 1 } + Box proc($wide bool) type { + return struct { + value Choose(wide) + items [length(wide)]i16 + other (if (wide) i16 else i8) + } + } + select proc($flag bool, value i16) i16 { + return if $(flag) value else compile_error!("unselected") + } + main proc() [3]i16 { + value Box(true) :: { value = 40, items = [1, 2], other = 42 } + return [value.value, i16(value.items.len), select(true, value.other)] + } + """, b"[40, 2, 42]\n", None, + ), + "conditional_expressions_keep_outer_match_default": ( + """ + choose proc(flag bool) i16 { + return match flag { + true: (if (flag) 42 + else 1) + else: 7 + } + } + main proc() i16 { return choose(false) } + """, b"7\n", None, + ), + "conditional_expressions_check_unselected_runtime_result_types": ( + """ + choose proc(flag bool) i16 { return if (flag) i16(42) else u8(1) } + main proc() i16 { return choose(true) } + """, b"42\n", b"incompatible value type", + ), + "conditional_expressions_require_else": ( + "main proc() i16 { return if (true) 42 }", + b"", b"unexpected token", + ), + "conditional_expressions_preserve_optional_and_fallible_result_context": ( + """ + Error :: enum { failed } + optional proc(flag bool) ?i16 { return if (flag) 42 else null } + fallible proc(flag bool) i16 ! Error { return if (flag) 42 else .failed } + main proc() [3]i16 { + if (optional(false) != null) return [0, 0, 0] + return [optional(true)?, fallible(true) catch 0, fallible(false) catch 7] + } + """, b"[42, 42, 7]\n", None, + ), + "conditional_expressions_materialize_direct_constants": ( + """ + main proc() [2]i16 { + return [i16($(if (true) 42 else divtrunc!(1, 0))), + $(if (false) 0 else i16(7))] + } + """, b"[42, 7]\n", None, + ), + "conditional_expressions_reject_nonboolean_guards": ( + "main proc() i16 { return if (1) 42 else 0 }", + b"", b"incompatible value type", + ), "integer_divtrunc_preserves_sign_and_single_evaluation": ( """ next proc(calls @mut i16) i16 { calls^ = calls^ + 1