feat: specialize constant-input type-producing procedures
This commit is contained in:
@@ -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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
ordinary procedures can compute scalar and nested record/array compile-time values
|
||||||
using mutable locals, loops and recursive calls; explicit `$()` expressions use
|
using mutable locals, loops and recursive calls; explicit `$()` expressions use
|
||||||
the same constant evaluation path. compile-time `if` selection and type comparisons
|
the same constant evaluation path. compile-time `if` selection and type comparisons
|
||||||
specialize only the selected body; general type-valued execution, reflection and
|
specialize only the selected body. type-producing procedures also accept ordinary
|
||||||
generic library acceptance remain open.
|
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
|
the driver retains frontend storage and source provenance through
|
||||||
`@source/compilation`, with compilation-wide AST node identities.
|
`@source/compilation`, with compilation-wide AST node identities.
|
||||||
file commands retain single-file behavior; directory commands compile the
|
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] 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] 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] 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 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`.
|
- [ ] 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.
|
- [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library.
|
||||||
|
|||||||
@@ -80,13 +80,13 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
|
|||||||
`identity proc($T type, value T) T {
|
`identity proc($T type, value T) T {
|
||||||
` if (T == i16) return value else return value.missing
|
` 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 {
|
`count proc() usize {
|
||||||
` result := 0
|
` result := 0
|
||||||
` while (result < 2) result = result + 1
|
` while (result < 2) result = result + 1
|
||||||
` return result
|
` 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] }
|
`data proc() [2]i16 { return [40, 2] }
|
||||||
`main proc() i16 {
|
`main proc() i16 {
|
||||||
` if $(i16 != i16) { return $(i8(128)) }
|
` if $(i16 != i16) { return $(i8(128)) }
|
||||||
|
|||||||
@@ -360,17 +360,15 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error {
|
|||||||
state.depth += 1
|
state.depth += 1
|
||||||
defer state.depth -= 1
|
defer state.depth -= 1
|
||||||
count :: ast.function_param_count(&state.tree, function)
|
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)
|
runtime := usize(0)
|
||||||
for (0..count) |i| {
|
for (0..count) |i| {
|
||||||
if (!ast.param(&state.tree, ast.function_param(&state.tree, function, i)).comptime) runtime += 1
|
if (!type_result and !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
|
|
||||||
}
|
}
|
||||||
supplied :: ast.call_arg_count(&state.tree, call)
|
supplied :: ast.call_arg_count(&state.tree, call)
|
||||||
explicit :: supplied == count
|
explicit :: supplied == count
|
||||||
if (!explicit and supplied != runtime) {
|
if (!explicit and (type_result or supplied != runtime)) {
|
||||||
try reject(state, call, .argument_count_mismatch)
|
try reject(state, call, .argument_count_mismatch)
|
||||||
return
|
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)
|
argument :: ast.call_arg(&state.tree, call, position)
|
||||||
position += 1
|
position += 1
|
||||||
try expand(state, argument)
|
try expand(state, argument)
|
||||||
if (view.comptime) {
|
if (type_result or view.comptime) {
|
||||||
state.evaluation_diagnostic = diagnostics.no_diagnostic
|
state.evaluation_diagnostic = diagnostics.no_diagnostic
|
||||||
annotation := view.type
|
annotation := view.type
|
||||||
if (contextual_literal(state, argument))
|
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| {
|
for (0..count) |i| {
|
||||||
parameter :: ast.function_param(&state.tree, signature, i)
|
parameter :: ast.function_param(&state.tree, signature, i)
|
||||||
view :: ast.param(&state.tree, parameter)
|
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)
|
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)
|
constraint :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, original), view.type, original)
|
||||||
try refresh(state)
|
try refresh(state)
|
||||||
@@ -528,6 +526,7 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error {
|
|||||||
@hide:file
|
@hide:file
|
||||||
instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error {
|
instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error {
|
||||||
function :: state.instances.items[index].function
|
function :: state.instances.items[index].function
|
||||||
|
type_result :: is_type(state, ast.function_decl(&state.tree, function).return_type)
|
||||||
count :: state.original_count
|
count :: state.original_count
|
||||||
clone_start :: ast.node_count(&state.tree)
|
clone_start :: ast.node_count(&state.tree)
|
||||||
mapping :: state.mapping
|
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)
|
parameters :: ast.function_param_count(&state.tree, function)
|
||||||
for (0..parameters) |i| {
|
for (0..parameters) |i| {
|
||||||
parameter :: ast.function_param(&state.tree, function, 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]
|
value :: state.instances.items[index].arguments[i]
|
||||||
replacement := value.node
|
replacement := value.node
|
||||||
annotation :: ast.param(&state.tree, parameter).type
|
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| {
|
for (0..parameters) |i| {
|
||||||
parameter :: ast.function_param(&state.tree, clone, i)
|
parameter :: ast.function_param(&state.tree, clone, i)
|
||||||
annotation :: ast.param(&state.tree, parameter).type
|
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])) {
|
if (!try valid_value(state, annotation, state.instances.items[index].arguments[i])) {
|
||||||
value :: state.instances.items[index].arguments[i]
|
value :: state.instances.items[index].arguments[i]
|
||||||
code :: if (value.kind == .integer and try integer_type(state, annotation))
|
code :: if (value.kind == .integer and try integer_type(state, annotation))
|
||||||
|
|||||||
+111
-11
@@ -11,6 +11,115 @@ HONEY = ROOT / "build/honey"
|
|||||||
|
|
||||||
# Explicit expectations guard against frontend bugs shared by both execution paths.
|
# Explicit expectations guard against frontend bugs shared by both execution paths.
|
||||||
CASES = {
|
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": (
|
"comptime_composed_type_identity_is_order_independent_and_cached": (
|
||||||
"""
|
"""
|
||||||
Left :: enum { left }
|
Left :: enum { left }
|
||||||
@@ -787,15 +896,6 @@ CASES = {
|
|||||||
}
|
}
|
||||||
""", b"42\n", None,
|
""", 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": (
|
"specialization_rejects_invalid_compile_time_comparison": (
|
||||||
"""
|
"""
|
||||||
ignore proc($flag bool) i16 { return 42 }
|
ignore proc($flag bool) i16 { return 42 }
|
||||||
@@ -2974,8 +3074,8 @@ def check_specialization_packages(temporary, executable):
|
|||||||
app.mkdir(parents=True)
|
app.mkdir(parents=True)
|
||||||
library.mkdir()
|
library.mkdir()
|
||||||
(library / "types.hon").write_text(textwrap.dedent("""
|
(library / "types.hon").write_text(textwrap.dedent("""
|
||||||
Box proc($T type) type { return struct { value T } }
|
Box proc(T type) type { return struct { value T } }
|
||||||
Fixed proc($T type, $N usize) type {
|
Fixed proc($T type, N usize) type {
|
||||||
return struct { values [N]mut T }
|
return struct { values [N]mut T }
|
||||||
}
|
}
|
||||||
""").strip() + "\n")
|
""").strip() + "\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user