From 303a21a8f5077f6072acbd661f3c6a5bd1aa57c2 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Mon, 26 Oct 2026 16:39:35 +0100 Subject: [PATCH] feat: specialize constant-input type-producing procedures --- README.md | 2 +- TODO.md | 6 +- source/compilation/compilation.test.hon | 4 +- source/sema/specialize/specialize.hon | 19 ++-- tests/native.py | 122 +++++++++++++++++++++--- 5 files changed, 127 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 918c059..e8aed88 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ main proc() i16 { } ``` -the specialization checkpoint supports type arguments and constant integer, boolean, string, callback and null value arguments; declared value-parameter types survive specialization, including narrow checked arithmetic and optional captures/unwrap. integer arguments are range-checked even when unused, and erased argument expressions still receive ordinary checking. immutable constant expressions may use unary signs, `!`, `+`, `-`, `*`, comparisons and logical operators. type-producing procedures require compile-time parameters (or no parameters) and support immutable locals, conditional returns, nested type-producing calls and generated record/array storage, including pointer-recursive records. runaway specialization is diagnosed at 128 nested evaluation/specialization calls or 4096 instances. +the specialization checkpoint supports type arguments and constant integer, boolean, string, callback and null value arguments; declared value-parameter types survive specialization, including narrow checked arithmetic and optional captures/unwrap. integer arguments are range-checked even when unused, and erased argument expressions still receive ordinary checking. immutable constant expressions may use unary signs, `!`, `+`, `-`, `*`, comparisons and logical operators. type-producing procedures accept ordinary parameters as well as `$` parameters: every argument must be supplied explicitly and evaluate to a constant, and every input participates in specialization caching. for example, `Fixed proc(T type, count usize) type { return struct { values [count]T } }` accepts `Fixed(i16, 2)` and reuses its nominal type for equivalent inputs. use `$` when a parameter must be visible in subsequent signature annotations. type-producing bodies support immutable locals, conditional returns, nested type-producing calls and generated record/array storage, including pointer-recursive records; mutable type-valued locals and loops in these bodies remain unsupported. runaway specialization is diagnosed at 128 nested evaluation/specialization calls or 4096 instances. 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. diff --git a/TODO.md b/TODO.md index 20e6156..fe22227 100644 --- a/TODO.md +++ b/TODO.md @@ -22,8 +22,9 @@ 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. compile-time `if` selection and type comparisons -specialize only the selected body; general type-valued execution, reflection and -generic library acceptance remain open. +specialize only the selected body. type-producing procedures also accept ordinary +constant inputs, cached and erased like explicit compile-time arguments; 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. file commands retain single-file behavior; directory commands compile the @@ -64,6 +65,7 @@ test discovery and FFI remain outside these completed slices. - [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. - [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. + - [x] constant-input type-procedure checkpoint: type-producing procedures accept ordinary parameters alongside `$` parameters, require every argument explicitly and evaluate/cache/erase all inputs. generated record/array storage, nominal cache identity, nested calls, dependent contextual aggregate inputs, exact erased-argument checking, source diagnostics, imported producers and repeatable compilation work through interpreter/native execution. mutable type-valued execution remains open. - [ ] 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. diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index d6b54fc..71fe999 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -80,13 +80,13 @@ recompiles_specialized_calls_without_borrowing_previous_ir test { `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 } } + `Box proc(T type) type { return struct { value T } } `count proc() usize { ` result := 0 ` while (result < 2) result = result + 1 ` return result `} - `Fixed proc($N usize) type { return struct { values [N]i16 } } + `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)) } diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 5560e64..caece0c 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -360,17 +360,15 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { state.depth += 1 defer state.depth -= 1 count :: ast.function_param_count(&state.tree, function) + # A type result has no runtime call: every input participates in specialization. + type_result :: is_type(state, ast.function_decl(&state.tree, function).return_type) runtime := usize(0) for (0..count) |i| { - if (!ast.param(&state.tree, ast.function_param(&state.tree, function, i)).comptime) runtime += 1 - } - if (runtime != 0 and is_type(state, ast.function_decl(&state.tree, function).return_type)) { - try reject(state, call, .comptime_argument_required) - return + if (!type_result and !ast.param(&state.tree, ast.function_param(&state.tree, function, i)).comptime) runtime += 1 } supplied :: ast.call_arg_count(&state.tree, call) explicit :: supplied == count - if (!explicit and supplied != runtime) { + if (!explicit and (type_result or supplied != runtime)) { try reject(state, call, .argument_count_mismatch) return } @@ -387,7 +385,7 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { argument :: ast.call_arg(&state.tree, call, position) position += 1 try expand(state, argument) - if (view.comptime) { + if (type_result or view.comptime) { state.evaluation_diagnostic = diagnostics.no_diagnostic annotation := view.type if (contextual_literal(state, argument)) @@ -507,7 +505,7 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { for (0..count) |i| { parameter :: ast.function_param(&state.tree, signature, i) view :: ast.param(&state.tree, parameter) - if (!view.comptime or is_type(state, view.type)) continue + if ((!type_result and !view.comptime) or is_type(state, view.type)) continue original :: ast.call_arg(&state.tree, call, i) constraint :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, original), view.type, original) try refresh(state) @@ -528,6 +526,7 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { @hide:file instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error { function :: state.instances.items[index].function + type_result :: is_type(state, ast.function_decl(&state.tree, function).return_type) count :: state.original_count clone_start :: ast.node_count(&state.tree) mapping :: state.mapping @@ -535,7 +534,7 @@ instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error { parameters :: ast.function_param_count(&state.tree, function) for (0..parameters) |i| { parameter :: ast.function_param(&state.tree, function, i) - if (!ast.param(&state.tree, parameter).comptime) continue + if (!type_result and !ast.param(&state.tree, parameter).comptime) continue value :: state.instances.items[index].arguments[i] replacement := value.node annotation :: ast.param(&state.tree, parameter).type @@ -571,7 +570,7 @@ instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error { for (0..parameters) |i| { parameter :: ast.function_param(&state.tree, clone, i) annotation :: ast.param(&state.tree, parameter).type - if (ast.param(&state.tree, parameter).comptime and !is_type(state, annotation)) { + if ((type_result or ast.param(&state.tree, parameter).comptime) and !is_type(state, annotation)) { if (!try valid_value(state, annotation, state.instances.items[index].arguments[i])) { value :: state.instances.items[index].arguments[i] code :: if (value.kind == .integer and try integer_type(state, annotation)) diff --git a/tests/native.py b/tests/native.py index 0c898e9..f470590 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,115 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "type_producer_ordinary_inputs_generate_storage_and_cache_identity": ( + """ + Fixed proc(T type, count usize) type { return struct { values [count]T } } + main proc() i16 { + value Fixed(i16, 2) :: { values = [40, 2] } + same Fixed(i16, 1 + 1) :: value + return same.values[0] + same.values[1] + } + """, b"42\n", None, + ), + "type_producer_mixes_explicit_and_ordinary_constant_inputs": ( + """ + Fixed proc($T type, count usize) type { return [count]T } + Choose proc(wide bool) type { + if (wide) return Fixed(i16, 2) + return Fixed(i8, 2) + } + main proc() i16 { + wide Choose(true) :: [40, 2] + narrow Choose(false) :: [1, 1] + return wide[0] + wide[1] + i16(narrow[0] - narrow[1]) + } + """, b"42\n", None, + ), + "type_producer_computed_ordinary_inputs_reuse_typed_execution": ( + """ + count proc(seed usize) usize { + result := seed + while (result < 2) result = result + 1 + return result + } + Fixed proc(size usize) type { return struct { values [size]i16 } } + main proc() i16 { + first Fixed(count(0)) :: { values = [40, 2] } + same Fixed(2) :: first + return same.values[0] + same.values[1] + } + """, b"42\n", None, + ), + "type_producer_ordinary_inputs_preserve_nominal_separation": ( + """ + Tag proc(value usize) type { return struct { value i16 } } + main proc() bool { return $(Tag(1) != Tag(2)) } + """, b"true\n", None, + ), + "type_producer_dependent_contextual_ordinary_input": ( + """ + Fixed proc($T type, $N usize, data [N]T) type { + return struct { values [N]T = data } + } + main proc() i16 { + value Fixed(i16, 2, [40, 2]) :: {} + same Fixed(i16, 2, [20 + 20, 2]) :: value + return same.values[0] + same.values[1] + } + """, b"42\n", None, + ), + "type_producer_rejects_runtime_dependency": ( + """ + Fixed proc(size usize) type { return [size]i16 } + read proc(size usize) i16 { + data Fixed(size) :: [40, 2] + return data[0] + data[1] + } + main proc() i16 { return read(2) } + """, b"", b"generic argument must be a compile-time value", + ), + "type_producer_checks_unused_ordinary_input_range": ( + """ + Tag proc(ignored u8) type { return struct { value i16 } } + main proc() i16 { + value Tag(256) :: { value = 42 } + return value.value + } + """, b"", b"numeric literal is outside", + ), + "type_producer_checks_erased_ordinary_input_exact_type": ( + """ + Tag proc(ignored i16) type { return struct { value i16 } } + main proc() i16 { + wrong u16 :: 1 + first Tag(1) :: { value = 42 } + same Tag(wrong) :: first + return same.value + } + """, b"42\n", b"incompatible value type", + ), + "type_producer_requires_all_ordinary_inputs": ( + """ + Tag proc(ignored i16) type { return struct { value i16 } } + main proc() i16 { + value Tag() :: { value = 42 } + return value.value + } + """, b"", b"argument count does not match", + ), + "type_producer_ordinary_input_trap_keeps_original_diagnostic": ( + """ + count proc() usize { + high usize :: 18446744073709551615 + return high + 1 + } + Fixed proc(size usize) type { return [size]i16 } + main proc() i16 { + value Fixed(count()) :: [40, 2] + return value[0] + value[1] + } + """, b"", b"integer arithmetic overflow", + ), "comptime_composed_type_identity_is_order_independent_and_cached": ( """ Left :: enum { left } @@ -787,15 +896,6 @@ CASES = { } """, b"42\n", None, ), - "specialization_type_producer_rejects_runtime_parameters": ( - """ - Box proc($T type, ignored i16) type { return struct { value T } } - main proc() i16 { - value Box(i16, true) :: { value = 42 } - return value.value - } - """, b"", b"generic argument must be a compile-time value", - ), "specialization_rejects_invalid_compile_time_comparison": ( """ ignore proc($flag bool) i16 { return 42 } @@ -2974,8 +3074,8 @@ def check_specialization_packages(temporary, executable): app.mkdir(parents=True) library.mkdir() (library / "types.hon").write_text(textwrap.dedent(""" - Box proc($T type) type { return struct { value T } } - Fixed proc($T type, $N usize) type { + Box proc(T type) type { return struct { value T } } + Fixed proc($T type, N usize) type { return struct { values [N]mut T } } """).strip() + "\n")