feat: specialize constant-input type-producing procedures

This commit is contained in:
2026-10-26 16:39:35 +01:00
parent 565733666e
commit 303a21a8f5
5 changed files with 127 additions and 26 deletions
+1 -1
View File
@@ -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.
+4 -2
View File
@@ -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.
+2 -2
View File
@@ -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)) }
+9 -10
View File
@@ -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))
+111 -11
View File
@@ -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")