diff --git a/README.md b/README.md index db9d5b2..918c059 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,11 @@ the specialization checkpoint supports type arguments and constant integer, bool ordinary procedures may compute integer, boolean, float and string compile-time arguments and return nested records/tuples and fixed arrays from constant inputs, such as `Fixed(i16, length(2))`. calls reuse the ordinary checker, typed IR and interpreter, preserving local type inference, checked arithmetic, mutable locals, range/while loops, break/continue and recursive or specialized helper calls. aggregate arguments receive their declared context, including dependent `[N]T` parameters; cached specializations compare normalized values, not literal spelling or evaluation identity. returned aggregates become owned source literals before temporary interpreter storage is released, preserving by-value copies, exact integer types, string bytes, enum fields, optional fields and IEEE float bits. -`$(expression)` explicitly evaluates a constant expression, including an ordinary procedure call, even in a package without templates. use a typed expression when its shape cannot be inferred independently, such as `$(Point { x = 40 })`; integer literals retain contextual typing. the resulting value works in runtime expressions, bindings and array lengths, while the erased expression still receives ordinary checking. each interpreter evaluation is isolated and limited to 100,000 IR instructions/terminators and 128 active frames; failures retain their originating source locations. runtime parameters and mutable local/global dependencies cannot supply compile-time inputs. data pointers, slices, callbacks, unions and fallible results cannot escape temporary evaluation as generated constants; records/arrays containing these unsupported values are diagnosed rather than retaining interpreter handles. executed foreign calls remain interpreter traps. general type-valued execution, compile-time branch/`inline` expansion, reflection, generated type/default arrays and existing generic-library acceptance remain open. +`$(expression)` explicitly evaluates a constant expression, including an ordinary procedure call, even in a package without templates. use a typed expression when its shape cannot be inferred independently, such as `$(Point { x = 40 })`; integer literals retain contextual typing. the resulting value works in runtime expressions, bindings and array lengths, while the erased expression still receives ordinary checking. each interpreter evaluation is isolated and limited to 100,000 IR instructions/terminators and 128 active frames; failures retain their originating source locations. runtime parameters and mutable local/global dependencies cannot supply compile-time inputs. data pointers, slices, callbacks, unions and fallible results cannot escape temporary evaluation as generated constants; records/arrays containing these unsupported values are diagnosed rather than retaining interpreter handles. executed foreign calls remain interpreter traps. general type-valued execution, compile-time match/`inline` expansion, reflection, generated type/default arrays and existing generic-library acceptance remain open. + +`if $(condition) { ... } else { ... }` evaluates a boolean guard before expanding or checking either body, including calls to ordinary constant-input procedures. concrete generic specializations also select branches whose guards use constants, immutable constant bindings, type comparisons or substituted compile-time parameters. only the selected body is specialized and checked; both bodies are still parsed and name-resolved. selected bodies retain their scope, cleanup and return behavior, and erased guards retain ordinary type checking and source diagnostics. ordinary calls in an implicit guard stay runtime; use `$()` to request their evaluation. optional captures remain runtime, and ordinary nongeneric `if (true)` still checks both bodies. + +type `==` and `!=` comparisons in compile-time expressions and specializations preserve aliases, nominal declaration identity and structural pointer/array/callback signatures. composed error-set identity and specialization caching ignore member ordering, grouping and repetition. for example, `read proc($T type, value T) i16 { if (T == i16) return value else return value.count }` checks the scalar or record path for each concrete call without requiring both operations on every `T`. callback signatures use `@proc(value i16) i16`, including grouped parameter names and trailing commas. compatible named functions can initialize callbacks, which may be passed, returned, replaced and stored in records/arrays; `Callback :: alias @proc(value i16) i16` names a reusable signature. parameter/result types match structurally, retaining nominal record identity and exact pointer mutability. callback fields call directly, such as `allocator.vtable.alloc(context, size, alignment)` once the remaining argument types are supported. `@mut proc` and `*proc` are invalid; `@mut Callback` instead points to writable callback storage. diff --git a/TODO.md b/TODO.md index d6724a8..20e6156 100644 --- a/TODO.md +++ b/TODO.md @@ -21,7 +21,8 @@ cached generic specialization and type-producing procedures support explicit/inf compile-time arguments and generated record/array storage through both paths. ordinary procedures can compute scalar and nested record/array compile-time values using mutable locals, loops and recursive calls; explicit `$()` expressions use -the same constant evaluation path. general type-valued execution, reflection and +the same constant evaluation path. compile-time `if` selection and type comparisons +specialize only the selected body; general type-valued execution, reflection and generic library acceptance remain open. the driver retains frontend storage and source provenance through `@source/compilation`, with compilation-wide AST node identities. @@ -62,7 +63,8 @@ test discovery and FFI remain outside these completed slices. - [x] specialization checkpoint: explicit/inferred type and integer/boolean/string/callback/null value parameters, cached concrete runtime calls, nominal type-producing procedures, generated record/array storage and recursive data pointers through interpreter/native execution. preserve declared value types, source diagnostics and repeatable compilation; runtime values cannot supply compile-time arguments. - [x] compile-time execution checkpoint: ordinary procedure calls with constant inputs and integer/boolean/string results reuse checking, typed IR and bounded interpreter execution. mutable scalar locals, range/while loops, break/continue, nested/generic/recursive calls, exact numeric types, runtime traps and dependency-file provenance survive argument erasure through interpreter/native execution. runtime parameters and mutable global dependencies are rejected; recompilation retains no prior execution state. - [x] aggregate/explicit-expression checkpoint: computed nested records/tuples and fixed arrays, contextual/dependent aggregate value parameters, normalized specialization caching and explicit `$()` expressions through interpreter/native execution. preserve by-value isolation, zero-cell shapes, integer widths, string ownership, enum/optional fields and exact IEEE float bits; reject runtime dependencies and values that would retain temporary interpreter storage. repeatable compilation restores erased expressions. - - [ ] remaining gate: general type-valued compile-time execution, compile-time branch/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), broader constant materialization (including unions, fallible results and storage-bearing values), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates. + - [x] conditional specialization checkpoint: explicit compile-time `if` guards and side-effect-free constant guards in concrete generic procedures select a body before specialization/checking. type equality retains aliases, nominal identity and structural signatures; composed error-set identity/cache keys ignore ordering, grouping and repetition. guard checking, selected-body diagnostics, scoped cleanup, runtime guards and repeatable compilation work through interpreter/native execution; both arms remain parsed and name-resolved. + - [ ] remaining gate: general type-valued compile-time execution, compile-time match/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), broader constant materialization (including unions, fallible results and storage-bearing values), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates. - [ ] remaining source constructs: collection/index iteration, expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. - [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library. - [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support. diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index 8d87f2a..d6b54fc 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -77,7 +77,9 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { defer deinit(&session) root :: try add_package(&session) bytes := try owned_text( - `identity proc($T type, value T) T { return value } + `identity proc($T type, value T) T { + ` if (T == i16) return value else return value.missing + `} `Box proc($T type) type { return struct { value T } } `count proc() usize { ` result := 0 @@ -87,6 +89,7 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { `Fixed proc($N usize) type { return struct { values [N]i16 } } `data proc() [2]i16 { return [40, 2] } `main proc() i16 { + ` if $(i16 != i16) { return $(i8(128)) } ` values :: $(data()) ` box Box(i16) :: { value = identity(i16, values[0]) } ` fixed Fixed(count()) :: { values = [box.value, 2] } diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index fa17070..4fa472a 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -3422,7 +3422,8 @@ type_syntax proc(kind ast.ExprKind) bool { @hide:file type_reference proc(state @State, id ast.NodeId) bool { - if (type_syntax(ast.expr_kind(state.tree, id))) return true + kind :: ast.expr_kind(state.tree, id) + if (type_syntax(kind) or kind == .array_type) return true binding :: sema.binding(state.resolution, id) if (binding.kind == .builtin) return true if (binding.kind != .declaration) return false diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index a31a1e5..5560e64 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -189,6 +189,41 @@ 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 == .statement and ast.stmt_kind(&state.tree, id) == .if_stmt) { + conditional :: ast.if_stmt(&state.tree, id) + explicit :: ast.expr_kind(&state.tree, conditional.condition) == .comptime_expr + if (conditional.capture == null and (explicit or usize(id) >= state.original_count)) { + condition :: if (explicit) ast.comptime_operand(&state.tree, conditional.condition) else conditional.condition + try expand(state, condition) + if (explicit or branch_constant(state, condition, 0)) { + state.evaluation_diagnostic = diagnostics.no_diagnostic + boolean :: try builtin(state, condition, .bool) + value :: try evaluate_ir(state, condition, boolean) + if (explicit and value.kind == .invalid and state.evaluation_diagnostic == diagnostics.no_diagnostic) + state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, + .comptime_argument_required, ast.node_span(&state.tree, condition)) + if (state.evaluation_diagnostic != diagnostics.no_diagnostic) { + invalid :: try ast.add_invalid_stmt(state.builder, ast.node_span(&state.tree, id), + state.evaluation_diagnostic) + try replace(state, id, invalid) + try refresh(state) + return + } + if (value.kind == .boolean) { + branch := conditional.then_block + if (!value.boolean) { + if (conditional.else_block) |other| branch = other + else branch = try ast.add_block(state.builder, ast.node_span(&state.tree, id), &[]) + } + replacement :: try ast.add_block_stmt(state.builder, branch) + try replace(state, id, replacement) + try refresh(state) + try expand(state, branch) + return + } + } + } + } if (class == .expression and ast.expr_kind(&state.tree, id) == .comptime_expr) { operand :: ast.comptime_operand(&state.tree, id) try arraylist.append(&state.rewrites.constraints, operand) @@ -225,6 +260,17 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { child :: ast.child(&state.tree, id, i) try expand(state, child) } + if (class == .expression and ast.expr_kind(&state.tree, id) == .binary) { + view :: ast.binary_expr(&state.tree, id) + if ( + (view.operator == .equal or view.operator == .not_equal) + and branch_constant(state, view.left, 0) and branch_constant(state, view.right, 0) + ) { + left :: try evaluate(state, view.left) + right :: try evaluate(state, view.right) + if (left.kind == .type and right.kind == .type) _ = try evaluate_binary(state, id) + } + } if (usize(id) >= state.original_count and class == .expression and ast.expr_kind(&state.tree, id) == .array_type) { view :: ast.array_type(&state.tree, id) if (view.count) |count_node| { @@ -241,6 +287,48 @@ expand proc(state @mut State, id ast.NodeId) void ! Error { } } +# Only probe side-effect-free expressions. Ordinary calls stay runtime unless the +# source requests evaluation with $(); probing must not execute FFI or emit traps. +@hide:file +branch_constant proc(state @State, id ast.NodeId, depth usize) bool { + if (depth >= 128) return false + resolved :: binding(state, id) + if (resolved.kind == .builtin) return true + if (resolved.kind == .declaration) { + target :: resolved.data.declaration + if (ast.node_class(&state.tree, target) == .function) return true + if (ast.node_class(&state.tree, target) != .statement) return false + declaration :: ast.statement_decl(&state.tree, target) + return ( + declaration.mutability == .constant + and branch_constant(state, declaration.initializer, depth + 1) + ) + } + match ast.expr_kind(&state.tree, id) { + .literal_int, .literal_float, .literal_bool, .literal_string, .literal_null: return true + .typed_constant: return branch_constant(state, ast.typed_constant(&state.tree, id).value, depth + 1) + .unary: { + view :: ast.unary_expr(&state.tree, id) + return ( + (view.operator == .negate or view.operator == .logical_not) + and branch_constant(state, view.operand, depth + 1) + ) + } + .binary: { + view :: ast.binary_expr(&state.tree, id) + return ( + branch_constant(state, view.left, depth + 1) + and branch_constant(state, view.right, depth + 1) + ) + } + .pointer_type: return branch_constant(state, ast.pointer_type(&state.tree, id).element, depth + 1) + .optional_type, .alias_expr: return branch_constant(state, ast.type_operand(&state.tree, id), depth + 1) + .record_type, .union_type, .enum_type, .distinct_type, .array_type, .slice_type, .function_type, + .fallible_type, .sum_type: return true + else: return false + } +} + @hide:file builtin proc(state @mut State, at ast.NodeId, kind sema.BuiltinType) ast.NodeId ! Error { id :: try ast.add_identifier(state.builder, ast.node_span(&state.tree, at), strpool.no_str) @@ -1022,6 +1110,19 @@ evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error { if (view.operator == .equal or view.operator == .not_equal) { if (left.kind != right.kind) return Value{} equal :: try value_equal(state, left, right) + if (left.kind == .type) { + operands [2]ast.NodeId :: [view.left, view.right] + for (operands) |operand| { + constraint :: try ast.add_type_operand(state.builder, + ast.node_span(&state.tree, operand), operand, true) + try refresh(state) + try arraylist.append(&state.rewrites.constraints, constraint) + } + folded :: try ast.add_boolean(state.builder, ast.node_span(&state.tree, id), + equal == (view.operator == .equal)) + try replace(state, id, folded) + try refresh(state) + } return Value{ kind = .boolean, node = id, boolean = equal == (view.operator == .equal) } } if (left.kind == .boolean and right.kind == .boolean) { @@ -1078,6 +1179,8 @@ same_type proc(state @mut State, left, right ast.NodeId) bool ! Error { return x.kind == .builtin and y.kind == .builtin and builtin_identity(x.data.builtin) == builtin_identity(y.data.builtin) } kind :: ast.expr_kind(&state.tree, a) + if (kind == .sum_type or ast.expr_kind(&state.tree, b) == .sum_type) + return try type_subset(state, a, b) and try type_subset(state, b, a) if (kind != ast.expr_kind(&state.tree, b)) return false match kind { .pointer_type: { @@ -1103,7 +1206,7 @@ same_type proc(state @mut State, left, right ast.NodeId) bool ! Error { return try same_type(state, p.element, q.element) } .optional_type: return try same_type(state, ast.type_operand(&state.tree, a), ast.type_operand(&state.tree, b)) - .fallible_type, .sum_type: { + .fallible_type: { p :: ast.binary_expr(&state.tree, a) q :: ast.binary_expr(&state.tree, b) return try same_type(state, p.left, q.left) and try same_type(state, p.right, q.right) @@ -1123,6 +1226,22 @@ same_type proc(state @mut State, left, right ast.NodeId) bool ! Error { } } +@hide:file +type_subset proc(state @mut State, left, right ast.NodeId) bool ! Error { + a :: try canonical_type(state, left) + b :: try canonical_type(state, right) + if (a == no_node or b == no_node) return false + if (ast.expr_kind(&state.tree, a) == .sum_type) { + view :: ast.binary_expr(&state.tree, a) + return try type_subset(state, view.left, b) and try type_subset(state, view.right, b) + } + if (ast.expr_kind(&state.tree, b) == .sum_type) { + view :: ast.binary_expr(&state.tree, b) + return try type_subset(state, a, view.left) or try type_subset(state, a, view.right) + } + return try same_type(state, a, b) +} + @hide:file value_equal proc(state @mut State, left, right Value) bool ! Error { if (left.kind != right.kind) return false @@ -1201,7 +1320,13 @@ type_hash proc(state @mut State, id ast.NodeId) usize ! Error { return mix(hash, try type_hash(state, view.element)) } .optional_type: return mix(hash, try type_hash(state, ast.type_operand(&state.tree, node))) - .fallible_type, .sum_type: { + .sum_type: { + view :: ast.binary_expr(&state.tree, node) + # Set fingerprints must ignore ordering, grouping and repeated members. + # Colliding fingerprints still use full type equality in the cache chain. + return try type_hash(state, view.left) | try type_hash(state, view.right) + } + .fallible_type: { view :: ast.binary_expr(&state.tree, node) return mix(mix(hash, try type_hash(state, view.left)), try type_hash(state, view.right)) } diff --git a/tests/native.py b/tests/native.py index fcee14d..0c898e9 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,150 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "comptime_composed_type_identity_is_order_independent_and_cached": ( + """ + Left :: enum { left } + Right :: enum { right } + Box proc($T type) type { return struct { value T } } + identical proc($A, $B type) bool { return A == B } + main proc() bool { + first Box(Left | Right) :: { value = .left } + same Box(Right | Left | Left) :: first + return ( + identical(Left | Right, Right | Left | Left) + and identical(Left, Left | Left) and same.value == .left + ) + } + """, b"true\n", None, + ), + "comptime_callback_branch_uses_optional_function_identity": ( + """ + Callback :: alias @proc(value i16) i16 + increment proc(value i16) i16 { return value + 1 } + apply proc($callback ?Callback, value i16) i16 { + if $(callback == null) { return value } + else { return callback?(value) } + } + main proc() i16 { return apply(null, 40) + apply(increment, 1) } + """, b"42\n", None, + ), + "comptime_branch_rejects_runtime_guard_dependencies": ( + """ + answer proc(take bool) i16 { + if $(take) { return 42 } else { return 0 } + } + main proc() i16 { return answer(true) } + """, b"", b"generic argument must be a compile-time value", + ), + "comptime_branch_checks_short_circuited_guard_operands": ( + """ + main proc() i16 { + if $(true or 1) { return 42 } else { return 0 } + } + """, b"", b"operator requires compatible operand types", + ), + "specialized_type_branches_check_only_selected_storage": ( + """ + count proc($T type, value T) i16 { + if (T == i16) return value else return value.count + } + Box :: struct { count i16 } + main proc() i16 { return count(i16, 40) + count(Box, Box { count = 2 }) } + """, b"42\n", None, + ), + "explicit_comptime_branch_without_templates": ( + """ + enabled proc() bool { return true } + main proc() i16 { + if $(enabled()) { return 42 } else { return false } + } + """, b"42\n", None, + ), + "specialized_branches_skip_recursive_instantiation_and_traps": ( + """ + answer proc($stop bool) i16 { + if (stop) return 42 else return answer(false) + } + main proc() i16 { + if $(false) { return $(i8(128)) } + return answer(true) + } + """, b"42\n", None, + ), + "specialized_branches_preserve_runtime_conditions": ( + """ + answer proc($T type, value T, take bool) i16 { + if (T != i16) return value.count + if (take) return value else return 2 + } + main proc() i16 { return answer(i16, 40, true) + answer(i16, 99, false) } + """, b"42\n", None, + ), + "specialized_branch_keeps_scope_and_cleanup": ( + """ + change proc($take bool, value @mut i16) void { + if (take) { + defer value^ = value^ + 2 + value^ = 40 + } + else value^ = 0 + } + main proc() i16 { + value i16 := 0 + change(true, &value) + return value + } + """, b"42\n", None, + ), + "comptime_type_equality_preserves_aliases_and_nominal_identity": ( + """ + First :: struct { value i16 } + Second :: struct { value i16 } + Same :: alias First + identical proc($A, $B type) bool { return A == B } + main proc() bool { + return ( + identical(First, Same) and !identical(First, Second) + and $(@mut i16 == @mut i16) and $(@mut i16 != @i16) + and $([2]i16 == [1 + 1]i16) and $([2]i16 != [2]u16) + ) + } + """, b"true\n", None, + ), + "specialized_branch_retains_guard_type_errors": ( + """ + answer proc($take bool) i16 { + if (take or 1) return 42 else return false + } + main proc() i16 { return answer(true) } + """, b"", b"operator requires compatible operand types", + ), + "specialized_branch_retains_selected_body_errors": ( + """ + answer proc($T type, value T) i16 { + if (T == i16) return value.missing + return 42 + } + main proc() i16 { return answer(i16, 0) } + """, b"", b"field selection requires a record or tuple", + ), + "ordinary_constant_branch_still_checks_both_arms": ( + """ + main proc() i16 { + if (true) return 42 else return false + } + """, b"42\n", b"return does not match", + ), + "specialized_branch_runtime_call_remains_reachable": ( + """ + fail proc(value i8) bool { return value + 1 == 0 } + answer proc($T type) i16 { + if (fail(127)) return 0 + return 42 + } + main proc() i16 { return answer(i16) } + """, b"", b"integer arithmetic overflow", + ), "comptime_contextual_aggregate_arguments_keep_dependent_types": ( """ sum proc(items [2]i16) i16 { return items[0] + items[1] }