feat: evaluate compile-time scalar procedure calls

This commit is contained in:
2026-10-26 14:50:34 +01:00
parent 6886d743b5
commit ecd20675ea
9 changed files with 404 additions and 11 deletions
+3 -1
View File
@@ -96,7 +96,9 @@ 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. runtime values and mutable bindings cannot supply compile-time arguments. runaway specialization is diagnosed at 128 nested evaluation/specialization calls or 4096 instances. arbitrary compile-time procedure execution, reflection, `inline` iteration and full generic standard-library compilation remain outside this checkpoint. 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.
ordinary procedures may compute integer, boolean and string compile-time arguments 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. each 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 global dependencies cannot supply compile-time inputs, and executed foreign calls remain interpreter traps. aggregate/type-valued execution, explicit `$()` expressions, reflection, `inline` expansion and existing generic-library acceptance remain outside this checkpoint.
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. 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.
+6 -3
View File
@@ -18,8 +18,10 @@ explicit fallible channels, composed error sets, `try`/`catch` and scoped LIFO
`defer`/`errdefer` run through both paths; source errors are values, not diagnostic `defer`/`errdefer` run through both paths; source errors are values, not diagnostic
traps or compiler infrastructure failures. traps or compiler infrastructure failures.
cached generic specialization and type-producing procedures support explicit/inferred cached generic specialization and type-producing procedures support explicit/inferred
compile-time arguments and generated record/array storage through both paths; compile-time arguments and generated record/array storage through both paths.
general compile-time execution/reflection and generic library acceptance remain open. ordinary procedures can compute integer/boolean/string compile-time arguments using
mutable locals, loops and recursive calls; general aggregate/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
@@ -57,7 +59,8 @@ test discovery and FFI remain outside these completed slices.
- [x] fallible results and cleanup: explicit `T ! E`, error-set composition, `try`/`catch`, `defer`/`errdefer`, error captures and matched error residuals through interpreter/native execution; infrastructure errors remain distinct from diagnostic traps. fallible `void` may complete on fallthrough; unhandled entry errors exit with status `1`. inferred `T!` signatures and yielding catch blocks remain with inference/value-control-flow work. examples: `source/main.hon`, `source/sema/check/check.hon`. - [x] fallible results and cleanup: explicit `T ! E`, error-set composition, `try`/`catch`, `defer`/`errdefer`, error captures and matched error residuals through interpreter/native execution; infrastructure errors remain distinct from diagnostic traps. fallible `void` may complete on fallthrough; unhandled entry errors exit with status `1`. inferred `T!` signatures and yielding catch blocks remain with inference/value-control-flow work. examples: `source/main.hon`, `source/sema/check/check.hon`.
- [ ] generic specialization and compile-time execution/reflection sufficient for existing `ArrayList`, hash maps, `enummap`, `meta` and `io.print`; include type/value parameters and compile-time generated storage. - [ ] generic specialization and compile-time execution/reflection sufficient for existing `ArrayList`, hash maps, `enummap`, `meta` and `io.print`; include type/value parameters and compile-time generated storage.
- [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] 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.
- [ ] remaining gate: general compile-time procedure execution, compile-time branch/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates and formatting parsing may interleave with the next source-construct gate. - [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.
- [ ] remaining gate: general aggregate/type-valued compile-time execution, explicit compile-time expressions, compile-time branch/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), 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.
- [ ] 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. - [ ] 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.
+27
View File
@@ -2042,6 +2042,25 @@ add_boolean proc(builder @mut Builder, at Span, value bool) NodeId ! mem.AllocEr
return try add_generated_literal(builder, at, .bool, text) return try add_generated_literal(builder, at, .bool, text)
} }
#! copies arbitrary bytes into owned quoted spelling, retaining `at` for diagnostics.
add_string proc(builder @mut Builder, at Span, bytes []u8) NodeId ! mem.AllocError {
if (bytes.len > divtrunc!(maxval!(usize) - 2, usize(4))) return .out_of_memory
allocator :: builder.storage.generated.allocator
text :: try mem.alloc(u8, allocator, bytes.len * 4 + 2)
errdefer mem.free(allocator, text)
text[0] = '"'
digits :: "0123456789abcdef"
for (bytes) |byte, index| {
start :: index * 4 + 1
text[start] = '\\'
text[start + 1] = 'x'
text[start + 2] = digits[usize(divtrunc!(byte, u8(16)))]
text[start + 3] = digits[usize(rem!(byte, u8(16)))]
}
text[text.len - 1] = '"'
return try add_owned_literal(builder, at, .string, text)
}
@hide:file @hide:file
add_generated_literal proc( add_generated_literal proc(
builder @mut Builder, at Span, kind LiteralKind, bytes []u8, builder @mut Builder, at Span, kind LiteralKind, bytes []u8,
@@ -2051,6 +2070,14 @@ add_generated_literal proc(
text :: try mem.alloc(u8, allocator, bytes.len) text :: try mem.alloc(u8, allocator, bytes.len)
errdefer mem.free(allocator, text) errdefer mem.free(allocator, text)
memcopy!(text, bytes) memcopy!(text, bytes)
return try add_owned_literal(builder, at, kind, text)
}
@hide:file
add_owned_literal proc(
builder @mut Builder, at Span, kind LiteralKind, text []mut u8,
) NodeId ! mem.AllocError {
debug.assert(usize(at.source) < builder.storage.sources.items.len)
index :: builder.storage.generated.items.len index :: builder.storage.generated.items.len
debug.assert(u64(index) < u64(maxval!(u32))) debug.assert(u64(index) < u64(maxval!(u32)))
try arraylist.append(&builder.storage.generated, text) try arraylist.append(&builder.storage.generated, text)
+9 -1
View File
@@ -79,9 +79,17 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
bytes := try owned_text( bytes := try owned_text(
`identity proc($T type, value T) T { return value } `identity proc($T type, value T) T { return value }
`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 } }
`main proc() i16 { `main proc() i16 {
` box Box(i16) :: { value = identity(i16, 40) } ` box Box(i16) :: { value = identity(i16, 40) }
` return identity(box.value) + 2 ` fixed Fixed(count()) :: { values = [box.value, 2] }
` same Fixed(2) :: fixed
` return identity(same.values[0]) + same.values[1]
`} `}
, ,
) )
+2
View File
@@ -111,6 +111,7 @@ Code :: enum {
cannot_infer_generic cannot_infer_generic
specialization_cycle specialization_cycle
specialization_limit specialization_limit
comptime_execution_limit
} }
Diagnostic :: struct { Diagnostic :: struct {
@@ -243,5 +244,6 @@ messages :: $enummap.init(Code, []u8, {
cannot_infer_generic = "cannot infer consistent compile-time arguments from runtime arguments", cannot_infer_generic = "cannot infer consistent compile-time arguments from runtime arguments",
specialization_cycle = "compile-time specialization depends on its unfinished result", specialization_cycle = "compile-time specialization depends on its unfinished result",
specialization_limit = "compile-time specialization exceeds its recursion or instance limit", specialization_limit = "compile-time specialization exceeds its recursion or instance limit",
comptime_execution_limit = "compile-time execution exceeds its recursion or step limit",
}) })
+31
View File
@@ -87,6 +87,28 @@ run proc(
allocator mem.Allocator, allocator mem.Allocator,
module @ir.Module, module @ir.Module,
diagnostic_store @mut diagnostics.Store, diagnostic_store @mut diagnostics.Store,
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
return try execute(false, allocator, module, diagnostic_store, null)
}
#! executes an isolated compile-time module with bounded work and stack depth.
#! ownership and diagnostic identities follow run.
run_evaluation proc(
allocator mem.Allocator,
module @ir.Module,
diagnostic_store @mut diagnostics.Store,
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
remaining := usize(100000)
return try execute(true, allocator, module, diagnostic_store, &remaining)
}
@hide:file
execute proc(
$bounded bool,
allocator mem.Allocator,
module @ir.Module,
diagnostic_store @mut diagnostics.Store,
remaining ?@mut usize,
) Outcome ! (mem.AllocError | diagnostics.CapacityError) { ) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
entry ir.FunctionId := ir.FunctionId(0) entry ir.FunctionId := ir.FunctionId(0)
if module.entry |function| { if module.entry |function| {
@@ -157,6 +179,15 @@ run proc(
function :: &module.functions.items[usize(frames.items[frame_index].function)] function :: &module.functions.items[usize(frames.items[frame_index].function)]
block :: &function.blocks.items[usize(frames.items[frame_index].block)] block :: &function.blocks.items[usize(frames.items[frame_index].block)]
instruction_index :: frames.items[frame_index].next_instruction instruction_index :: frames.items[frame_index].next_instruction
if $(bounded) {
budget :: remaining?
if (budget^ == 0 or frames.items.len > 128) {
span :: if (instruction_index < block.instructions.items.len)
block.instructions.items[instruction_index].span else block.terminator.span
return try runtime_trap(diagnostic_store, .comptime_execution_limit, span)
}
budget^ -= 1
}
if instruction_index == block.instructions.items.len { if instruction_index == block.instructions.items.len {
terminator :: block.terminator terminator :: block.terminator
if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic } if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic }
+29 -2
View File
@@ -72,6 +72,32 @@ lower_package proc(
checked @typecheck.Result, checked @typecheck.Result,
root_package ast.NodeId, root_package ast.NodeId,
diagnostic_store @mut diagnostics.Store, diagnostic_store @mut diagnostics.Store,
) ir.Module ! (mem.AllocError | diagnostics.CapacityError) {
return try lower_module(allocator, tree, resolution, checked, root_package, null, diagnostic_store)
}
#! lowers an isolated compile-time call wrapper without applying program entry-name rules.
#! the caller supplies a checked parameterless function and owns the returned IR.
lower_evaluation proc(
allocator mem.Allocator,
tree @ast.Tree,
resolution @sema.Resolution,
checked @typecheck.Result,
entry ast.NodeId,
diagnostic_store @mut diagnostics.Store,
) ir.Module ! (mem.AllocError | diagnostics.CapacityError) {
return try lower_module(allocator, tree, resolution, checked, tree.root, entry, diagnostic_store)
}
@hide:file
lower_module proc(
allocator mem.Allocator,
tree @ast.Tree,
resolution @sema.Resolution,
checked @typecheck.Result,
root_package ast.NodeId,
entry ?ast.NodeId,
diagnostic_store @mut diagnostics.Store,
) ir.Module ! (mem.AllocError | diagnostics.CapacityError) { ) ir.Module ! (mem.AllocError | diagnostics.CapacityError) {
module := ir.init(allocator) module := ir.init(allocator)
errdefer ir.deinit(&module) errdefer ir.deinit(&module)
@@ -123,7 +149,8 @@ lower_package proc(
.import, .invalid: {} .import, .invalid: {}
} }
} }
try find_entry(&state, root_package) if (entry) |function| module.entry = ir.FunctionId(storage[usize(function)].index)
else try find_entry(&state, root_package)
for (0..ast.package_decl_count(tree)) |i| { for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i) id :: ast.package_decl(tree, i)
match ast.decl_kind(tree, id) { match ast.decl_kind(tree, id) {
@@ -2136,7 +2163,7 @@ lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError
@hide:file @hide:file
lower_string proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { lower_string proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
span :: ast.node_span(state.tree, id) span :: ast.node_span(state.tree, id)
text :: ast.source_text(state.tree, span) text :: ast.literal_text(state.tree, id)
owned :: try mem.alloc(u8, state.module.allocator, text.len - 2) owned :: try mem.alloc(u8, state.module.allocator, text.len - 2)
transferred := false transferred := false
defer if (!transferred) mem.free(state.module.allocator, owned) defer if (!transferred) mem.free(state.module.allocator, owned)
+144 -2
View File
@@ -4,7 +4,12 @@ import "@std/hashmap"
import "@source/ast" import "@source/ast"
import "@source/diagnostics" import "@source/diagnostics"
import "@source/strpool" import "@source/strpool"
import "@source/lower"
import "@source/interpreter"
import "@source/ir"
sema :: import "@source/sema" sema :: import "@source/sema"
typecheck :: import "@source/sema/check"
@hide:file no_node :: maxval!(ast.NodeId) @hide:file no_node :: maxval!(ast.NodeId)
@hide:file no_index :: maxval!(usize) @hide:file no_index :: maxval!(usize)
@@ -81,6 +86,8 @@ State :: struct {
original_count usize original_count usize
rewrites @mut Result rewrites @mut Result
mapping []mut ast.NodeId mapping []mut ast.NodeId
resolution @mut sema.Resolution
evaluation_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
} }
#! elaborates templates into cached concrete declarations in the caller-owned arena. #! elaborates templates into cached concrete declarations in the caller-owned arena.
@@ -117,6 +124,7 @@ elaborate proc(
original_count = ast.node_count(&original), original_count = ast.node_count(&original),
rewrites = &result, rewrites = &result,
mapping = mapping, mapping = mapping,
resolution = resolution,
} }
defer arraylist.deinit(&state.bindings) defer arraylist.deinit(&state.bindings)
defer arraylist.deinit(&state.declarations) defer arraylist.deinit(&state.declarations)
@@ -268,9 +276,10 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error {
position += 1 position += 1
try expand(state, argument) try expand(state, argument)
if (view.comptime) { if (view.comptime) {
state.evaluation_diagnostic = diagnostics.no_diagnostic
arguments[i] = try evaluate(state, argument) arguments[i] = try evaluate(state, argument)
if (arguments[i].kind == .invalid) { if (arguments[i].kind == .invalid) {
try reject(state, call, .comptime_argument_required) try reject_evaluation(state, call)
return return
} }
if (is_type(state, view.type) != (arguments[i].kind == .type)) { if (is_type(state, view.type) != (arguments[i].kind == .type)) {
@@ -458,9 +467,10 @@ instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error {
} }
} }
if (is_type(state, view.return_type)) { if (is_type(state, view.return_type)) {
state.evaluation_diagnostic = diagnostics.no_diagnostic
returned :: try return_type_value(state, view.body) returned :: try return_type_value(state, view.body)
if (returned == no_node) { if (returned == no_node) {
try reject(state, call, .comptime_argument_required) try reject_evaluation(state, call)
return return
} }
result := returned result := returned
@@ -495,6 +505,136 @@ instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error {
} }
} }
@hide:file
reject_evaluation proc(state @mut State, call ast.NodeId) void ! Error {
diagnostic :: state.evaluation_diagnostic
if (diagnostic == diagnostics.no_diagnostic) {
try reject(state, call, .comptime_argument_required)
return
}
invalid :: try ast.add_invalid_expr(state.builder, ast.node_span(&state.tree, call), diagnostic)
try replace(state, call, invalid)
try refresh(state)
state.bindings.items[usize(call)] = sema.Binding{ kind = .invalid, data = sema.BindingData{ diagnostic = diagnostic } }
}
@hide:file
evaluation_dependencies proc(
state @mut State, id ast.NodeId, flags []mut u8, declarations @mut arraylist.ArrayList(ast.NodeId),
) bool ! Error {
if ((flags[usize(id)] & 4) != 0) return true
flags[usize(id)] = flags[usize(id)] | 4
resolved :: binding(state, id)
if (resolved.kind == .declaration) {
target :: resolved.data.declaration
if ((flags[usize(target)] & 1) != 0 and (flags[usize(target)] & 2) == 0) {
if (ast.node_class(&state.tree, target) == .statement
and ast.statement_decl(&state.tree, target).mutability == .variable) return false
flags[usize(target)] = flags[usize(target)] | 2
try arraylist.append(declarations, target)
}
}
for (0..ast.child_count(&state.tree, id)) |i| {
if (!try evaluation_dependencies(state, ast.child(&state.tree, id, i), flags, declarations)) return false
}
return true
}
@hide:file
evaluate_call proc(state @mut State, call ast.NodeId) Value ! Error {
callee :: try evaluate(state, ast.call_expr(&state.tree, call).callee)
if (callee.kind != .function and callee.kind != .type) return Value{}
result_type := callee.node
if (callee.kind == .function) {
resolved :: binding(state, callee.node)
if (resolved.kind != .declaration) return Value{}
result_type = ast.function_decl(&state.tree, resolved.data.declaration).return_type
}
arguments := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&arguments)
for (0..ast.call_arg_count(&state.tree, call)) |i| {
value :: try evaluate(state, ast.call_arg(&state.tree, call, i))
if (value.kind == .invalid) return Value{}
concrete := try materialize(state, value)
if (value.annotation != no_node) {
concrete.node = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, value.node),
value.annotation, concrete.node)
try refresh(state)
}
try arraylist.append(&arguments, concrete.node)
}
# Reuse checking, typed IR and the interpreter: compile-time arithmetic and local
# inference must have exactly the same semantics as an ordinary runtime call.
span :: ast.node_span(&state.tree, call)
invocation :: try ast.add_call(state.builder, span, callee.node, arguments.items)
returned :: try ast.add_return_stmt(state.builder, span, invocation)
statements [1]ast.NodeId :: [returned]
body :: try ast.add_block(state.builder, span, statements[..])
wrapper :: try ast.add_function_decl(state.builder, span, span, strpool.no_str, result_type, &[], body)
try refresh(state)
declarations := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&declarations)
try arraylist.append(&declarations, wrapper)
flags := arraylist.init(u8, state.allocator)
defer arraylist.deinit(&flags)
index := usize(0)
registered := usize(0)
while (index < declarations.items.len) {
declaration :: declarations.items[index]
try expand(state, declaration)
while (flags.items.len < ast.node_count(&state.tree)) try arraylist.append(&flags, u8(0))
while (registered < state.declarations.items.len) {
target :: state.declarations.items[registered]
flags.items[usize(target)] = flags.items[usize(target)] | 1
registered += 1
}
flags.items[usize(declaration)] = flags.items[usize(declaration)] | 2
if (!try evaluation_dependencies(state, declaration, flags.items, &declarations)) return Value{}
index += 1
}
root :: try ast.add_package(state.builder, span, declarations.items)
try refresh(state)
bindings :: try mem.alloc(sema.Binding, state.allocator, state.bindings.items.len)
memcopy!(bindings, state.bindings.items)
sema.replace_bindings(state.resolution, bindings)
tree :: ast.view(state.builder, root)
diagnostic_store := diagnostics.init(state.allocator)
defer diagnostics.deinit(&diagnostic_store)
# Resolution's existing diagnostic IDs remain valid in this temporary store.
for (state.diagnostics.list.items) |diagnostic| try arraylist.append(&diagnostic_store.list, diagnostic)
original_diagnostics :: diagnostics.count(&diagnostic_store)
checked := try typecheck.check(state.allocator, &tree, state.resolution, &diagnostic_store)
defer typecheck.deinit(&checked)
if (diagnostics.count(&diagnostic_store) != original_diagnostics) return Value{}
module := try lower.lower_evaluation(state.allocator, &tree, state.resolution, &checked, wrapper, &diagnostic_store)
defer ir.deinit(&module)
outcome := try interpreter.run_evaluation(state.allocator, &module, &diagnostic_store)
defer interpreter.deinit(&outcome)
if (outcome.trap) |trap| {
if (usize(trap) < original_diagnostics) state.evaluation_diagnostic = trap
else {
diagnostic :: diagnostics.get(&diagnostic_store, trap)?
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, diagnostic.code, diagnostic.span)
}
return Value{}
}
value :: outcome.value
if (ir.integer_bits(value.type) != 0) {
negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0
magnitude :: if (negative) u64(-(value.data.integer + 1)) + 1 else value.data.unsigned
return Value{ kind = .integer, node = call, magnitude = magnitude, negative = negative, annotation = result_type }
}
if (value.type == .bool) return Value{
kind = .boolean, node = call, boolean = value.data.boolean, annotation = result_type,
}
if (value.type == .string) {
literal :: try ast.add_string(state.builder, span, ir.string_bytes(&module, value.data.string))
try refresh(state)
return Value{ kind = .string, node = literal, annotation = result_type }
}
return Value{}
}
@hide:file @hide:file
return_type_value proc(state @mut State, block ast.NodeId) ast.NodeId ! Error { return_type_value proc(state @mut State, block ast.NodeId) ast.NodeId ! Error {
count :: ast.block_statement_count(&state.tree, block) count :: ast.block_statement_count(&state.tree, block)
@@ -616,6 +756,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
return value return value
} }
.binary: return try evaluate_binary(state, id) .binary: return try evaluate_binary(state, id)
.call: return try evaluate_call(state, id)
else: return Value{} else: return Value{}
} }
} }
@@ -624,6 +765,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error { evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error {
view :: ast.binary_expr(&state.tree, id) view :: ast.binary_expr(&state.tree, id)
left :: try evaluate(state, view.left) left :: try evaluate(state, view.left)
if (left.kind == .invalid) return Value{}
if (left.kind == .boolean) { if (left.kind == .boolean) {
if (view.operator == .logical_and and !left.boolean) return Value{ kind = .boolean, node = id, boolean = false } if (view.operator == .logical_and and !left.boolean) return Value{ kind = .boolean, node = id, boolean = false }
if (view.operator == .logical_or and left.boolean) return Value{ kind = .boolean, node = id, boolean = true } if (view.operator == .logical_or and left.boolean) return Value{ kind = .boolean, node = id, boolean = true }
+153 -2
View File
@@ -11,6 +11,131 @@ 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 = {
"comptime_calls_share_runtime_inference_loops_and_recursion": (
"""
length proc() usize {
result := 0
while (result < 2) result = result + 1
return result
}
factorial proc(count usize) usize {
if (count == 0) return 1
return count * factorial(count - 1)
}
sum proc(count usize) usize {
result := 0
for (0..count) |index| {
if (index == 1) continue
if (index == 4) break
result = result + index
}
return result
}
identity proc($T type, value T) T { return value }
Fixed proc($N usize) type { return struct { values [N]i16 } }
main proc() i16 {
first Fixed(identity(usize, length())) :: { values = [40, 2] }
same Fixed(factorial(3) - sum(6) + 1) :: first
return same.values[0] + same.values[1]
}
""", b"42\n", None,
),
"comptime_call_string_result_owns_escaped_bytes": (
r"""
choose proc(take bool, text string) string {
if (take) return text
return "unused"
}
offset proc($text string, value i16) i16 { return value + i16(text.len) }
main proc() i16 { return offset(choose(true, "a\"\\\x00é"), 36) }
""", b"42\n", None,
),
"comptime_calls_preserve_checked_narrow_intermediates": (
"""
overflow proc(seed i8) i8 {
result := seed
result = result + 1
return result - 1
}
answer proc($N i8) i16 { return 42 }
main proc() i16 { return answer(overflow(127)) }
""", b"", b"integer arithmetic overflow",
),
"comptime_calls_preserve_unsigned_and_signed_boundaries": (
"""
unsigned proc(value u64) u64 { return value }
signed proc(value i64) i64 { return value }
answer proc($high u64, $low i64) i16 {
if (high == 18446744073709551615 and low == -9223372036854775808) return 42
return 0
}
main proc() i16 {
return answer(unsigned(18446744073709551615), signed(-9223372036854775808))
}
""", b"42\n", None,
),
"comptime_call_cannot_promote_runtime_parameter": (
"""
identity proc(value usize) usize { return value }
answer proc($N usize) i16 { return 42 }
runtime proc(value usize) i16 { return answer(identity(value)) }
main proc() i16 { return runtime(2) }
""", b"", b"generic argument must be a compile-time value",
),
"comptime_call_cannot_read_mutable_global": (
"""
state usize := 2
read proc() usize { return state }
answer proc($N usize) i16 { return 42 }
main proc() i16 { return answer(read()) }
""", b"", b"generic argument must be a compile-time value",
),
"comptime_call_cannot_mutate_runtime_storage": (
"""
state usize := 0
write proc() usize {
state = 2
return state
}
answer proc($N usize) i16 { return 42 }
main proc() i16 { return answer(write()) }
""", b"", b"generic argument must be a compile-time value",
),
"comptime_call_keeps_uninitialized_local_traps": (
"""
premature proc() usize {
before :: after
after :: 2
return before
}
answer proc($N usize) i16 { return 42 }
main proc() i16 { return answer(premature()) }
""", b"", b"local value was read before initialization",
),
"comptime_call_bounds_empty_loops": (
"""
forever proc() usize {
while true {}
return 2
}
answer proc($N usize) i16 { return 42 }
main proc() i16 { return answer(forever()) }
""", b"", b"compile-time execution exceeds",
),
"comptime_call_bounds_recursive_frames": (
"""
forever proc(value usize) usize { return forever(value) }
answer proc($N usize) i16 { return 42 }
main proc() i16 { return answer(forever(2)) }
""", b"", b"compile-time execution exceeds",
),
"comptime_call_foreign_execution_remains_a_source_trap": (
"""
getpid c_func() c_int
answer proc($N c_int) i16 { return 42 }
main proc() i16 { return answer(getpid()) }
""", b"", b"foreign function calls are unsupported",
),
"specialization_parameterless_type_producer_is_cached": ( "specialization_parameterless_type_producer_is_cached": (
""" """
Token proc() type { return struct { value i16 } } Token proc() type { return struct { value i16 } }
@@ -2570,6 +2695,11 @@ def check_specialization_packages(temporary, executable):
first proc($T type, $N usize, value @Fixed(T, N)) T { first proc($T type, $N usize, value @Fixed(T, N)) T {
return value.values[0] return value.values[0]
} }
count proc() usize {
result := 0
while (result < 2) result = result + 1
return result
}
""").strip() + "\n") """).strip() + "\n")
source = app / "main.hon" source = app / "main.hon"
source.write_text(textwrap.dedent(""" source.write_text(textwrap.dedent("""
@@ -2579,7 +2709,7 @@ def check_specialization_packages(temporary, executable):
main proc() i16 { main proc() i16 {
imported ImportedBox :: { value = 40 } imported ImportedBox :: { value = 40 }
copy lib.Box(i16) :: imported copy lib.Box(i16) :: imported
fixed lib.Fixed(i16, 2) :: { values = [2, 0] } fixed lib.Fixed(i16, lib.count()) :: { values = [2, 0] }
return copy.value + lib.identity(lib.first(&fixed)) return copy.value + lib.identity(lib.first(&fixed))
} }
""").strip() + "\n") """).strip() + "\n")
@@ -2594,7 +2724,28 @@ def check_specialization_packages(temporary, executable):
} }
""").strip() + "\n") """).strip() + "\n")
check(app, executable, (b"", b"incompatible value type")) check(app, executable, (b"", b"incompatible value type"))
return 2 sizing = library / "sizing.hon"
sizing.write_text(textwrap.dedent("""
size proc() usize {
high usize :: 18446744073709551615
return high + 1
}
""").strip() + "\n")
source.write_text(textwrap.dedent("""
lib :: import "../library"
main proc() i16 {
if (false) {
fixed lib.Fixed(i16, lib.size()) :: { values = [40, 2] }
return fixed.values[0]
}
return 42
}
""").strip() + "\n")
check(app, executable, (b"42\n", b"integer arithmetic overflow"))
interpreted = run(str(HONEY), "run", str(app))
assert interpreted.stderr.startswith(f"{sizing.resolve()}:3:12: ".encode()), outcome(interpreted)
assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted)
return 3
def check_dependency_provenance(temporary, executable): def check_dependency_provenance(temporary, executable):