fix: propagate contextual compile-time execution

This commit is contained in:
2026-11-01 18:31:50 +01:00
parent 1f457d6b41
commit 22b40d3718
6 changed files with 836 additions and 46 deletions
+513 -20
View File
@@ -51,6 +51,7 @@ Instance :: struct {
diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic
signature ast.NodeId = no_node
value ast.NodeId = no_node
demanded bool = false
}
@hide:file
@@ -102,6 +103,11 @@ State :: struct {
value_expansion_depth usize = 0
return_type ast.NodeId = no_node
yield_type ast.NodeId = no_node
demand_depth usize = 0
snapshot_uninitialized bool = false
demanded_functions []mut u8
execution_functions []mut u8
execution_visited arraylist.ArrayList(ast.NodeId)
}
#! elaborates templates into cached concrete declarations in the caller-owned `builder` arena.
@@ -160,6 +166,12 @@ elaborate proc(
if (!needed) return result
mapping :: try mem.alloc(ast.NodeId, allocator, ast.node_count(&original))
defer mem.free(allocator, mapping)
demanded_functions :: try mem.alloc(u8, allocator, ast.node_count(&original))
defer mem.free(allocator, demanded_functions)
mem.set(demanded_functions, 0)
execution_functions :: try mem.alloc(u8, allocator, ast.node_count(&original))
defer mem.free(allocator, execution_functions)
mem.set(execution_functions, 0)
state := State{
allocator = allocator, builder = builder, tree = ast.view(builder, root), strings = strings,
bindings = arraylist.init(allocator), declarations = arraylist.init(allocator),
@@ -170,21 +182,27 @@ elaborate proc(
rewrites = &result,
mapping = mapping,
resolution = resolution,
demanded_functions = demanded_functions,
execution_functions = execution_functions,
execution_visited = arraylist.init(allocator),
}
defer arraylist.deinit(&state.bindings)
defer arraylist.deinit(&state.declarations)
defer arraylist.deinit(&state.expanded)
defer hashmap.deinit(&state.cache)
defer hashmap.deinit(&state.capture_types)
defer arraylist.deinit(&state.execution_visited)
defer {
for (state.instances.items) |instance| mem.free(allocator, instance.arguments)
arraylist.deinit(&state.instances)
}
count :: ast.node_count(&state.tree)
for (0..count) |i| try arraylist.append(&state.bindings, sema.binding(resolution, ast.NodeId(i)))
try collect_demanded_functions(&state, root, false)
for (0..ast.package_decl_count(&state.tree)) |i| {
id :: ast.package_decl(&state.tree, i)
if (ast.decl_kind(&state.tree, id) == .function and template(&state, id)) continue
if (ast.decl_kind(&state.tree, id) == .function
and (template(&state, id) or state.demanded_functions[usize(id)] == 1)) continue
try arraylist.append(&state.declarations, id)
}
index := 0
@@ -277,6 +295,166 @@ expand_compile_error proc(state @mut State, id ast.NodeId) void ! Error {
try reject_evaluation(state, id)
}
# ordinary constant calls retain one interpreter frame and its aliases. only
# bodies whose type-control operations need value-dependent execution use snapshots.
@hide:file
metadata_capture proc(state @State, id, capture ast.NodeId) bool {
if (ast.node_class(&state.tree, id) == .expression and ast.expr_kind(&state.tree, id) == .match_expr) {
subject :: ast.match_expr(&state.tree, id).subject
if (ast.expr_kind(&state.tree, subject) == .builtin_call
and mem.eql(ast.literal_text(&state.tree, ast.builtin_call(&state.tree, subject).name), "typeinfo")) {
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? == capture) return true
}
}
}
for (0..ast.child_count(&state.tree, id)) |i|
if (metadata_capture(state, ast.child(&state.tree, id, i), capture)) return true
return false
}
@hide:file
runtime_dependency proc(state @State, id, body ast.NodeId, 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) return !ast.param(&state.tree, declaration).comptime
if (class == .statement) {
view :: ast.statement_decl(&state.tree, declaration)
return view.mutability == .variable or runtime_dependency(state, view.initializer, body, depth + 1)
}
if (class == .expression) return !metadata_capture(state, body, declaration)
}
for (0..ast.child_count(&state.tree, id)) |i|
if (runtime_dependency(state, ast.child(&state.tree, id, i), body, depth + 1)) return true
return false
}
@hide:file
scan_demand_execution proc(state @mut State, id, body ast.NodeId, static_control bool) bool ! mem.AllocError {
class :: ast.node_class(&state.tree, id)
if (static_control and class == .statement and ast.stmt_kind(&state.tree, id) == .if_stmt) {
conditional :: ast.if_stmt(&state.tree, id)
if (try scan_demand_execution(state, conditional.condition, body, true)) return true
selected_statically :: !runtime_dependency(state, conditional.condition, body, 0)
if (try scan_demand_execution(state, conditional.then_block, body, selected_statically)) return true
if (conditional.else_block) |other|
if (try scan_demand_execution(state, other, body, selected_statically)) return true
return false
}
if (class == .expression) {
kind :: ast.expr_kind(&state.tree, id)
if (!static_control and kind == .builtin_call
and mem.eql(ast.literal_text(&state.tree, ast.builtin_call(&state.tree, id).name), "compile_error")) return true
if (kind == .match_expr) {
subject :: ast.match_expr(&state.tree, id).subject
if (ast.expr_kind(&state.tree, subject) == .builtin_call
and mem.eql(ast.literal_text(&state.tree, ast.builtin_call(&state.tree, subject).name), "typeinfo")) {
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 (try scan_demand_execution(state, arm.body, body, true)) return true
}
return false
}
}
if (kind == .index) {
view :: ast.index_expr(&state.tree, id)
root := view.base
while (ast.expr_kind(&state.tree, root) == .selection)
root = ast.selection_expr(&state.tree, root).base
resolved :: binding(state, root)
if (resolved.kind == .declaration and runtime_dependency(state, view.index, body, 0)
and metadata_capture(state, body, resolved.data.declaration)) return true
}
if (kind == .call) {
callee :: binding(state, ast.call_expr(&state.tree, id).callee)
if (callee.kind == .declaration and usize(callee.data.declaration) < state.original_count
and ast.node_class(&state.tree, callee.data.declaration) == .function
and !ast.function_external(&state.tree, callee.data.declaration)
and !compiletime_call(state, callee.data.declaration)) {
function :: callee.data.declaration
flag :: state.execution_functions[usize(function)]
if (flag == 2) return true
if (flag == 0) {
try arraylist.append(&state.execution_visited, function)
state.execution_functions[usize(function)] = 3
called_body :: ast.function_decl(&state.tree, function).body
if (try scan_demand_execution(state, called_body, called_body, false)) return true
}
}
}
}
for (0..ast.child_count(&state.tree, id)) |i|
if (try scan_demand_execution(state, ast.child(&state.tree, id, i), body, static_control)) return true
return false
}
@hide:file
needs_demand_execution proc(state @mut State, function ast.NodeId) bool ! mem.AllocError {
if (usize(function) >= state.original_count) return false
flag :: state.execution_functions[usize(function)]
if (flag == 1) return false
if (flag == 2) return true
defer {
for (state.execution_visited.items) |visited| {
if (state.execution_functions[usize(visited)] == 3) state.execution_functions[usize(visited)] = 0
}
state.execution_visited.items = state.execution_visited.items[..0]
}
try arraylist.append(&state.execution_visited, function)
state.execution_functions[usize(function)] = 3
body :: ast.function_decl(&state.tree, function).body
required :: try scan_demand_execution(state, body, body, false)
if (!required) {
for (state.execution_visited.items) |visited| state.execution_functions[usize(visited)] = 1
} else state.execution_functions[usize(function)] = 2
return required
}
# find demanded callees before walking runtime declarations, independent of source
# order. a later runtime reference retains the original declaration as usual.
@hide:file
collect_demanded_functions proc(state @mut State, id ast.NodeId, demanded bool) void ! mem.AllocError {
class :: ast.node_class(&state.tree, id)
demand := demanded
if (class == .statement and ast.stmt_kind(&state.tree, id) == .for_stmt
and ast.for_stmt(&state.tree, id).is_inline) {
loop :: ast.for_stmt(&state.tree, id)
try collect_demanded_functions(state, loop.iterable, true)
try collect_demanded_functions(state, loop.body, demanded)
return
}
if (class == .expression) {
kind :: ast.expr_kind(&state.tree, id)
if (kind == .comptime_expr) demand = true
if (kind == .call) {
callee :: binding(state, ast.call_expr(&state.tree, id).callee)
if (callee.kind == .declaration
and ast.node_class(&state.tree, callee.data.declaration) == .function) {
function :: callee.data.declaration
if (demand and !ast.function_external(&state.tree, function)
and state.demanded_functions[usize(function)] == 0
and try needs_demand_execution(state, function)) {
state.demanded_functions[usize(function)] = 1
try collect_demanded_functions(state, ast.function_decl(&state.tree, function).body, true)
}
if (ast.call_arg_count(&state.tree, id) == ast.function_param_count(&state.tree, function)) {
for (0..ast.call_arg_count(&state.tree, id)) |i| {
parameter :: ast.param(&state.tree, ast.function_param(&state.tree, function, i))
try collect_demanded_functions(state, ast.call_arg(&state.tree, id, i), demand or parameter.comptime)
}
return
}
}
}
}
for (0..ast.child_count(&state.tree, id)) |i|
try collect_demanded_functions(state, ast.child(&state.tree, id, i), demand)
}
@hide:file
expand proc(state @mut State, id ast.NodeId) void ! Error {
try expand_expected(state, id, no_node)
@@ -285,6 +463,16 @@ expand proc(state @mut State, id ast.NodeId) void ! Error {
@hide:file
expand_expected proc(state @mut State, id, expected ast.NodeId) void ! Error {
try refresh(state)
if (state.demand_depth == 0) {
resolved :: binding(state, id)
if (resolved.kind == .declaration and usize(resolved.data.declaration) < state.original_count) {
declaration :: resolved.data.declaration
if (state.demanded_functions[usize(declaration)] == 1 and !template(state, declaration)) {
state.demanded_functions[usize(declaration)] = 2
try arraylist.append(&state.declarations, declaration)
}
}
}
if (state.expanded.items[usize(id)] != 0) return
state.expanded.items[usize(id)] = 1
class :: ast.node_class(&state.tree, id)
@@ -508,6 +696,8 @@ expand_expected proc(state @mut State, id, expected ast.NodeId) void ! Error {
}
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .comptime_expr) {
state.demand_depth += 1
defer state.demand_depth -= 1
operand :: ast.comptime_operand(&state.tree, id)
try expand_expected(state, operand, expected)
state.evaluation_diagnostic = diagnostics.no_diagnostic
@@ -567,7 +757,10 @@ expand_expected proc(state @mut State, id, expected ast.NodeId) void ! Error {
if (
resolved.kind == .declaration
and ast.node_class(&state.tree, resolved.data.declaration) == .function
and template(state, resolved.data.declaration)
and (template(state, resolved.data.declaration)
or (state.demand_depth > 0 and usize(resolved.data.declaration) < state.original_count
and !ast.function_external(&state.tree, resolved.data.declaration)
and try needs_demand_execution(state, resolved.data.declaration)))
) {
try specialize_call(state, id, resolved.data.declaration, expected)
return
@@ -1512,9 +1705,11 @@ expand_inline_match proc(state @mut State, id, subject_type ast.NodeId) void ! E
return
}
subject :: ast.match_expr(&state.tree, id).subject
try expand(state, subject)
annotation := subject_type
if (annotation == no_node) annotation = try expression_type(state, subject, 0)
if (annotation == no_node) {
try expand(state, subject)
annotation = try expression_type(state, subject, 0)
}
if (annotation == no_node) {
try reject(state, id, .invalid_inline_match_subject)
return
@@ -1737,8 +1932,8 @@ specialize_match proc(state @mut State, id, statement, expected ast.NodeId) bool
return true
}
# only probe side-effect-free expressions. ordinary calls stay runtime unless the
# source requests evaluation with $(); probing must not execute FFI or emit traps.
# implicit probing is limited to constants and effect-free, fully bound type
# helpers. ordinary runtime calls still require an explicit evaluation request.
@hide:file
branch_constant proc(state @State, id ast.NodeId, depth usize) bool {
if (depth >= 128) return false
@@ -1762,6 +1957,8 @@ branch_constant proc(state @State, id ast.NodeId, depth usize) bool {
.union_constant, .slice_constant: return true
.typed_constant: return branch_constant(state, ast.typed_constant(&state.tree, id).value, depth + 1)
.enum_literal: return true
.call: return branch_type_call(state, id, depth + 1)
.block_expr: return branch_body_constant(state, ast.block_expr(&state.tree, id), depth + 1)
.builtin_call: {
name :: ast.literal_text(&state.tree, ast.builtin_call(&state.tree, id).name)
if (ast.call_arg_count(&state.tree, id) != 1
@@ -1811,6 +2008,68 @@ branch_constant proc(state @State, id ast.NodeId, depth usize) bool {
}
}
@hide:file
branch_type_call proc(state @State, id ast.NodeId, depth usize) bool {
if (depth >= 128 or ast.call_arg_count(&state.tree, id) != 0) return false
callee :: binding(state, ast.call_expr(&state.tree, id).callee)
if (callee.kind != .declaration) return false
function :: callee.data.declaration
if (ast.node_class(&state.tree, function) != .function
or ast.function_external(&state.tree, function)
or ast.function_param_count(&state.tree, function) != 0) return false
for (state.instances.items) |instance| {
if (instance.declaration != function) continue
for (0..ast.function_param_count(&state.tree, instance.function)) |i| {
parameter :: ast.param(&state.tree, ast.function_param(&state.tree, instance.function, i))
if (parameter.comptime and is_type(state, parameter.type))
return branch_body_constant(state, ast.function_decl(&state.tree, function).body, depth + 1)
}
return false
}
return false
}
@hide:file
branch_body_constant proc(state @State, id ast.NodeId, depth usize) bool {
if (depth >= 128) return false
if (ast.node_class(&state.tree, id) == .block) {
for (0..ast.block_statement_count(&state.tree, id)) |i| {
if (!branch_body_constant(state, ast.block_statement(&state.tree, id, i), depth + 1)) return false
}
return true
}
if (ast.node_class(&state.tree, id) != .statement) return false
match ast.stmt_kind(&state.tree, id) {
.return_stmt, .yield_stmt: {
value :: if (ast.stmt_kind(&state.tree, id) == .yield_stmt)
ast.yield_stmt(&state.tree, id).value else ast.return_stmt(&state.tree, id).value
return value == null or branch_constant(state, value?, depth + 1)
}
.block_stmt: return branch_body_constant(state, ast.statement_block(&state.tree, id), depth + 1)
.expression: return branch_constant(state, ast.statement_expr(&state.tree, id), depth + 1)
.declaration: {
declaration :: ast.statement_decl(&state.tree, id)
initializer :: ast.expr_kind(&state.tree, declaration.initializer)
if (declaration.type != null and (initializer == .record_literal or initializer == .array_literal)) return false
return (
declaration.mutability == .constant
and branch_constant(state, declaration.initializer, depth + 1)
)
}
.if_stmt: {
conditional :: ast.if_stmt(&state.tree, id)
return (
conditional.capture == null
and branch_constant(state, conditional.condition, depth + 1)
and branch_body_constant(state, conditional.then_block, depth + 1)
and (conditional.else_block == null
or branch_body_constant(state, conditional.else_block?, depth + 1))
)
}
else: return false
}
}
@hide:file
builtin proc(state @mut State, at ast.NodeId, kind sema.BuiltinType) ast.NodeId ! Error {
id :: try ast.add_identifier(state.builder, ast.node_span(&state.tree, at), strpool.no_str)
@@ -2121,15 +2380,17 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void
state.depth += 1
defer state.depth -= 1
count :: ast.function_param_count(&state.tree, function)
# metadata inputs, results and locals require a fully constant call.
type_result :: compiletime_call(state, function)
# demand is scoped to explicit evaluation, never inferred from constant arguments.
metadata_result :: compiletime_call(state, function)
demanded :: !metadata_result and state.demand_depth > 0 and try needs_demand_execution(state, function)
type_result :: metadata_result or demanded
runtime := usize(0)
for (0..count) |i| {
if (!type_result and !ast.param(&state.tree, ast.function_param(&state.tree, function, i)).comptime) runtime += 1
if (!metadata_result and !ast.param(&state.tree, ast.function_param(&state.tree, function, i)).comptime) runtime += 1
}
supplied :: ast.call_arg_count(&state.tree, call)
explicit :: supplied == count
if (!explicit and (type_result or supplied < runtime or supplied > count)) {
if (!explicit and (metadata_result or supplied < runtime or supplied > count)) {
try reject(state, call, .argument_count_mismatch)
return
}
@@ -2139,13 +2400,13 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void
runtime_arguments := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&runtime_arguments)
# supplied compile-time arguments form a declaration-order prefix; infer the rest.
supplied_comptime :: if (type_result) count else supplied - runtime
supplied_comptime :: if (metadata_result) count else supplied - runtime
comptime_position := usize(0)
position := 0
for (0..count) |i| {
parameter :: ast.function_param(&state.tree, function, i)
view :: ast.param(&state.tree, parameter)
if (view.comptime and !type_result) {
if (view.comptime and !metadata_result) {
if (comptime_position >= supplied_comptime) continue
comptime_position += 1
}
@@ -2172,9 +2433,8 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void
try reject(state, call, .type_mismatch)
return
}
} else {
try arraylist.append(&runtime_arguments, argument)
}
if (!view.comptime and !metadata_result) try arraylist.append(&runtime_arguments, argument)
}
if (!explicit) {
# rigid arguments constrain first; result context then types nested generic
@@ -2230,7 +2490,8 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void
annotation :: try argument_annotation(state, function, view.type, arguments)
try expand_expected(state, argument, annotation)
}
key := usize(function)
demand_key :: if (demanded) usize(1) else usize(0)
key := mix(usize(function), demand_key)
for arguments |argument| {
if (argument.kind != .invalid) key = mix(key, try value_hash(state, argument))
}
@@ -2240,7 +2501,7 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void
instance := no_index
while (current != no_index) {
entry :: state.instances.items[current]
equal := entry.function == function and entry.arguments.len == count
equal := entry.function == function and entry.arguments.len == count and entry.demanded == demanded
if (equal) {
for (0..count) |i| {
if (!try value_equal(state, entry.arguments[i], arguments[i])) {
@@ -2262,7 +2523,9 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void
values :: try mem.alloc(Value, state.allocator, count)
memcopy!(values, arguments)
instance = state.instances.items.len
arraylist.append(&state.instances, Instance{ function = function, arguments = values, next = head }) catch |err| {
arraylist.append(&state.instances, Instance{
function = function, arguments = values, next = head, demanded = demanded,
}) catch |err| {
mem.free(state.allocator, values)
return err
}
@@ -2308,7 +2571,7 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void
for (0..count) |i| {
parameter :: ast.function_param(&state.tree, signature, i)
view :: ast.param(&state.tree, parameter)
if (view.comptime and !type_result) {
if (view.comptime and !metadata_result) {
if (comptime_position >= supplied_comptime) continue
comptime_position += 1
}
@@ -2337,7 +2600,7 @@ specialize_call proc(state @mut State, call, function, expected ast.NodeId) void
@hide:file
instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error {
function :: state.instances.items[index].function
type_result :: compiletime_call(state, function)
type_result :: compiletime_call(state, function) or state.instances.items[index].demanded
count :: state.original_count
clone_start :: ast.node_count(&state.tree)
mapping :: state.mapping
@@ -2394,6 +2657,8 @@ instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error {
}
}
if (type_result) {
state.demand_depth += 1
defer state.demand_depth -= 1
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
defer state.type_execution_depth -= 1
@@ -2586,6 +2851,8 @@ argument_annotation proc(
@hide:file
evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Error {
state.demand_depth += 1
defer state.demand_depth -= 1
try expand_expected(state, id, annotation)
if (!is_type(state, annotation) and type_record_storage(state, annotation, 0)) {
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
@@ -2752,6 +3019,7 @@ serialize_value proc(
uninitialized = ir.aggregate_item(module, value, 0).uninitialized
}
if (uninitialized) {
if (state.snapshot_uninitialized) return try ast.add_literal(state.builder, .undefined, span)
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span)
return no_node
}
@@ -3203,6 +3471,9 @@ TypeLocal :: struct {
declaration ast.NodeId
value ast.NodeId
annotation ast.NodeId
# unannotated numeric literal origins stay contextual while executing constants;
# annotations, casts and rigid assignment sources retain ordinary exact types.
contextual bool = false
}
@hide:file
@@ -3338,7 +3609,10 @@ prepare_type_snapshot proc(state @mut State, execution @mut TypeExecution, id as
if (resolved.kind == .declaration) {
for (execution.locals.items) |local| {
if (local.declaration == resolved.data.declaration) {
execution.mapping[usize(id)] = local.value
value := local.value
if (local.contextual and ast.expr_kind(&state.tree, value) == .typed_constant)
value = ast.typed_constant(&state.tree, value).value
execution.mapping[usize(id)] = value
return
}
}
@@ -3356,6 +3630,11 @@ prepare_type_snapshot proc(state @mut State, execution @mut TypeExecution, id as
}
if (execution.evaluate_values and ast.node_class(&state.tree, id) == .expression) {
kind :: ast.expr_kind(&state.tree, id)
if (kind == .call and type_call_borrows(state, execution, id)) {
execution.mapping[usize(id)] = try type_borrow_call(state, execution, id)
if (execution.mapping[usize(id)] == no_node) execution.pending_flow = .invalid
return
}
if (kind == .slice) {
base :: ast.slice_expr(&state.tree, id).base
root := base
@@ -3727,6 +4006,9 @@ type_snapshot_try proc(state @mut State, execution @mut TypeExecution, id ast.No
type_dispatch_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error {
subject :: try type_snapshot(state, execution, ast.match_expr(&state.tree, id).subject)
if (subject == no_node) return no_node
subject_type :: try expression_type(state, subject, 0)
try expand_inline_match(state, id, subject_type)
if (ast.expr_kind(&state.tree, id) != .match_expr) return no_node
saved :: execution.evaluate_values
execution.evaluate_values = false
defer execution.evaluate_values = saved
@@ -3886,6 +4168,8 @@ inline_block proc(state @mut State, block ast.NodeId) TypeFlow ! Error {
@hide:file
expand_inline_for proc(state @mut State, id ast.NodeId) void ! Error {
state.demand_depth += 1
defer state.demand_depth -= 1
loop :: ast.for_stmt(&state.tree, id)
if (state.type_execution_depth == 0) state.type_execution_remaining = 4096
state.type_execution_depth += 1
@@ -5250,6 +5534,13 @@ type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.No
return concrete.node
}
kind :: ast.expr_kind(&state.tree, expression)
result_type :: binding(state, try canonical_type(state, annotation))
if (result_type.kind == .builtin and result_type.data.builtin == .void) {
if (kind == .literal_void) return expression
value :: try evaluate_ir(state, expression, annotation)
if (value.kind == .invalid) return no_node
return (try materialize(state, value)).node
}
if (kind == .literal_string or kind == .typed_constant) {
value :: try evaluate(state, expression)
if (
@@ -5347,9 +5638,194 @@ type_assignment_value proc(
return owned
}
# borrowed places live only in one checked interpreter invocation. its result owns
# snapshots of the changed roots; neither slice pointers nor interpreter cells escape.
@hide:file
type_borrowed_place proc(state @State, id, declaration ast.NodeId, borrowed bool) bool {
resolved :: binding(state, id)
if (borrowed and resolved.kind == .declaration and resolved.data.declaration == declaration) return true
borrowing := borrowed
if (ast.node_class(&state.tree, id) == .expression) {
kind :: ast.expr_kind(&state.tree, id)
borrowing = borrowing or kind == .slice
if (kind == .unary) borrowing = borrowing or ast.unary_expr(&state.tree, id).operator == .address_of
}
for (0..ast.child_count(&state.tree, id)) |i|
if (type_borrowed_place(state, ast.child(&state.tree, id, i), declaration, borrowing)) return true
return false
}
@hide:file
type_call_borrows proc(state @State, execution @TypeExecution, call ast.NodeId) bool {
for (execution.locals.items) |local| {
if (type_borrowed_place(state, call, local.declaration, false)) return true
}
return false
}
@hide:file
TypeBorrow :: struct { index usize, previous ast.NodeId, temporary ast.NodeId }
@hide:file
type_snapshot_uninitialized proc(state @State, id ast.NodeId) bool {
if (ast.expr_kind(&state.tree, id) == .literal_undefined) return true
if (ast.expr_kind(&state.tree, id) == .typed_constant)
return type_snapshot_uninitialized(state, ast.typed_constant(&state.tree, id).value)
if (ast.expr_kind(&state.tree, id) == .array_literal) {
for (0..ast.array_element_count(&state.tree, id)) |i|
if (type_snapshot_uninitialized(state, ast.array_element(&state.tree, id, i))) return true
}
if (ast.expr_kind(&state.tree, id) == .record_literal) {
for (0..ast.record_field_count(&state.tree, id)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i))
if (field.value != null and type_snapshot_uninitialized(state, field.value?)) return true
}
}
return false
}
# initialization state is not a value: recreate it as storage and write only
# initialized subobjects before entering the interpreter's borrowed call.
@hide:file
type_restore_snapshot proc(
state @mut State, target, input ast.NodeId, statements @mut arraylist.ArrayList(ast.NodeId),
) void ! Error {
value := input
if (ast.expr_kind(&state.tree, value) == .typed_constant) value = ast.typed_constant(&state.tree, value).value
kind :: ast.expr_kind(&state.tree, value)
if (kind == .literal_undefined) return
if (!type_snapshot_uninitialized(state, value)) {
assignment :: try ast.add_assignment_stmt(state.builder, ast.node_span(&state.tree, value), target, input)
try arraylist.append(statements, assignment)
try refresh(state)
return
}
count :: if (kind == .array_literal) ast.array_element_count(&state.tree, value)
else ast.record_field_count(&state.tree, value)
for (0..count) |i| {
child := no_node
place := no_node
span :: ast.node_span(&state.tree, value)
if (kind == .array_literal) {
child = ast.array_element(&state.tree, value, i)
index :: try ast.add_integer(state.builder, span, u64(i))
place = try ast.add_index(state.builder, span, target, index)
} else {
field :: ast.field(&state.tree, ast.record_field(&state.tree, value, i))
child = field.value?
member := no_node
if (field.name) |name| member = name
else member = try ast.add_integer(state.builder, span, u64(i))
place = try ast.add_selection(state.builder, span, target, member)
}
try refresh(state)
try type_restore_snapshot(state, place, child, statements)
}
}
@hide:file
type_borrow_call proc(state @mut State, execution @mut TypeExecution, call ast.NodeId) ast.NodeId ! Error {
saved_demand :: state.demand_depth
state.demand_depth = 0
defer state.demand_depth = saved_demand
span :: ast.node_span(&state.tree, call)
borrowed := arraylist.init(TypeBorrow, state.allocator)
defer arraylist.deinit(&borrowed)
statements := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&statements)
fields := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&fields)
values := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&values)
for (execution.locals.items) |local, i| {
if (!type_borrowed_place(state, call, local.declaration, false)) continue
if (type_record_storage(state, local.annotation, 0)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span)
return no_node
}
mutability :: if (ast.node_class(&state.tree, local.declaration) == .statement)
ast.statement_decl(&state.tree, local.declaration).mutability else ast.Mutability.constant
partial :: type_snapshot_uninitialized(state, local.value)
initializer := local.value
if (partial) initializer = try ast.add_literal(state.builder, .undefined, span)
temporary :: try ast.add_statement_decl(state.builder, span, span, strpool.no_str,
local.annotation, mutability, initializer)
try refresh(state)
try arraylist.append(&statements, temporary)
if (partial) try type_restore_snapshot(state, try reference(state, call, temporary), local.value, &statements)
try arraylist.append(&borrowed, TypeBorrow{ index = i, previous = local.value, temporary = temporary })
}
snapshot := no_node
{
saved_values :: execution.evaluate_values
execution.evaluate_values = false
defer execution.evaluate_values = saved_values
defer for (borrowed.items) |entry| execution.locals.items[entry.index].value = entry.previous
for (borrowed.items) |entry|
execution.locals.items[entry.index].value = try reference(state, call, entry.temporary)
snapshot = try type_snapshot(state, execution, call)
}
if (snapshot == no_node) return no_node
try expand(state, snapshot)
result_type :: try expression_type(state, snapshot, 0)
if (result_type == no_node) return no_node
resolved_result :: binding(state, try canonical_type(state, result_type))
void_result :: resolved_result.kind == .builtin and resolved_result.data.builtin == .void
result_offset :: if (void_result) usize(0) else usize(1)
if (void_result) {
discarded :: try ast.add_expression_stmt(state.builder, snapshot)
try arraylist.append(&statements, discarded)
} else {
result_local :: try ast.add_statement_decl(state.builder, span, span, strpool.no_str,
result_type, .constant, snapshot)
try refresh(state)
try arraylist.append(&statements, result_local)
result_field :: try ast.add_field(state.builder, span, null, result_type, null)
result_value :: try ast.add_field(state.builder, span, null, null, try reference(state, call, result_local))
try arraylist.append(&fields, result_field)
try arraylist.append(&values, result_value)
}
for (borrowed.items) |entry| {
field :: try ast.add_field(state.builder, span, null, execution.locals.items[entry.index].annotation, null)
value :: try ast.add_field(state.builder, span, null, null, try reference(state, call, entry.temporary))
try arraylist.append(&fields, field)
try arraylist.append(&values, value)
}
tuple :: try ast.add_record_type(state.builder, span, fields.items)
literal :: try ast.add_record_literal(state.builder, span, null, values.items)
returned :: try ast.add_return_stmt(state.builder, span, literal)
try arraylist.append(&statements, returned)
body :: try ast.add_block(state.builder, span, statements.items)
try refresh(state)
saved_uninitialized :: state.snapshot_uninitialized
state.snapshot_uninitialized = true
defer state.snapshot_uninitialized = saved_uninitialized
result :: try evaluate_ir_body(state, call, body, tuple)
if (result.kind != .aggregate) return no_node
for (borrowed.items) |entry, i| {
value :: ast.field(&state.tree, ast.record_field(&state.tree, result.node, i + result_offset)).value?
owned :: try ast.add_typed_constant(state.builder, span,
execution.locals.items[entry.index].annotation, value)
try refresh(state)
execution.locals.items[entry.index].value = owned
}
if (void_result) {
value :: try ast.add_literal(state.builder, .void, span)
try refresh(state)
return value
}
value :: ast.field(&state.tree, ast.record_field(&state.tree, result.node, 0)).value?
owned :: try ast.add_typed_constant(state.builder, span, result_type, value)
try refresh(state)
return owned
}
@hide:file
execute_type_block proc(state @mut State, execution @mut TypeExecution, block ast.NodeId) TypeFlow ! Error {
if (block == execution.inline_body) {
saved_demand :: state.demand_depth
state.demand_depth = 0
defer state.demand_depth = saved_demand
snapshot :: try type_snapshot(state, execution, block)
if (snapshot == no_node) return .invalid
flow :: try inline_block(state, snapshot)
@@ -5538,6 +6014,7 @@ execute_type_statement_body proc(state @mut State, execution @mut TypeExecution,
annotation = ast.typed_constant(&state.tree, value).type
try arraylist.append(&execution.locals, TypeLocal{
declaration = statement, value = value, annotation = annotation,
contextual = declaration.type == null and flexible(state, expression),
})
}
.assignment_stmt: {
@@ -5609,9 +6086,21 @@ execute_type_statement_body proc(state @mut State, execution @mut TypeExecution,
}
expression :: try type_snapshot_expected(state, execution,
assignment.value, execution.locals.items[index].annotation)
if (expression == no_node) return .invalid
if (execution.locals.items[index].contextual and !flexible(state, expression)) {
if (!try expand_type_matches(state, expression)) return .invalid
actual :: try expression_type(state, expression, 0)
resolved_type :: binding(state, actual)
numeric :: (try integer_type(state, actual)
or (resolved_type.kind == .builtin
and builtin_identity(resolved_type.data.builtin) == .float))
if (numeric) execution.locals.items[index].annotation = actual
}
value :: try type_execution_value(state, expression, execution.locals.items[index].annotation)
if (value == no_node) return .invalid
execution.locals.items[index].value = value
contextual :: execution.locals.items[index].contextual and flexible(state, expression)
execution.locals.items[index].contextual = contextual
}
.if_stmt, .while_stmt: {
looping :: ast.stmt_kind(&state.tree, statement) == .while_stmt
@@ -5962,6 +6451,10 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
try expand(state, id)
kind :: ast.expr_kind(&state.tree, id)
resolved :: binding(state, id)
if (resolved.kind == .invalid) {
state.evaluation_diagnostic = resolved.data.diagnostic
return Value{}
}
if (resolved.kind == .builtin and resolved.data.builtin != .type) return Value{ kind = .type, node = id }
if (resolved.kind == .declaration) {
target :: resolved.data.declaration
+254 -1
View File
@@ -11,6 +11,13 @@ import "@source/parser"
@hide:file
expect_specialization proc(
source []u8, expected diagnostics.Code, expected_count usize,
) void ! (mem.AllocError | diagnostics.CapacityError | strpool.InternError | lexer.SourceError | testing.Error) {
try expect_specialization_guards(source, expected, expected_count, maxval!(usize))
}
@hide:file
expect_specialization_guards proc(
source []u8, expected diagnostics.Code, expected_count, expected_guards usize,
) void ! (mem.AllocError | diagnostics.CapacityError | strpool.InternError | lexer.SourceError | testing.Error) {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
@@ -40,15 +47,261 @@ expect_specialization proc(
for (0..ast.function_param_count(&tree, declaration)) |j|
try testing.expect(!ast.param(&tree, ast.function_param(&tree, declaration, j)).comptime)
}
if (expected_guards != maxval!(usize))
try testing.expect_equal(expected_guards, runtime_guard_count(&tree, root))
} else {
count := usize(0)
for (diagnostic_store.list.items) |diagnostic| {
if (diagnostic.code == expected) count += 1
}
try testing.expect_equal(expected_count, count)
if (expected_count == maxval!(usize)) try testing.expect(count > 0)
else try testing.expect_equal(expected_count, count)
}
}
@hide:file
runtime_guard_count proc(tree @ast.Tree, id ast.NodeId) usize {
count := usize(0)
if (ast.node_class(tree, id) == .statement and ast.stmt_kind(tree, id) == .if_stmt) count += 1
for (0..ast.child_count(tree, id)) |i| count += runtime_guard_count(tree, ast.child(tree, id, i))
return count
}
selects_known_type_helper_guards_without_explicit_comptime test {
try expect_specialization_guards(
`Id :: distinct i16
`primitive proc($T type) bool { return T == i16 or T == Id }
`scalar proc($T type) bool { return primitive(T) }
`accept proc($T type, value T) T {
` if scalar(T) { return value }
` else { compile_error!("expected scalar") }
`}
`main proc() i16 { return accept(i16(42)) }
, .compile_error, 0, 0,
)
try expect_specialization(
`scalar proc($T type) bool { return T == i16 }
`accept proc($T type, value T) T {
` if scalar(T) { return value }
` else { compile_error!("expected scalar") }
`}
`main proc() bool { return accept(true) }
, .compile_error, 1,
)
}
selects_matched_type_helper_guards_before_expanding_invalid_arms test {
try expect_specialization_guards(
`scalar proc($T type) bool {
` match T == i16 {
` true: return true
` else: return false
` }
`}
`accept proc($T type, value T) T {
` if scalar(T) { return value }
` else { compile_error!("expected scalar") }
`}
`main proc() i16 { return accept(i16(42)) }
, .compile_error, 0, 0,
)
}
preserves_runtime_guards_with_constant_arguments_or_type_dependent_effects test {
sources [6][]u8 :: [
`advance proc(value @mut i16) void { value^ += 1 }
`predicate proc(value i16) bool {
` local i16 := value
` advance(&local)
` return local == 42
`}
`choose proc($T type, value T) T {
` if predicate(42) { return value } else { return value }
`}
`main proc() i16 { return choose(i16(42)) }
,
`advance proc(value @mut i16) void { value^ += 1 }
`predicate proc($T type) bool {
` local i16 := 41
` advance(&local)
` return T == i16 and local == 42
`}
`choose proc($T type, value T) T {
` if predicate(T) { return value } else { return value }
`}
`main proc() i16 { return choose(i16(42)) }
,
`enabled := true
`predicate proc($T type) bool { return enabled }
`choose proc($T type, value T) T {
` if predicate(T) { return value } else { return value }
`}
`main proc() i16 { return choose(i16(42)) }
,
`observe c_func() bool
`predicate proc($T type) bool { return observe() }
`choose proc($T type, value T) T {
` if predicate(T) { return value } else { return value }
`}
`main proc() i16 { return choose(i16(42)) }
,
`predicate proc($T type) bool { unreachable }
`choose proc($T type, value T) T {
` if predicate(T) { return value } else { return value }
`}
`main proc() i16 { return choose(i16(42)) }
,
`predicate proc($T type, flag bool) bool { return flag }
`choose proc($T type, value T) T {
` if predicate(T, true) { return value } else { return value }
`}
`main proc() i16 { return choose(i16(42)) }
,
]
for (sources) |source|
try expect_specialization_guards(source, .compile_error, 0, 1)
}
demands_inline_iterable_calls_before_expanding_their_bodies test {
try expect_specialization(
`fill proc(values []mut i16, value i16) void {
` for (0..values.len) |i| values[i] = value
`}
`produce proc($N usize, $seed i16) [N]mut i16 {
` values [N]mut i16 := undefined
` fill(values[..], seed)
` if values[0] != seed { compile_error!("unexpected initialization") }
` return values
`}
`main proc() i16 {
` total i16 := 0
` inline for (produce(3, 14)) |value| total += value
` return total
`}
, .compile_error, 0,
)
try expect_specialization(
`produce proc($N usize) [N]i16 {
` if N == 2 { compile_error!("reached failure") }
` return [1, 2]
`}
`main proc() void { inline for (produce(2)) |value| {} }
, .compile_error, 1,
)
}
explicit_demand_executes_ordinary_calls_and_preserves_borrow_aliases test {
try expect_specialization(
`update proc(left, right []mut i16) void {
` left[0] = 19
` right[1] = left[0] + 4
`}
`produce proc() [2]mut i16 {
` values [2]mut i16 := undefined
` update(values[..], values[..])
` if values[1] != 23 { compile_error!("lost mutable alias") }
` return values
`}
`main proc() i16 { values :: $(produce())
` return values[0] + values[1]
`}
, .compile_error, 0,
)
}
demanded_calls_still_infer_omitted_compiletime_type_arguments test {
try expect_specialization(
`identity proc($T type, value T) T { return value }
`main proc() i16 { return $(identity(i16(42))) }
, .cannot_infer_generic, 0,
)
}
demanded_expression_inline_matches_bind_payloads_and_static_tags test {
try expect_specialization(
`Detail :: union(enum) { count usize, empty void }
`score proc(input Detail) i16 {
` return match input {
` inline |payload, tag|: match tag { .count: i16(payload), else: 0 },
` }
`}
`main proc() i16 { return $(score(.count{42})) }
, .invalid_match_capture, 0,
)
}
keeps_borrowed_snapshots_owned_and_explicit_numeric_annotations_rigid test {
try expect_specialization(
`borrow proc(values []mut i16) []mut i16 { return values }
`produce proc() []mut i16 {
` values [2]mut i16 := [19, 23]
` return borrow(values[..])
`}
`main proc() void { values :: $(produce()) }
, .comptime_value_escape, 1,
)
try expect_specialization(
`produce proc() usize {
` value i16 := 1
` if value == 0 { compile_error!("unselected typed guard") }
` return value
`}
`main proc() void { value :: $(produce()) }
, .type_mismatch, 1,
)
}
keeps_inferred_compiletime_literals_contextual_until_a_rigid_source test {
try expect_specialization(
`produce proc() usize {
` width := 2
` width = 3
` cursor := 0
` cursor += width
` values [4]i16 :: [1, 2, 3, 42]
` if values[cursor] != 42 { compile_error!("wrong contextual index") }
` count := 0
` count = values.len
` return count
`}
`main proc() usize { return $(produce()) }
, .compile_error, 0,
)
try expect_specialization(
`produce proc() usize {
` width i16 := 2
` cursor usize := 0
` if cursor == 99 { compile_error!("unselected typed guard") }
` cursor += width
` return cursor
`}
`main proc() usize { return $(produce()) }
, .invalid_operand, maxval!(usize),
)
}
demanded_local_execution_retains_overflow_and_initialization_diagnostics test {
try expect_specialization(
`produce proc() u8 {
` value u8 := 255
` value += 1
` return value
`}
`main proc() u8 { return $(produce()) }
, .integer_overflow, 1,
)
try expect_specialization(
`initialize proc(values []mut i16) void { values[0] = 42 }
`produce proc() i16 {
` values [2]mut i16 := undefined
` initialize(values[..])
` return values[1]
`}
`main proc() i16 { return $(produce()) }
, .read_before_initialization, 1,
)
}
infers_nested_nominal_pointer_parameters_and_readonly_slice_views test {
try expect_specialization(
`Storage proc($T type) type { return struct { items []mut T, capacity usize } }