feat: support mixed runtime and metadata execution

This commit is contained in:
2026-11-01 20:45:36 +01:00
parent 22b40d3718
commit 2a4efd0b7f
4 changed files with 695 additions and 15 deletions
+12 -1
View File
@@ -134,7 +134,18 @@ procedures returning fixed arrays or records/tuples containing `type`, such as `
ordinary parameters containing `type` also make a procedure compile-time-only, even when its result is a runtime value. for example, `count proc(types []type) usize { return types.len }` computes `count([i16, bool])` without runtime metadata. every input must be explicitly supplied and constant; use `$` parameters when later signature annotations depend on them. helpers can return owned scalar, aggregate and string values, recurse over metadata and use checked `try`/`catch` with scoped cleanup. plain and fallible `void` support bare returns and successful fallthrough. ordinary runtime dependencies and incomplete metadata arguments remain diagnostic; mutable metadata borrowing and general type-valued execution remain open.
local metadata storage also makes a procedure compile-time-only, even with ordinary scalar inputs and results. explicit annotations (`T type := i16`) and inferred mutable type bindings (`T := i16`), metadata array/record/tuple literals, helper results, checked projections and yielding blocks use owned snapshots and erased calls. nested storage, partial initialization, checked updates, loops, recursion and scoped cleanup reuse type-control execution and its shared 4096-step bound. inferred arrays retain readonly elements unless their type explicitly permits element updates; whole-value replacement remains available for mutable bindings. every input remains explicitly supplied and constant; this is not partial evaluation of runtime inputs. ordinary local `alias` declarations and scalar projections from metadata helpers keep their runtime-call contract. mixed runtime/type-valued execution and mutable metadata borrowing remain open.
local metadata storage uses owned compile-time snapshots alongside ordinary runtime parameters and effects. explicit annotations (`T type := i16`), inferred mutable type bindings (`T := i16`), metadata arrays/records/tuples, helper results and checked projections are erased before runtime checking. sequential updates, saved copies, static branches, bounded construction loops and scoped metadata cleanup select concrete runtime storage without emitting type values. runtime branches and loops may create independent local metadata; they cannot conditionally update an enclosing compile-time snapshot. runtime optional/collection captures and ordinary effects keep their execution semantics.
```hon
compute proc(value i16) i16 {
T := i8
T = i16
local T :: value
return local
}
```
procedures whose metadata depends on ordinary parameters retain fully constant-input execution; every input to those calls remains explicitly supplied and constant. metadata parameters/results also retain their existing compile-time-only contract. nested storage, partial initialization, checked updates, recursion and fallible cleanup reuse type-control execution and its shared 4096-step bound. inferred arrays retain readonly elements unless their annotation permits updates; mutable bindings still permit whole-value replacement. ordinary local aliases and scalar metadata projections keep their runtime-call contract. mutable metadata borrowing remains open.
readonly type-valued slices (`[]type` and slices of metadata records/arrays) also cross compile-time procedure boundaries as owned snapshots. contextual array literals, including `[]`, construct them; explicit views of immutable readonly arrays and existing metadata slices support checked `usize` bounds, empty end views, indexing and `.len`. nested slices, ordinary/inline by-value iteration, erased `$` inputs and optional/fallible channels retain exact nominal leaves and content-based specialization identity. snapshots generate runtime types/defaults, but metadata itself has no runtime storage. mutable slice types, sentinels and views of mutable array places remain rejected rather than silently losing borrowing semantics; copy an array into an immutable binding before taking an owned view. construction shares the type-control step bound, and uninitialized leaves cannot cross procedure boundaries.
+7 -6
View File
@@ -58,8 +58,8 @@ checked immutable-array/slice views, by-value iteration and erased arguments;
mutable metadata borrowing remains open.
ordinary metadata-input procedures also compute owned runtime values, including
reflected field names/counts, recursive helpers and fallible cleanup.
explicit and inferred local metadata storage also make ordinary procedures
constant-input producers, preserving owned updates, loops, recursion and cleanup.
explicit and inferred local metadata storage also coexist with ordinary runtime
parameters and effects; parameter-dependent metadata retains constant-input execution.
`field!` selects record/tuple/enum and sequence members using compile-time names,
including runtime places and type-control updates without runtime reflection.
`struct_type!` generates ordinary/C-layout records from constant name/type/default
@@ -71,7 +71,7 @@ with static tags and exact by-value payloads, preserving runtime dispatch and sc
`typeinfo!` exposes owned compile-time array/distinct/record/enum metadata, including
readonly field collections, exact nominal types and reflected generated storage/defaults.
the existing `meta.EnumFieldStruct` and `enummap.EnumMap`, `init` and `get` work;
general type-valued execution, broader materialization and full library acceptance remain open.
mutable metadata borrowing, broader materialization and full library acceptance remain open.
`some!` preserves nested optional presence; ordinary procedures initialize concrete
runtime `undefined` locals and compute fully initialized materializable constants.
the existing `ArrayList` uses nested generic inference, file-local helpers and
@@ -180,10 +180,11 @@ test discovery and FFI remain outside these completed slices.
- [x] readonly type-slice checkpoint: `[]type` and slices of type-valued records/arrays use owned compile-time snapshots. contextual literals, immutable readonly array views, nested slice views, checked `usize` bounds/indexing, `.len`, empty shapes and ordinary/inline by-value iteration preserve exact nominal leaves. constant-input procedure parameters/results, erased generic inputs, optional/fallible nesting and normalized cache keys generate runtime storage/defaults through interpreter/native execution. mutable annotations/backing, metadata sentinels, invalid ranges/indices, readonly writes, incomplete leaves and runtime dependencies retain diagnostics. imported private types, source/session disposal, shared construction bounds and repeat compilation preserve ownership. mutable metadata borrowing, runtime metadata storage, general type-valued execution and broader constant materialization remain open.
- [x] tagged type-storage checkpoint: tagged unions containing type leaves own active payload snapshots through typed/contextual construction, direct and nested field/index updates, immutable by-value matches and whole-value variant replacement. constant-input procedure arguments/results, erased generic inputs and normalized cache keys preserve nominal leaves and saved copies while generating runtime storage/defaults through interpreter/native execution. undefined union locals and nested record/array storage require whole-value initialization; inactive reads/writes, malformed explicit tags, nominal initializer conflicts, readonly updates, incomplete results and narrow overflow retain diagnostics. explicit sparse tag enums may contain unused members without adding match cases. imported private types, source/session disposal and repeat compilation preserve ownership. untagged metadata layouts remain invalid; mutable metadata borrowing, runtime metadata storage, general type-valued execution and broader constant materialization remain open.
- [x] metadata-input execution checkpoint: procedures with ordinary type-valued parameters execute entirely at compile time even when returning runtime-representable values. all inputs remain explicit constants; owned scalar/record/array/string results, recursive readonly metadata iteration, reflected field names/counts and private nominal leaves work through interpreter/native execution. plain/fallible `void` retain bare-return/fallthrough semantics; ordinary fallible results propagate through `try`/`catch` and run scoped `defer`/`errdefer`. runtime dependencies and incomplete payloads retain diagnostics; source/session disposal and repeat compilation preserve ownership. dependent signature parameters still require `$`; general type-valued execution, mutable metadata borrowing and broader constant materialization remain open.
- [x] explicit metadata-local execution checkpoint: procedures with explicitly annotated type-valued locals execute entirely at compile time without metadata inputs or results. scalar inputs remain explicit constants; nested metadata records/arrays, undefined initialization, owned copies, checked field/index updates, loops, captures, recursion and scoped fallible cleanup reuse existing type-control execution through interpreter/native paths. runtime inputs and uninitialized leaves diagnose; ordinary unannotated local type aliases retain runtime parameters. imported private nominal leaves, source/session disposal and repeat compilation preserve ownership. inferred metadata-only locals are covered below; mixed runtime/type-valued execution, mutable metadata borrowing and broader constant materialization remain open.
- [x] inferred metadata-local execution checkpoint: inferred mutable type bindings and metadata array/record/tuple locals select fully constant-input execution without metadata inputs, results or local annotations. literal/helper results, checked field/index/optional projections, yielding blocks and conditional/fallible selection preserve owned copies, nominal leaves, whole-value replacement, loops, recursion and scoped cleanup through interpreter/native execution. inferred array elements remain readonly; runtime inputs diagnose. ordinary local aliases and scalar metadata projections retain runtime parameters. ordinary fallible success/error snapshots use checked channel discrimination rather than assuming every payload carries a nominal type. imported private types, source/session disposal and repeat compilation preserve ownership. erased compile-time-only procedures cannot become runtime entries. mixed runtime/type-valued execution, mutable metadata borrowing and broader materialization remain open.
- [x] explicit metadata-local execution checkpoint: procedures with explicitly annotated type-valued locals support constant-input execution without metadata inputs or results. nested metadata records/arrays, undefined initialization, owned copies, checked field/index updates, loops, captures, recursion and scoped fallible cleanup reuse type-control execution through interpreter/native paths. uninitialized leaves and runtime dependencies diagnose; imported private nominal leaves, source/session disposal and repeat compilation preserve ownership. independent runtime parameters are supported by the mixed-execution checkpoint below.
- [x] inferred metadata-local execution checkpoint: inferred mutable type bindings and metadata array/record/tuple locals support constant-input execution without metadata inputs, results or local annotations. literal/helper results, checked field/index/optional projections, yielding blocks and conditional/fallible selection preserve owned copies, nominal leaves, whole-value replacement, loops, recursion and scoped cleanup through interpreter/native execution. inferred array elements remain readonly; ordinary local aliases and scalar metadata projections retain runtime parameters. ordinary fallible success/error snapshots use checked channel discrimination rather than assuming every payload carries a nominal type. imported private types, source/session disposal and repeat compilation preserve ownership. independent runtime parameters and metadata-local entry points are supported below.
- [x] formatted-IO acceptance checkpoint: existing `std/io.print` parses constant formats and expands tuple fields through interpreter/native execution without replacement or adapted library code. escaped braces, all integer bases/boundaries, booleans, strings/arrays, sparse/aliased enums, nested scalar distinct values, runtime arguments and computed formats retain exact output; short writes, zero progress, invalid callback counts and write errors retain checked propagation. malformed formats, wrong tuple/count/type inputs and unsupported specifiers diagnose. native `f64` default/decimal/scientific formatting uses imported variadic `snprintf` at `-O0`/`-O2`, while interpreter FFI remains a demanded trap. character literals preserve Unicode code points and byte escapes; newline-separated conditional branches and package-local visibility match Bro. partial compile-time prefixes infer remaining arguments without overriding explicit values; pure type builtins and effect-free bound type helpers specialize guards. `rem!` preserves exact numeric types, signed-minimum remainder and checked zero divisors. immutable C byte-pointer coercions, borrowed pointer-to-array slices and optional presence retain mutability/nominal checks. inline iterables propagate constant demand through original parser calls, metadata indexing and borrowed initialization; guarded errors follow reached control flow, owned snapshots commit local mutation, contextual constant numeric locals preserve rigid boundaries, and runtime bodies/effects remain runtime. general type-valued execution, broader materialization and broader `meta` acceptance remain open.
- [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including address-bearing untagged unions, nonnull data pointers and mutable slices), plus remaining existing-library acceptance for broader `meta` operations. broader imported C-binding support may interleave with later gates.
- [x] mixed metadata-local execution checkpoint: ordinary and generic procedures retain runtime scalar/pointer inputs and effects alongside erased explicit/inferred type, array and record locals. owned snapshots preserve sequential field/index/whole updates, saved copies, generated runtime storage, constant branches, bounded construction loops, static exits and scoped metadata cleanup. runtime branches/loops create independent metadata locals; optional/collection captures keep their runtime bindings. constant tagged/optional metadata selection specializes only the selected body. runtime-dependent metadata still requires fully constant-input execution, and runtime paths cannot mutate enclosing metadata snapshots. interpreter/native `-O0`/`-O2`, imported private nominal leaves, source/session disposal and repeat compilation retain ownership and diagnostics. mutable metadata borrowing and broader constant materialization remain open.
- [ ] remaining gate: mutable metadata borrowing, broader constant materialization (including address-bearing untagged unions, nonnull data pointers and mutable slices), plus remaining existing-library acceptance for broader `meta` operations. broader imported C-binding support may interleave with later gates.
- [ ] remaining source constructs: multiline strings. boolean and optional-capture conditional expressions, yielding value blocks, `yield`, compound updates and bitwise operators are covered above. 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.
+462 -3
View File
@@ -485,6 +485,13 @@ expand_expected proc(state @mut State, id, expected ast.NodeId) void ! Error {
if (class == .function) {
state.return_type = ast.function_decl(&state.tree, id).return_type
state.yield_type = no_node
if (!ast.function_external(&state.tree, id)
and type_local_storage(&state.tree, state.bindings.items,
ast.function_decl(&state.tree, id).body, 0)
and !compiletime_call(state, id)) {
try expand_runtime_metadata(state, id)
return
}
}
if (class == .statement) {
match ast.stmt_kind(&state.tree, id) {
@@ -2318,16 +2325,167 @@ type_local_storage proc(tree @ast.Tree, bindings []sema.Binding, id ast.NodeId,
return false
}
# local metadata does not make unrelated parameters constant. retain constant-input
# execution only when a parameter participates in constructing or updating metadata.
@hide:file
metadata_local proc(state @State, id ast.NodeId) bool {
if (ast.node_class(&state.tree, id) != .statement
or ast.stmt_kind(&state.tree, id) != .declaration) return false
declaration :: ast.statement_decl(&state.tree, id)
if (declaration.type) |annotation| return type_record_storage(state, annotation, 0)
return type_storage_initializer(&state.tree, state.bindings.items, declaration.initializer,
declaration.mutability == .variable, 0)
}
@hide:file
assignment_root proc(state @State, id ast.NodeId) ast.NodeId {
kind :: ast.expr_kind(&state.tree, id)
if (kind == .selection) return assignment_root(state, ast.selection_expr(&state.tree, id).base)
if (kind == .index) return assignment_root(state, ast.index_expr(&state.tree, id).base)
if (kind == .builtin_call
and mem.eql(ast.literal_text(&state.tree, ast.builtin_call(&state.tree, id).name), "field")
and ast.call_arg_count(&state.tree, id) == 2)
return assignment_root(state, ast.call_arg(&state.tree, id, 0))
resolved :: binding(state, id)
return if (resolved.kind == .declaration) resolved.data.declaration else no_node
}
@hide:file
DependencyPath :: struct { declaration ast.NodeId, parent ?@DependencyPath }
@hide:file
parameter_dependency proc(state @State, id, body ast.NodeId, path ?@DependencyPath, depth usize) bool {
if (depth >= 128) return true
resolved :: binding(state, id)
if (resolved.kind == .declaration) {
declaration :: resolved.data.declaration
class :: ast.node_class(&state.tree, declaration)
if (class == .parameter and ast.param(&state.tree, declaration).type != no_node)
return !ast.param(&state.tree, declaration).comptime
if (class == .statement or class == .expression or class == .parameter) {
current := path
while (current) |entry| {
if (entry.declaration == declaration) return false
current = entry.parent
}
entry :: DependencyPath{ declaration = declaration, parent = path }
if (class == .statement
and parameter_dependency(state, ast.statement_decl(&state.tree, declaration).initializer,
body, &entry, depth + 1)) return true
if (class == .statement and ast.statement_decl(&state.tree, declaration).mutability == .constant)
return false
return declaration_parameter_dependency(state, body, declaration, body, &entry, false, depth + 1)
}
}
for (0..ast.child_count(&state.tree, id)) |i|
if (parameter_dependency(state, ast.child(&state.tree, id, i), body, path, depth + 1)) return true
return false
}
@hide:file
subtree_has proc(state @State, root, node ast.NodeId) bool {
if (root == node) return true
for (0..ast.child_count(&state.tree, root)) |i|
if (subtree_has(state, ast.child(&state.tree, root, i), node)) return true
return false
}
# follow writes as well as initializers; the declaration path breaks assignment cycles.
@hide:file
declaration_parameter_dependency proc(
state @State, id, declaration, body ast.NodeId, path @DependencyPath, dynamic bool, depth usize,
) bool {
if (depth >= 128) return true
selected := dynamic
class :: ast.node_class(&state.tree, id)
if (class == .statement) {
kind :: ast.stmt_kind(&state.tree, id)
if (kind == .assignment_stmt) {
assignment :: ast.assignment_stmt(&state.tree, id)
if (assignment_root(state, assignment.target) == declaration)
return (dynamic or parameter_dependency(state, assignment.target, body, path, depth + 1)
or parameter_dependency(state, assignment.value, body, path, depth + 1))
}
if (kind == .if_stmt or kind == .while_stmt or kind == .for_stmt) {
condition := no_node
captured := false
if (kind == .if_stmt) {
view :: ast.if_stmt(&state.tree, id)
condition = view.condition
captured = view.capture != null and view.capture? == declaration
} else if (kind == .while_stmt) {
view :: ast.while_stmt(&state.tree, id)
condition = view.condition
captured = view.capture != null and view.capture? == declaration
} else {
view :: ast.for_stmt(&state.tree, id)
condition = view.iterable
captured = ((view.capture != null and view.capture? == declaration)
or (view.index != null and view.index? == declaration))
}
if (captured) return parameter_dependency(state, condition, body, path, depth + 1)
if (!subtree_has(state, id, declaration))
selected = dynamic or parameter_dependency(state, condition, body, path, depth + 1)
}
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .match_expr) {
subject :: ast.match_expr(&state.tree, id).subject
for (0..ast.match_arm_count(&state.tree, id)) |i| {
arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, id, i))
if (arm.capture != null and arm.capture? == declaration)
return parameter_dependency(state, subject, body, path, depth + 1)
}
if (!subtree_has(state, id, declaration))
selected = dynamic or parameter_dependency(state, subject, body, path, depth + 1)
}
for (0..ast.child_count(&state.tree, id)) |i|
if (declaration_parameter_dependency(state, ast.child(&state.tree, id, i),
declaration, body, path, selected, depth + 1)) return true
return false
}
@hide:file
metadata_parameter_dependency proc(state @State, id, body, dynamic ast.NodeId) bool {
class :: ast.node_class(&state.tree, id)
if (metadata_local(state, id) and parameter_dependency(state, id, body, null, 0)) return true
if (class == .statement) {
kind :: ast.stmt_kind(&state.tree, id)
if (kind == .assignment_stmt) {
assignment :: ast.assignment_stmt(&state.tree, id)
root :: assignment_root(state, assignment.target)
if (root != no_node and metadata_local(state, root))
return ((dynamic != no_node and !subtree_has(state, dynamic, root))
or parameter_dependency(state, assignment.target, body, null, 0)
or parameter_dependency(state, assignment.value, body, null, 0))
}
if (kind == .if_stmt or kind == .while_stmt or kind == .for_stmt) {
condition :: if (kind == .if_stmt) ast.if_stmt(&state.tree, id).condition
else if (kind == .while_stmt) ast.while_stmt(&state.tree, id).condition
else ast.for_stmt(&state.tree, id).iterable
selected :: if (parameter_dependency(state, condition, body, null, 0)) id else dynamic
for (0..ast.child_count(&state.tree, id)) |i|
if (metadata_parameter_dependency(state, ast.child(&state.tree, id, i), body, selected)) return true
return false
}
}
for (0..ast.child_count(&state.tree, id)) |i|
if (metadata_parameter_dependency(state, ast.child(&state.tree, id, i), body, dynamic)) return true
return false
}
@hide:file
compiletime_call proc(state @State, function ast.NodeId) bool {
if (type_record_storage(state, ast.function_decl(&state.tree, function).return_type, 0)) return true
runtime_input := false
for (0..ast.function_param_count(&state.tree, function)) |i| {
parameter :: ast.param(&state.tree, ast.function_param(&state.tree, function, i))
if (!parameter.comptime and type_record_storage(state, parameter.type, 0)) return true
if (!parameter.comptime) runtime_input = true
}
return (!ast.function_external(&state.tree, function)
and type_local_storage(&state.tree, state.bindings.items,
ast.function_decl(&state.tree, function).body, 0))
if (!runtime_input or ast.function_external(&state.tree, function)) return false
body :: ast.function_decl(&state.tree, function).body
if (!type_local_storage(&state.tree, state.bindings.items, body, 0)) return false
return metadata_parameter_dependency(state, body, body, no_node)
}
@hide:file
@@ -3495,11 +3653,273 @@ TypeExecution :: struct {
loop_depth usize = 0
inline_body ast.NodeId = no_node
inline_blocks ?@mut arraylist.ArrayList(ast.NodeId) = null
residual ?@mut arraylist.ArrayList(ast.NodeId) = null
residual_boundary usize = 0
residual_body ast.NodeId = no_node
residual_flow TypeFlow = .next
}
@hide:file
TypeFlow :: enum { next, returned, yielded, break, continue, invalid }
@hide:file
ResidualCleanup :: struct { position usize, statement ast.NodeId }
@hide:file
metadata_reads proc(state @State, id, declaration ast.NodeId, depth usize) bool {
if (depth >= 128) return false
resolved :: binding(state, id)
if (resolved.kind == .declaration) {
if (resolved.data.declaration == declaration) return true
if (ast.node_class(&state.tree, resolved.data.declaration) == .statement)
return metadata_reads(state, ast.statement_decl(&state.tree, resolved.data.declaration).initializer,
declaration, depth + 1)
}
for (0..ast.child_count(&state.tree, id)) |i|
if (metadata_reads(state, ast.child(&state.tree, id, i), declaration, depth + 1)) return true
return false
}
@hide:file
metadata_updates_outer proc(state @State, id, scope ast.NodeId) bool {
if (ast.node_class(&state.tree, id) == .statement and ast.stmt_kind(&state.tree, id) == .assignment_stmt) {
root :: assignment_root(state, ast.assignment_stmt(&state.tree, id).target)
if (root != no_node and metadata_local(state, root) and !subtree_has(state, scope, root)) return true
}
for (0..ast.child_count(&state.tree, id)) |i|
if (metadata_updates_outer(state, ast.child(&state.tree, id, i), scope)) return true
return false
}
@hide:file
metadata_updates proc(state @State, id ast.NodeId) bool {
return metadata_updates_outer(state, id, id)
}
@hide:file
metadata_needs proc(state @State, id, declaration ast.NodeId) bool {
if (metadata_local(state, id) and metadata_reads(state, id, declaration, 0)) return true
if (ast.node_class(&state.tree, id) == .statement) {
kind :: ast.stmt_kind(&state.tree, id)
if (kind == .assignment_stmt) {
root :: assignment_root(state, ast.assignment_stmt(&state.tree, id).target)
if (root != no_node and metadata_local(state, root)
and metadata_reads(state, id, declaration, 0)) return true
}
if ((kind == .while_stmt or kind == .for_stmt or kind == .if_stmt) and metadata_updates(state, id)) {
condition :: if (kind == .while_stmt) ast.while_stmt(&state.tree, id).condition
else if (kind == .for_stmt) ast.for_stmt(&state.tree, id).iterable
else ast.if_stmt(&state.tree, id).condition
if (metadata_reads(state, condition, declaration, 0)) return true
}
}
for (0..ast.child_count(&state.tree, id)) |i|
if (metadata_needs(state, ast.child(&state.tree, id, i), declaration)) return true
return false
}
# erase metadata storage before ordinary checking. runtime scopes may create their
# own metadata, but cannot change a snapshot belonging to an enclosing runtime path.
@hide:file
residual_block proc(state @mut State, execution @mut TypeExecution, block ast.NodeId, boundary usize) ast.NodeId ! Error {
saved :: execution.locals.items.len
saved_output :: execution.residual
saved_boundary :: execution.residual_boundary
statements := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&statements)
cleanups := arraylist.init(ResidualCleanup, state.allocator)
defer arraylist.deinit(&cleanups)
execution.residual = &statements
execution.residual_boundary = boundary
execution.residual_flow = .next
defer {
execution.locals.items = execution.locals.items[..saved]
execution.residual = saved_output
execution.residual_boundary = saved_boundary
}
for (0..ast.block_statement_count(&state.tree, block)) |i| {
statement :: ast.block_statement(&state.tree, block, i)
if (!try type_step(state, statement)) return no_node
kind :: ast.stmt_kind(&state.tree, statement)
if ((kind == .break_stmt or kind == .continue_stmt) and execution.loop_depth > 0) {
execution.residual_flow = if (kind == .break_stmt) TypeFlow.break else TypeFlow.continue
break
}
if (kind == .defer_stmt) {
try arraylist.append(&cleanups, ResidualCleanup{ position = statements.items.len, statement = statement })
try arraylist.append(&statements, statement)
continue
}
constant := metadata_local(state, statement)
if (kind == .declaration and !constant) {
declaration :: ast.statement_decl(&state.tree, statement)
constant = (type_storage_initializer(&state.tree, state.bindings.items,
declaration.initializer, true, 0)
or metadata_needs(state, execution.residual_body, statement))
}
if (kind == .assignment_stmt) {
root :: assignment_root(state, ast.assignment_stmt(&state.tree, statement).target)
for (execution.locals.items) |local, index| {
if (root != local.declaration) continue
if (index < boundary) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.comptime_argument_required, ast.node_span(&state.tree, statement))
return no_node
}
constant = true
}
}
if (constant) {
saved_values :: execution.evaluate_values
execution.evaluate_values = true
flow :: execute_type_statement(state, execution, statement) catch |err| {
execution.evaluate_values = saved_values
return err
}
execution.evaluate_values = saved_values
if (flow != .next) return no_node
continue
}
if (kind == .block_stmt) {
child :: try residual_block(state, execution, ast.statement_block(&state.tree, statement), boundary)
if (child == no_node) return no_node
try arraylist.append(&statements, try ast.add_block_stmt(state.builder, child))
try refresh(state)
if (execution.residual_flow != .next) break
continue
}
if (kind == .expression
and ast.expr_kind(&state.tree, ast.statement_expr(&state.tree, statement)) == .match_expr) {
expression :: ast.statement_expr(&state.tree, statement)
subject :: try type_snapshot(state, execution, ast.match_expr(&state.tree, expression).subject)
if (subject == no_node) return no_node
try expand(state, subject)
if (type_record_storage(state, try expression_type(state, subject, 0), 0)
or branch_constant(state, subject, 0)) {
arms := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&arms)
for (0..ast.match_arm_count(&state.tree, expression)) |position|
try arraylist.append(&arms, ast.match_arm(&state.tree, expression, position))
dispatch :: try ast.add_match(state.builder, ast.node_span(&state.tree, expression), subject, arms.items)
try refresh(state)
try expand_inline_match(state, dispatch, try expression_type(state, subject, 0))
index :: try type_match_arm(state, dispatch)
if (index == no_index) return no_node
arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, dispatch, index))
selected :: try type_match_body(state, dispatch, arm)
if (selected == no_node) return no_node
if (arm.expression) {
snapshot :: try type_snapshot(state, execution, selected)
if (snapshot == no_node) return no_node
try arraylist.append(&statements, try ast.add_expression_stmt(state.builder, snapshot))
} else {
body :: try residual_block(state, execution, selected, boundary)
if (body == no_node) return no_node
value :: try ast.add_block_expr(state.builder, ast.node_span(&state.tree, expression), body)
try arraylist.append(&statements, try ast.add_expression_stmt(state.builder, value))
if (execution.residual_flow == .yielded) execution.residual_flow = .next
}
try refresh(state)
if (execution.residual_flow != .next) break
continue
}
}
if (kind == .if_stmt and ast.if_stmt(&state.tree, statement).capture != null) {
condition :: try type_snapshot(state, execution, ast.if_stmt(&state.tree, statement).condition)
if (condition == no_node) return no_node
try expand(state, condition)
if (type_record_storage(state, try expression_type(state, condition, 0), 0)) {
flow :: try execute_type_statement(state, execution, statement)
if (flow == .invalid) return no_node
execution.residual_flow = flow
if (flow != .next) break
continue
}
}
if (kind == .if_stmt and ast.if_stmt(&state.tree, statement).capture == null) {
condition :: try type_snapshot(state, execution, ast.if_stmt(&state.tree, statement).condition)
if (condition == no_node) return no_node
try expand(state, condition)
if (branch_constant(state, condition, 0)) {
value :: try evaluate_ir(state, condition, try builtin(state, condition, .bool))
if (value.kind != .boolean) return no_node
view :: ast.if_stmt(&state.tree, statement)
selected := if (value.boolean) view.then_block else no_node
if (!value.boolean and view.else_block != null) selected = view.else_block?
if (selected != no_node) {
child :: try residual_block(state, execution, selected, boundary)
if (child == no_node) return no_node
try arraylist.append(&statements, try ast.add_block_stmt(state.builder, child))
try refresh(state)
if (execution.residual_flow != .next) break
}
continue
}
}
if ((kind == .for_stmt or kind == .while_stmt) and metadata_updates(state, statement)) {
flow :: try execute_type_statement(state, execution, statement)
if (flow == .invalid) return no_node
execution.residual_flow = flow
if (flow != .next) break
continue
}
snapshot :: try type_snapshot(state, execution, statement)
if (snapshot == no_node) return no_node
try arraylist.append(&statements, snapshot)
if (kind == .return_stmt or kind == .yield_stmt) {
execution.residual_flow = if (kind == .return_stmt) TypeFlow.returned else TypeFlow.yielded
break
}
}
remaining := cleanups.items.len
while (remaining > 0) {
remaining -= 1
cleanup :: cleanups.items[remaining]
saved_flow :: execution.residual_flow
body :: try residual_block(state, execution, ast.cleanup_stmt(&state.tree, cleanup.statement).body, boundary)
if (body == no_node) return no_node
statements.items[cleanup.position] = try ast.add_cleanup_stmt(state.builder,
ast.node_span(&state.tree, cleanup.statement), body, null, false)
try refresh(state)
execution.residual_flow = saved_flow
}
result :: try ast.add_block(state.builder, ast.node_span(&state.tree, block), statements.items)
try refresh(state)
return result
}
@hide:file
expand_runtime_metadata proc(state @mut State, function ast.NodeId) void ! Error {
view :: ast.function_decl(&state.tree, function)
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
state.evaluation_diagnostic = diagnostics.no_diagnostic
execution := TypeExecution{
locals = arraylist.init(state.allocator),
mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)),
first_owned = ast.NodeId(0), result_type = view.return_type, residual_body = view.body,
}
defer arraylist.deinit(&execution.locals)
defer mem.free(state.allocator, execution.mapping)
mem.set(execution.mapping, no_node)
body := try residual_block(state, &execution, view.body, 0)
if (body == no_node) {
diagnostic := state.evaluation_diagnostic
if (diagnostic == diagnostics.no_diagnostic)
diagnostic = try diagnostics.add(state.diagnostics, .comptime_argument_required, view.span)
invalid :: try ast.add_invalid_stmt(state.builder, view.span, diagnostic)
statements [1]ast.NodeId :: [invalid]
body = try ast.add_block(state.builder, view.span, statements[..])
}
try replace(state, view.body, body)
try refresh(state)
try expand(state, view.return_type)
for (0..ast.function_param_count(&state.tree, function)) |i|
try expand(state, ast.function_param(&state.tree, function, i))
try expand(state, view.body)
}
# type values cannot enter runtime IR. execute their control flow here, substituting
# owned constants into snapshots; ordinary value operations still use checked IR.
@hide:file
@@ -3599,6 +4019,37 @@ type_step proc(state @mut State, at ast.NodeId) bool ! Error {
@hide:file
prepare_type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) void ! Error {
if (id == no_node or usize(id) >= execution.mapping.len or execution.pending_flow != .next) return
resolved_capture :: binding(state, id)
if (execution.residual != null and !execution.evaluate_values
and (ast.node_class(&state.tree, id) == .expression or ast.node_class(&state.tree, id) == .parameter)
and resolved_capture.kind == .declaration and resolved_capture.data.declaration == id) return
if (execution.residual != null and !execution.evaluate_values) {
# captures precede their bodies semantically, regardless of AST child order.
for (0..ast.child_count(&state.tree, id)) |i| {
child :: ast.child(&state.tree, id, i)
resolved :: binding(state, child)
if ((ast.node_class(&state.tree, child) != .expression and ast.node_class(&state.tree, child) != .parameter)
or resolved.kind != .declaration or resolved.data.declaration != child) continue
capture :: try ast.copy_node(state.builder, child)
try refresh(state)
execution.mapping[usize(child)] = capture
state.bindings.items[usize(capture)] = sema.Binding{
kind = .declaration, data = sema.BindingData{ declaration = capture },
}
}
}
if (execution.residual != null and !execution.evaluate_values and ast.node_class(&state.tree, id) == .block) {
saved_flow :: execution.residual_flow
saved_loop :: execution.loop_depth
execution.loop_depth = 0
defer {
execution.residual_flow = saved_flow
execution.loop_depth = saved_loop
}
execution.mapping[usize(id)] = try residual_block(state, execution, id, execution.locals.items.len)
if (execution.mapping[usize(id)] == no_node) execution.pending_flow = .invalid
return
}
# substituted arguments belong to the caller, including nominal type identity.
if (id < execution.first_owned) {
execution.mapping[usize(id)] = id
@@ -5822,6 +6273,14 @@ type_borrow_call proc(state @mut State, execution @mut TypeExecution, call ast.N
@hide:file
execute_type_block proc(state @mut State, execution @mut TypeExecution, block ast.NodeId) TypeFlow ! Error {
if (execution.residual != null and !execution.evaluate_values) {
block_value :: try residual_block(state, execution, block, execution.residual_boundary)
if (block_value == no_node) return .invalid
statement :: try ast.add_block_stmt(state.builder, block_value)
try refresh(state)
try arraylist.append(execution.residual?, statement)
return execution.residual_flow
}
if (block == execution.inline_body) {
saved_demand :: state.demand_depth
state.demand_depth = 0
+214 -5
View File
@@ -170,14 +170,223 @@ CASES = {
}
""", b"[65, 233, 128578, 10, 13, 9, 92, 39]\n", None,
),
"compiletime_only_main_cannot_select_another_runtime_function": (
"mixed_metadata_locals_preserve_snapshots_and_runtime_effects": (
"""
Config :: struct { child type, count usize }
compute proc($T type, effects @mut i16, value T) T {
Explicit type := i8
Explicit = T
Inferred := bool
Inferred = Explicit
types [2]mut type := [i8, bool]
saved_types :: types
if (T == i16) { types[0] = Inferred } else { types[0] = i16 }
inferred_types := [i8, bool]
saved_inferred_types :: inferred_types
inferred_types = [types[0], bool]
config Config := { child = i8, count = 1 }
saved_config :: config
config.child = inferred_types[0]
config.count += 1
inferred_config := Config { child = bool, count = 1 }
saved_inferred_config :: inferred_config
inferred_config.child = config.child
inferred_config.count = config.count
if (saved_types[0] != i8 or saved_inferred_types[0] != i8) return T(-1)
if (saved_config.child != i8 or saved_config.count != 1) return T(-2)
if (saved_inferred_config.child != bool or saved_inferred_config.count != 1) return T(-3)
Element :: inferred_config.child
values [inferred_config.count]Element :: [value, T(2)]
effects^ += 1
return values[0] + values[1]
}
forward proc(effects @mut i16, value i16) i16 { return compute(i16, effects, value) }
main proc() [2]i16 {
effects := i16(0)
first :: forward(&effects, 18)
second :: forward(&effects, 20)
return [first + second, effects]
}
""", b"[42, 2]\n", None,
),
"mixed_runtime_branches_and_loops_keep_metadata_locals_independent": (
"""
Config :: struct { child type, count usize }
compute proc(value i16, repeats usize, take bool) i16 {
total := value
if (take) {
Element := i8
Element = i16
amount Element :: 2
total += amount
} else {
config Config := { child = i8, count = 1 }
config.child = i16
amount config.child :: 4
total += amount
}
index := usize(0)
while (index < repeats) {
config := Config { child = i8, count = 1 }
saved :: config
config.child = i16
config.count += 1
if (saved.child != i8 or saved.count != 1) return -1
values [config.count]config.child :: [total, 1]
total = values[0] + values[1]
index += 1
}
return total
}
forward proc(value i16, repeats usize, take bool) i16 { return compute(value, repeats, take) }
main proc() [2]i16 { return [forward(38, 2, true), forward(36, 2, false)] }
""", b"[42, 42]\n", None,
),
"mixed_metadata_construction_static_exits_and_cleanup": (
"""
Config :: struct { child type, count usize }
compute proc(value i16, effects @mut i16) i16 {
config Config := { child = i8, count = 0 }
types [2]mut type := undefined
for (0..usize(3)) |index| {
if (index == 2) break
types[index] = i16
config.count += 1
effects^ += 1
}
{
defer config.child = types[0]
config.child = bool
}
selected ?type :: types[1]
if selected |Child| {
if (Child != i16) return -1
amount Child :: 2
values [config.count]config.child :: [value, amount]
return values[0] + values[1]
}
return -2
}
main proc() [2]i16 {
value := i16(40)
effects := i16(0)
result :: compute(value, &effects)
return [result, effects]
}
""", b"[42, 2]\n", None,
),
"mixed_runtime_captures_and_tagged_metadata_selection": (
"""
Choice :: union(enum) { child type, empty void }
compute proc(value ?i16, values []i16) i16 {
T := i16
choice Choice :: { child = T }
match choice {
.child |Child|: { if (Child != i16) return -1 },
.empty: return -2,
}
total := i16(0)
if value |captured| {
U := T
local U :: captured
total += local
}
for values |captured, index| {
U := T
local U :: captured
total += local + i16(index)
}
return total
}
main proc() i16 {
values [2]i16 :: [10, 10]
return compute(21, values[..])
}
""", b"42\n", None,
),
"metadata_input_dependencies_follow_mutable_scalar_updates": (
"""
compute proc(count usize) usize {
size := usize(0)
size = count
config struct { child type, count usize } := { child = i16, count = size }
return config.count
}
main proc() usize { return compute(42) }
""", b"42\n", None,
),
"metadata_scalar_update_dependencies_still_require_constant_inputs": (
"""
compute proc(count usize) usize {
size := usize(0)
size = count
config struct { child type, count usize } := { child = i16, count = size }
return config.count
}
forward proc(count usize) usize { return compute(count) }
main proc() usize { return forward(42) }
""", b"", b"compile-time value",
),
"mixed_metadata_locals_reject_runtime_type_initializers": (
"""
compute proc(wide bool) i16 {
Element := if (wide) i16 else i8
value Element :: 42
return i16(value)
}
forward proc(wide bool) i16 { return compute(wide) }
main proc() i16 { return forward(true) }
""", b"", b"compile-time value",
),
"mixed_metadata_locals_reject_runtime_record_updates": (
"""
Config :: struct { child type, count usize }
compute proc(wide bool) i16 {
config Config := { child = i8, count = 1 }
config.child = if (wide) i16 else i8
value config.child :: 42
return i16(value)
}
forward proc(wide bool) i16 { return compute(wide) }
main proc() i16 { return forward(true) }
""", b"", b"compile-time value",
),
"mixed_runtime_branches_reject_outer_type_mutation": (
"""
compute proc(wide bool) i16 {
Element := i8
if (wide) { Element = i16 }
value Element :: 42
return i16(value)
}
forward proc(wide bool) i16 { return compute(wide) }
main proc() i16 { return forward(true) }
""", b"", b"compile-time value",
),
"mixed_runtime_loops_reject_outer_metadata_mutation": (
"""
Config :: struct { child type, count usize }
compute proc(repeats usize) usize {
config := Config { child = i16, count = 0 }
index := usize(0)
while (index < repeats) {
config.count += 1
index += 1
}
return config.count
}
forward proc(repeats usize) usize { return compute(repeats) }
main proc() usize { return forward(42) }
""", b"", b"compile-time value",
),
"metadata_only_main_keeps_runtime_entrypoint": (
"""
fallback proc() i16 { return -99 }
main proc() i16 {
types := [i16, bool]
return 42
}
""", b"", b"main must be a runtime function with no parameters",
""", b"42\n", None,
),
"inferred_metadata_locals_preserve_owned_copies_and_updates": (
"""
@@ -296,7 +505,7 @@ CASES = {
}
runtime proc(value usize) usize { return count(value) }
main proc() usize { return runtime(42) }
""", b"", b"generic argument must be a compile-time value",
""", b"", b"compile-time value",
),
"inferred_metadata_arrays_keep_readonly_elements": (
"""
@@ -369,7 +578,7 @@ CASES = {
}
""", b"42\n", None,
),
"explicit_metadata_locals_require_constant_runtime_inputs": (
"explicit_metadata_locals_allow_independent_runtime_inputs": (
"""
count proc(value usize) usize {
T type :: i16
@@ -377,7 +586,7 @@ CASES = {
}
runtime proc(value usize) usize { return count(value) }
main proc() usize { return runtime(42) }
""", b"", b"generic argument must be a compile-time value",
""", b"42\n", None,
),
"explicit_metadata_locals_reject_incomplete_results": (
"""