import "@std/mem" import "@std/arraylist" import "@std/hashmap" import "@std/enummap" import "@source/ast" import "@source/diagnostics" import "@source/strpool" import "@source/lower" import "@source/interpreter" import "@source/ir" sema :: import "@source/sema" typecheck :: import "@source/sema/check" @hide:file no_node :: maxval!(ast.NodeId) @hide:file no_index :: maxval!(usize) @hide:file Error :: alias mem.AllocError | diagnostics.CapacityError | strpool.InternError @hide:file ValueKind :: enum { invalid type integer boolean string function null real aggregate } @hide:file Value :: struct { kind ValueKind = .invalid node ast.NodeId = no_node magnitude u64 = 0 negative bool = false boolean bool = false annotation ast.NodeId = no_node } @hide:file Instance :: struct { function ast.NodeId arguments []mut Value declaration ast.NodeId = no_node type ast.NodeId = no_node next usize = no_index diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic signature ast.NodeId = no_node value ast.NodeId = no_node demanded bool = false } @hide:file Rewrite :: struct { target ast.NodeId, saved ast.NodeId } #! owns an undo log and retained checking obligations; deinit releases them with their retained allocator. #! borrows the caller's AST builder; call deinit before finishing or deinitializing that builder. #! generated nodes remain arena-owned and are released with the AST. Result :: struct { builder @mut ast.Builder changes arraylist.ArrayList(Rewrite) constraints arraylist.ArrayList(ast.NodeId) } #! restores rewritten source nodes in the borrowed builder and releases the undo log and checking obligations. #! does not remove generated arena nodes or restore resolution bindings. deinit proc(result @mut Result) void { index := result.changes.items.len while (index > 0) { index -= 1 change :: result.changes.items[index] ast.replace_node_from_source(result.builder, change.target, change.saved) } arraylist.deinit(&result.changes) arraylist.deinit(&result.constraints) } @hide:file State :: struct { allocator mem.Allocator builder @mut ast.Builder tree ast.Tree strings @mut strpool.StringPool bindings arraylist.ArrayList(sema.Binding) declarations arraylist.ArrayList(ast.NodeId) instances arraylist.ArrayList(Instance) cache hashmap.AutoHashMap(usize, usize) expanded arraylist.ArrayList(u8) capture_types hashmap.AutoHashMap(ast.NodeId, ast.NodeId) diagnostics @mut diagnostics.Store depth usize = 0 original_count usize rewrites @mut Result mapping []mut ast.NodeId resolution @mut sema.Resolution evaluation_diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic type_execution_depth usize = 0 type_execution_remaining usize = 0 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. #! extends `resolution` to cover generated nodes; no template reaches checking or lowering. #! `allocator` supplies returned logs, temporary storage released on every exit, and replacement resolution bindings. #! use the allocator retained by `resolution`; the returned Result borrows `builder` until the caller calls deinit. #! on failure, restores rewritten source nodes and releases logs; generated nodes remain arena-owned. elaborate proc( allocator mem.Allocator, builder @mut ast.Builder, root ast.NodeId, resolution @mut sema.Resolution, strings @mut strpool.StringPool, diagnostic_store @mut diagnostics.Store, ) Result ! Error { result := Result{ builder = builder, changes = arraylist.init(allocator), constraints = arraylist.init(allocator) } errdefer deinit(&result) # explicit evaluation is useful even in packages without generic declarations. original :: ast.view(builder, root) needed := false for (0..ast.node_count(&original)) |i| { id :: ast.NodeId(i) class :: ast.node_class(&original, id) if (class == .expression and (ast.expr_kind(&original, id) == .comptime_expr or ast.expr_kind(&original, id) == .block_expr)) needed = true if (class == .expression and ast.expr_kind(&original, id) == .binary and ast.binary_expr(&original, id).operator == .bit_or) needed = true if (class == .statement and ast.stmt_kind(&original, id) == .for_stmt and ast.for_stmt(&original, id).is_inline) needed = true if (class == .match_arm and ast.match_arm_view(&original, id).is_inline) needed = true if (class == .expression and ast.expr_kind(&original, id) == .builtin_call) { name :: ast.builtin_call(&original, id).name text :: ast.source_text(&original, ast.node_span(&original, name)) if (mem.eql(text, "field") or mem.eql(text, "struct_type") or mem.eql(text, "typeinfo") or mem.eql(text, "compile_error")) needed = true } if (class == .function and ast.function_generic(&original, id)) needed = true if (class == .function) { if (type_storage_annotation(&original, resolution.storage.bindings, ast.function_decl(&original, id).return_type, 0)) needed = true for (0..ast.function_param_count(&original, id)) |position| { parameter :: ast.param(&original, ast.function_param(&original, id, position)) if (!parameter.comptime and type_storage_annotation(&original, resolution.storage.bindings, parameter.type, 0)) needed = true } if (!ast.function_external(&original, id) and type_local_storage(&original, resolution.storage.bindings, ast.function_decl(&original, id).body, 0)) needed = true } if (class == .statement and ast.stmt_kind(&original, id) == .declaration and type_storage_annotation(&original, resolution.storage.bindings, ast.statement_decl(&original, id).initializer, 0)) needed = true if (needed) break } 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), instances = arraylist.init(allocator), cache = hashmap.init(allocator), capture_types = hashmap.init(allocator), expanded = arraylist.init(allocator), diagnostics = diagnostic_store, original_count = ast.node_count(&original), 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) or state.demanded_functions[usize(id)] == 1)) continue try arraylist.append(&state.declarations, id) } index := 0 while (index < state.declarations.items.len) { try expand(&state, state.declarations.items[index]) index += 1 } # metadata definitions have no runtime layout. retain their AST and bindings: # surviving runtime annotations still resolve the definition and are rejected. retained := usize(0) index = 0 while (index < state.declarations.items.len) { declaration :: state.declarations.items[index] index += 1 if (ast.node_class(&state.tree, declaration) == .statement and ast.stmt_kind(&state.tree, declaration) == .declaration) { view :: ast.statement_decl(&state.tree, declaration) if (view.mutability == .constant and type_record_storage(&state, view.initializer, 0)) { state.type_execution_remaining = 4096 state.type_execution_depth += 1 defer state.type_execution_depth -= 1 state.evaluation_diagnostic = diagnostics.no_diagnostic if (try type_record_check(&state, view.initializer)) continue try reject_evaluation(&state, view.initializer) } } state.declarations.items[retained] = declaration retained += 1 } state.declarations.items = state.declarations.items[..retained] try remember(&state, root) try ast.set_package_declarations(builder, root, state.declarations.items) try refresh(&state) owned :: try mem.alloc(sema.Binding, allocator, state.bindings.items.len) memcopy!(owned, state.bindings.items) sema.replace_bindings(resolution, owned) return result } @hide:file refresh proc(state @mut State) void ! mem.AllocError { state.tree = ast.view(state.builder, state.tree.root) count :: ast.node_count(&state.tree) while (state.bindings.items.len < count) try arraylist.append(&state.bindings, sema.Binding{}) while (state.expanded.items.len < count) try arraylist.append(&state.expanded, u8(0)) } @hide:file binding proc(state @State, id ast.NodeId) sema.Binding { if (usize(id) >= state.bindings.items.len) return sema.Binding{} return state.bindings.items[usize(id)] } @hide:file reject proc(state @mut State, id ast.NodeId, code diagnostics.Code) void ! Error { span :: ast.node_span(&state.tree, id) diagnostic :: try diagnostics.add(state.diagnostics, code, span) invalid :: try ast.add_invalid_expr(state.builder, span, diagnostic) try replace(state, id, invalid) try refresh(state) state.bindings.items[usize(id)] = sema.Binding{ kind = .invalid, data = sema.BindingData{ diagnostic = diagnostic } } } @hide:file expand_compile_error proc(state @mut State, id ast.NodeId) void ! Error { if (ast.call_arg_count(&state.tree, id) != 1) { try reject(state, id, .invalid_compile_error) return } state.evaluation_diagnostic = diagnostics.no_diagnostic value :: try evaluate(state, ast.call_arg(&state.tree, id, 0)) if (value.kind != .string) { if (state.evaluation_diagnostic != diagnostics.no_diagnostic) try reject_evaluation(state, id) else try reject(state, id, .invalid_compile_error) return } text :: ast.literal_text(&state.tree, value.node) bytes := arraylist.init(u8, state.allocator) defer arraylist.deinit(&bytes) cursor := usize(1) while (cursor < text.len - 1) { byte :: ast.string_next(text, &cursor) catch { try reject(state, id, .invalid_escape) return } try arraylist.append(&bytes, byte) } state.evaluation_diagnostic = try diagnostics.add_message(state.diagnostics, .compile_error, ast.node_span(&state.tree, id), bytes.items) 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) } @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) saved_return :: state.return_type saved_yield :: state.yield_type defer { state.return_type = saved_return state.yield_type = saved_yield } 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) { .declaration: { view :: ast.statement_decl(&state.tree, id) annotation := no_node if (view.type) |type| { try expand(state, type) annotation = type } try expand_expected(state, view.initializer, annotation) return } .assignment_stmt: { view :: ast.assignment_stmt(&state.tree, id) try expand(state, view.target) try expand_expected(state, view.value, try expression_type(state, view.target, 0)) return } .return_stmt, .yield_stmt: { yielding :: ast.stmt_kind(&state.tree, id) == .yield_stmt value :: if (yielding) ast.yield_stmt(&state.tree, id).value else ast.return_stmt(&state.tree, id).value annotation :: if (yielding) state.yield_type else state.return_type if (value) |operand| try expand_expected(state, operand, annotation) return } else: {} } } if (class == .field) { view :: ast.field(&state.tree, id) annotation := no_node if (view.type) |type| { try expand(state, type) annotation = type } if (view.value) |value| try expand_expected(state, value, annotation) return } if (class == .expression and ast.expr_kind(&state.tree, id) == .block_expr) { body :: ast.block_expr(&state.tree, id) annotation :: if (expected != no_node) expected else try value_block_type(state, body, 0) if (is_type(state, annotation) or type_record_storage(state, annotation, 0)) { selected :: try evaluate_type_value_block(state, id, annotation) if (selected == no_node) { try reject_evaluation(state, id) return } try replace(state, id, selected) try refresh(state) state.bindings.items[usize(id)] = binding(state, selected) return } state.value_expansion_depth += 1 defer state.value_expansion_depth -= 1 state.yield_type = annotation try expand(state, body) return } if (class == .block and state.value_expansion_depth > 0) { statements := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&statements) count :: ast.block_statement_count(&state.tree, id) for (0..count) |i| { statement :: ast.block_statement(&state.tree, id, i) try expand(state, statement) try arraylist.append(&statements, statement) if (value_block_terminal(state, statement)) break } if (statements.items.len != count) { replacement :: try ast.add_block(state.builder, ast.node_span(&state.tree, id), statements.items) try replace(state, id, replacement) try refresh(state) } return } if (class == .statement and ast.stmt_kind(&state.tree, id) == .for_stmt and ast.for_stmt(&state.tree, id).is_inline) { try expand_inline_for(state, id) return } if (class == .statement and ast.stmt_kind(&state.tree, id) == .for_stmt) { loop :: ast.for_stmt(&state.tree, id) try expand(state, loop.iterable) if (loop.index) |capture| { annotation :: try builtin(state, id, .usize) hashmap.put(&state.capture_types, capture, annotation) catch |err| { match err { .key_exists: {} else: return err } } } if (!loop.pointer_capture and loop.capture != null) { iterable :: try expression_type(state, loop.iterable, 0) element := no_node if (ast.expr_kind(&state.tree, loop.iterable) == .range) { element = try expression_type(state, ast.range_expr(&state.tree, loop.iterable).start, 0) } else if (iterable != no_node) { kind :: ast.expr_kind(&state.tree, iterable) if (kind == .array_type) element = ast.array_type(&state.tree, iterable).element if (kind == .slice_type) element = ast.slice_type(&state.tree, iterable).element if (kind == .pointer_type) { array :: try canonical_type(state, ast.pointer_type(&state.tree, iterable).element) if (array != no_node and ast.expr_kind(&state.tree, array) == .array_type) element = ast.array_type(&state.tree, array).element } if (binding(state, iterable).kind == .builtin and binding(state, iterable).data.builtin == .string) element = try builtin(state, id, .u8) } if (element != no_node) { hashmap.put(&state.capture_types, loop.capture?, element) catch |err| { match err { .key_exists: {} else: return err } } } } } if (class == .statement and (ast.stmt_kind(&state.tree, id) == .if_stmt or ast.stmt_kind(&state.tree, id) == .while_stmt)) { looping :: ast.stmt_kind(&state.tree, id) == .while_stmt capture :: if (looping) ast.while_stmt(&state.tree, id).capture else ast.if_stmt(&state.tree, id).capture condition :: if (looping) ast.while_stmt(&state.tree, id).condition else ast.if_stmt(&state.tree, id).condition if (capture) |name| { annotation :: try expression_type(state, condition, 0) if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .optional_type) { child :: ast.type_operand(&state.tree, annotation) hashmap.put(&state.capture_types, name, child) catch |err| { match err { .key_exists: {} else: return err } } } } } if (class == .expression and ast.expr_kind(&state.tree, id) == .catch_expr) { expression :: ast.catch_expr(&state.tree, id) try expand_expected(state, expression.operand, expected) operand_type :: try expression_type(state, expression.operand, 0) if (type_record_storage(state, operand_type, 0) and ast.expr_kind(&state.tree, operand_type) == .fallible_type) { try specialize_type_catch(state, id, operand_type, expected) return } if (expression.capture) |capture| { annotation :: try expression_type(state, expression.operand, 0) if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .fallible_type) { hashmap.put(&state.capture_types, capture, ast.binary_expr(&state.tree, annotation).right) catch |err| { match err { .key_exists: {} else: return err } } } } annotation :: if (expected != no_node) expected else try expression_type(state, id, 0) state.yield_type = annotation try expand_expected(state, expression.handler, annotation) return } if (class == .statement and ast.stmt_kind(&state.tree, id) == .expression) { expression :: ast.statement_expr(&state.tree, id) if ( ast.expr_kind(&state.tree, expression) == .match_expr and try specialize_match(state, expression, id, no_node) ) return } if ( class == .expression and ast.expr_kind(&state.tree, id) == .match_expr and try specialize_match(state, id, no_node, expected) ) return if (class == .statement and ast.stmt_kind(&state.tree, id) == .if_stmt) { conditional :: ast.if_stmt(&state.tree, id) explicit :: ast.expr_kind(&state.tree, conditional.condition) == .comptime_expr if (conditional.capture == null and (explicit or usize(id) >= state.original_count)) { condition :: if (explicit) ast.comptime_operand(&state.tree, conditional.condition) else conditional.condition try expand(state, condition) if (explicit or branch_constant(state, condition, 0)) { state.evaluation_diagnostic = diagnostics.no_diagnostic boolean :: try builtin(state, condition, .bool) value :: try evaluate_ir(state, condition, boolean) if (explicit and value.kind == .invalid and state.evaluation_diagnostic == diagnostics.no_diagnostic) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_argument_required, ast.node_span(&state.tree, condition)) if (state.evaluation_diagnostic != diagnostics.no_diagnostic) { invalid :: try ast.add_invalid_stmt(state.builder, ast.node_span(&state.tree, id), state.evaluation_diagnostic) try replace(state, id, invalid) try refresh(state) return } if (value.kind == .boolean) { branch := conditional.then_block if (!value.boolean) { if (conditional.else_block) |other| branch = other else branch = try ast.add_block(state.builder, ast.node_span(&state.tree, id), &[]) } replacement :: try ast.add_block_stmt(state.builder, branch) try replace(state, id, replacement) try refresh(state) try expand(state, branch) return } } } } 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 value :: try evaluate(state, operand) if (value.annotation == no_node or !type_record_storage(state, value.annotation, 0)) try arraylist.append(&state.rewrites.constraints, operand) if (value.kind == .invalid) { try reject_evaluation(state, id) return } concrete :: try materialize(state, value) replacement := concrete.node if (value.annotation != no_node and value.kind != .type) { replacement = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, id), value.annotation, replacement) try refresh(state) } try replace(state, id, replacement) try refresh(state) return } if (class == .expression and ast.expr_kind(&state.tree, id) == .builtin_call) { name :: ast.builtin_call(&state.tree, id).name if ( mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "some") and expected != no_node and ast.call_arg_count(&state.tree, id) == 1 ) { annotation := try canonical_type(state, expected) if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .fallible_type) annotation = try canonical_type(state, ast.binary_expr(&state.tree, annotation).left) if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .optional_type) { try expand_expected(state, ast.call_arg(&state.tree, id, 0), ast.type_operand(&state.tree, annotation)) return } } if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "compile_error")) { try expand_compile_error(state, id) return } if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "field")) { try expand_field(state, id) return } if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "struct_type")) { try expand_struct_type(state, id) return } if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "typeinfo")) { try expand_typeinfo(state, id) return } } if (class == .expression and ast.expr_kind(&state.tree, id) == .call) { callee :: ast.call_expr(&state.tree, id).callee try expand(state, callee) resolved :: binding(state, callee) if ( resolved.kind == .declaration and ast.node_class(&state.tree, resolved.data.declaration) == .function 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 } signature := no_node if (resolved.kind == .declaration and ast.node_class(&state.tree, resolved.data.declaration) == .function) signature = resolved.data.declaration else signature = try expression_type(state, callee, 0) if ( signature != no_node and (ast.node_class(&state.tree, signature) == .function or ast.expr_kind(&state.tree, signature) == .function_type) and ast.function_param_count(&state.tree, signature) == ast.call_arg_count(&state.tree, id) ) { for (0..ast.call_arg_count(&state.tree, id)) |i| { annotation :: ast.param(&state.tree, ast.function_param(&state.tree, signature, i)).type try expand_expected(state, ast.call_arg(&state.tree, id, i), annotation) } return } } if (class == .expression and expected != no_node) { kind :: ast.expr_kind(&state.tree, id) if (kind == .unary) { view :: ast.unary_expr(&state.tree, id) if (view.operator == .negate or view.operator == .bit_not or view.operator == .logical_not) { try expand_expected(state, view.operand, expected) return } } if (kind == .binary and !is_type(state, expected)) { view :: ast.binary_expr(&state.tree, id) match view.operator { .add, .subtract, .multiply, .divide, .bit_and, .bit_or, .bit_xor, .logical_and, .logical_or: { try expand_expected(state, view.left, expected) try expand_expected(state, view.right, expected) return } .shift_left, .shift_right, .shift_left_saturating: { try expand_expected(state, view.left, expected) try expand(state, view.right) return } else: {} } } if (kind == .try_expr) { try expand_expected(state, ast.type_operand(&state.tree, id), expected) return } if (kind == .match_expr) { try expand(state, ast.match_expr(&state.tree, id).subject) for (0..ast.match_arm_count(&state.tree, id)) |i| { arm_id :: ast.match_arm(&state.tree, id, i) arm :: ast.match_arm_view(&state.tree, arm_id) for (0..ast.match_pattern_count(&state.tree, arm_id)) |j| try expand(state, ast.match_pattern(&state.tree, arm_id, j)) state.yield_type = expected annotation :: if (arm.expression) expected else no_node try expand_expected(state, arm.body, annotation) } return } } if (class == .expression) { kind :: ast.expr_kind(&state.tree, id) if (kind == .record_literal or kind == .array_literal) { annotation := expected if (kind == .record_literal) { if (ast.record_literal(&state.tree, id).type) |type| { try expand(state, type) annotation = type } } shape := if (annotation != no_node) try canonical_type(state, annotation) else no_node if (shape != no_node and ast.expr_kind(&state.tree, shape) == .fallible_type) shape = try canonical_type(state, ast.binary_expr(&state.tree, shape).left) if (shape != no_node and ast.expr_kind(&state.tree, shape) == .optional_type) shape = try canonical_type(state, ast.type_operand(&state.tree, shape)) if ( kind == .array_literal and shape != no_node and (ast.expr_kind(&state.tree, shape) == .array_type or ast.expr_kind(&state.tree, shape) == .slice_type) ) { element :: if (ast.expr_kind(&state.tree, shape) == .array_type) ast.array_type(&state.tree, shape).element else ast.slice_type(&state.tree, shape).element for (0..ast.array_element_count(&state.tree, id)) |i| try expand_expected(state, ast.array_element(&state.tree, id, i), element) return } if ( kind == .record_literal and shape != no_node and (ast.expr_kind(&state.tree, shape) == .record_type or ast.expr_kind(&state.tree, shape) == .union_type) ) { for (0..ast.record_field_count(&state.tree, id)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i)) slot :: if (field.name != null) type_record_slot(&state.tree, shape, field.name?) else i field_type := no_node if (slot < ast.record_field_count(&state.tree, shape)) { if (ast.field(&state.tree, ast.record_field(&state.tree, shape, slot)).type) |type| field_type = type } if (field.value) |value| try expand_expected(state, value, field_type) } return } } } count :: ast.child_count(&state.tree, id) for (0..count) |i| { child :: ast.child(&state.tree, id, i) try expand(state, child) } if (!try type_array_projection(state, id)) { try reject_evaluation(state, id) return } if (class == .expression and ast.expr_kind(&state.tree, id) == .binary) { view :: ast.binary_expr(&state.tree, id) if (view.operator == .bit_or and (is_type(state, try expression_type(state, view.left, 0)) or is_type(state, try expression_type(state, view.right, 0)))) { composed :: try ast.add_binary_type(state.builder, view.span, view.left, view.right, false) try replace(state, id, composed) try refresh(state) return } if (view.operator == .equal or view.operator == .not_equal) { operand := no_node if (ast.expr_kind(&state.tree, view.left) == .literal_null) operand = view.right else if (ast.expr_kind(&state.tree, view.right) == .literal_null) operand = view.left if (operand != no_node) { annotation :: try expression_type(state, operand, 0) if (type_record_storage(state, annotation, 0) and ast.expr_kind(&state.tree, annotation) == .optional_type) { value :: try evaluate_expected(state, operand, annotation) if (value.kind != .aggregate) { try reject_evaluation(state, id) return } absent :: ast.expr_kind(&state.tree, value.node) == .literal_null folded :: try ast.add_boolean(state.builder, view.span, absent == (view.operator == .equal)) try replace(state, id, folded) try refresh(state) return } } } if ( (view.operator == .equal or view.operator == .not_equal) and branch_constant(state, view.left, 0) and branch_constant(state, view.right, 0) ) { left :: try evaluate(state, view.left) right :: try evaluate(state, view.right) if (left.kind == .type and right.kind == .type) _ = try evaluate_binary(state, id) } } if (usize(id) >= state.original_count and class == .expression and ast.expr_kind(&state.tree, id) == .array_type) { view :: ast.array_type(&state.tree, id) if (view.count) |count_node| { try arraylist.append(&state.rewrites.constraints, count_node) value :: try evaluate(state, count_node) if (value.kind == .integer) { folded :: try materialize(state, value) replacement :: try ast.add_array_type(state.builder, ast.node_span(&state.tree, id), folded.node, view.element, view.mutable) try replace(state, id, replacement) try refresh(state) } } } } @hide:file value_block_terminal proc(state @State, id ast.NodeId) bool { class :: ast.node_class(&state.tree, id) if (class == .block) { count :: ast.block_statement_count(&state.tree, id) return count > 0 and value_block_terminal(state, ast.block_statement(&state.tree, id, count - 1)) } if (class != .statement) return false match ast.stmt_kind(&state.tree, id) { .yield_stmt, .return_stmt, .break_stmt, .continue_stmt, .unreachable_stmt: return true .block_stmt: return value_block_terminal(state, ast.statement_block(&state.tree, id)) .if_stmt: { view :: ast.if_stmt(&state.tree, id) return ( view.else_block != null and value_block_terminal(state, view.then_block) and value_block_terminal(state, view.else_block?) ) } else: return false } } # reflection values are owned AST snapshots. only ordinary leaves and selected # runtime bodies enter IR; metadata slices never retain interpreter addresses. @hide:file metadata_member proc(state @mut State, at ast.NodeId, text []u8) ast.NodeId ! Error { result :: try ast.add_generated_identifier(state.builder, ast.node_span(&state.tree, at), try strpool.intern(state.strings, text), text) try refresh(state) return result } @hide:file metadata_package_contains proc(state @State, package ast.NodeId, source diagnostics.SourceId) bool { for (0..ast.package_decl_count_at(&state.tree, package)) |i| { if (ast.node_span(&state.tree, ast.package_decl_at(&state.tree, package, i)).source == source) return true } return false } @hide:file metadata_tags proc(state @mut State, type ast.NodeId) ast.NodeId ! Error { if (ast.expr_kind(&state.tree, type) == .enum_type) return type if (ast.union_type(&state.tree, type).tag) |tag| return try canonical_type(state, tag) return try metadata_variant_tags(state, type) } # dispatch covers union variants, not unused members of an explicit tag enum. @hide:file metadata_variant_tags proc(state @mut State, type ast.NodeId) ast.NodeId ! Error { members := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&members) for (0..ast.record_field_count(&state.tree, type)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i)) member :: try ast.add_field(state.builder, field.span, field.name, null, null) try arraylist.append(&members, member) try refresh(state) } result :: try ast.add_enum_type(state.builder, ast.node_span(&state.tree, type), null, members.items) try refresh(state) return result } @hide:file metadata_import proc(state @State, at ast.NodeId) ast.NodeId { source :: ast.node_span(&state.tree, at).source for (0..state.original_count) |i| { imported :: ast.NodeId(i) resolved :: binding(state, imported) if (ast.node_class(&state.tree, imported) != .import or resolved.kind != .namespace) continue if (!mem.eql(ast.source_text(&state.tree, ast.import_decl(&state.tree, imported).path), "@std/meta")) continue other :: ast.node_span(&state.tree, imported).source if (source == other) return resolved.data.namespace if (metadata_package_contains(state, resolved.data.namespace, source)) return resolved.data.namespace for (0..state.original_count) |j| { package :: ast.NodeId(j) if (package == state.tree.root or ast.node_class(&state.tree, package) != .package) continue if ( metadata_package_contains(state, package, source) and metadata_package_contains(state, package, other) ) return resolved.data.namespace } } return no_node } @hide:file metadata_type proc(state @mut State, package ast.NodeId, name []u8) ast.NodeId ! Error { for (0..ast.package_decl_count_at(&state.tree, package)) |i| { declaration :: ast.package_decl_at(&state.tree, package, i) if (ast.node_class(&state.tree, declaration) != .statement) continue if (mem.eql(strpool.get_str(state.strings, ast.name_id(&state.tree, declaration))?, name)) return try canonical_type(state, ast.statement_decl(&state.tree, declaration).initializer) } return no_node } @hide:file metadata_record proc(state @mut State, at, type ast.NodeId, values []ast.NodeId) ast.NodeId ! Error { try refresh(state) fields := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&fields) span :: ast.node_span(&state.tree, at) for (values) |value, i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i)) owned := value if (!is_type(state, field.type?)) owned = try ast.add_typed_constant(state.builder, span, field.type?, value) member :: try ast.add_field(state.builder, span, field.name, null, owned) try arraylist.append(&fields, member) try refresh(state) } literal :: try ast.add_record_literal(state.builder, span, null, fields.items) result :: try ast.add_typed_constant(state.builder, span, type, literal) try refresh(state) return result } @hide:file metadata_schema proc(state @mut State, type ast.NodeId, names [][]u8, types []ast.NodeId) bool ! Error { if ( type == no_node or ast.expr_kind(&state.tree, type) != .record_type or ast.record_field_count(&state.tree, type) != names.len ) return false for (names) |name, i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i)) if ( field.name == null or field.type == null or !mem.eql(ast.literal_text(&state.tree, field.name?), name) or !try same_type(state, field.type?, types[i]) ) return false } return true } @hide:file metadata_abi proc(state @mut State, at ast.NodeId, types []ast.NodeId) bool ! Error { info :: types[0] if ( info == no_node or ast.expr_kind(&state.tree, info) != .union_type or !ast.union_type(&state.tree, info).auto_tag ) return false span :: ast.node_span(&state.tree, at) byte_string :: try ast.add_slice_type(state.builder, span, try builtin(state, at, .u8), false, null) optional_name :: try ast.add_optional_type(state.builder, span, byte_string) fields :: try ast.add_slice_type(state.builder, span, types[2], false, null) usize_type :: try builtin(state, at, .usize) type_type :: try builtin(state, at, .type) bool_type :: try builtin(state, at, .bool) try refresh(state) array_names [2][]u8 :: ["child", "len"] array_types [2]ast.NodeId :: [type_type, usize_type] field_names [3][]u8 :: ["name", "type", "index"] field_types [3]ast.NodeId :: [byte_string, type_type, usize_type] record_names [4][]u8 :: ["name", "fields", "is_tuple", "layout"] record_types [4]ast.NodeId :: [optional_name, fields, bool_type, types[5]] enum_names [1][]u8 :: ["fields"] enum_types [1]ast.NodeId :: [fields] if ( !try metadata_schema(state, types[1], array_names[..], array_types[..]) or !try metadata_schema(state, types[2], field_names[..], field_types[..]) or !try metadata_schema(state, types[3], record_names[..], record_types[..]) or !try metadata_schema(state, types[4], enum_names[..], enum_types[..]) ) return false layout :: types[5] if ( layout == no_node or ast.expr_kind(&state.tree, layout) != .enum_type or ast.enum_member_count(&state.tree, layout) != 2 ) return false for (0..usize(2)) |i| { name :: ast.field(&state.tree, ast.enum_member(&state.tree, layout, i)).name expected :: if (i == 0) "auto" else "c" if (name == null or !mem.eql(ast.literal_text(&state.tree, name?), expected)) return false } names [18][]u8 :: ["invalid", "void", "noreturn", "anyopaque", "bool", "integer", "float", "array", "pointer", "slice", "range", "optional", "function", "enum", "record", "union", "fallible", "distinct"] if (ast.record_field_count(&state.tree, info) != names.len) return false void_type :: try builtin(state, at, .void) for (names) |name, i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, info, i)) expected := void_type if (i == 7) expected = types[1] if (i == 13) expected = types[4] if (i == 14) expected = types[3] if (i == 17) expected = type_type if ( field.name == null or field.type == null or !mem.eql(ast.literal_text(&state.tree, field.name?), name) or !try same_type(state, field.type?, expected) ) return false } return true } @hide:file expand_typeinfo proc(state @mut State, id ast.NodeId) void ! Error { if (ast.call_arg_count(&state.tree, id) != 1) { try reject(state, id, .argument_count_mismatch) return } 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 argument :: ast.call_arg(&state.tree, id, 0) value :: try evaluate(state, argument) if (value.kind != .type) { if (state.evaluation_diagnostic != diagnostics.no_diagnostic) try reject_evaluation(state, id) else try reject(state, id, .expected_type) return } package :: metadata_import(state, id) if (package == no_node) { try reject(state, id, .missing_metadata_import) return } types [6]mut ast.NodeId := undefined names [6][]u8 :: ["TypeInfo", "ArrayInfo", "FieldInfo", "RecordInfo", "EnumInfo", "Layout"] for (names) |name, i| { types[i] = try metadata_type(state, package, name) if (types[i] == no_node) { try reject(state, id, .invalid_metadata_abi) return } } if (!try metadata_abi(state, id, types[..])) { try reject(state, id, .invalid_metadata_abi) return } type :: value.node if (type_record_storage(state, type, 0)) { if (!try type_record_check(state, type)) { try reject_evaluation(state, id) return } } else if (binding(state, type).kind != .builtin) { constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, argument), argument, true) try refresh(state) try arraylist.append(&state.rewrites.constraints, constraint) } kind :: ast.expr_kind(&state.tree, type) tag []u8 := "invalid" payload := no_node shape := type resolved :: binding(state, type) if (resolved.kind == .builtin) { builtin_kind :: builtin_identity(resolved.data.builtin) if (try integer_type(state, type)) tag = "integer" else if (builtin_kind == .float) tag = "float" else if (builtin_kind == .bool) tag = "bool" else if (builtin_kind == .void) tag = "void" else if (builtin_kind == .anyopaque) tag = "anyopaque" else if (builtin_kind == .string) tag = "pointer" } else if (kind == .array_type) { tag = "array" array :: ast.array_type(&state.tree, type) length := Value{} if (array.count) |count| length = try evaluate(state, count) if (length.kind != .integer or length.negative) { try reject(state, id, .invalid_array_length) return } child :: (try materialize(state, try evaluate(state, array.element))).node count :: try ast.add_integer(state.builder, ast.node_span(&state.tree, id), length.magnitude) values [2]ast.NodeId :: [child, count] payload = try metadata_record(state, id, types[1], values[..]) } else if (kind == .distinct_type) { tag = "distinct" payload = (try materialize(state, try evaluate(state, ast.type_operand(&state.tree, type)))).node } else if (kind == .pointer_type) tag = "pointer" else if (kind == .slice_type) tag = "slice" else if (kind == .optional_type) tag = "optional" else if (kind == .function_type) tag = "function" else if (kind == .fallible_type) tag = "fallible" else if (kind == .union_type) tag = "union" else if (kind == .record_type) tag = "record" else if (kind == .enum_type or kind == .union_tag_type) { tag = "enum" if (kind == .union_tag_type) { variants :: try inline_match_shape(state, try canonical_type(state, ast.type_operand(&state.tree, type))) shape = try metadata_tags(state, variants) } } else if (kind == .sum_type) { shape = try inline_match_shape(state, type) if (shape == no_node) { try reject_evaluation(state, id) return } tag = if (ast.expr_kind(&state.tree, shape) == .enum_type) "enum" else "union" } span :: ast.node_span(&state.tree, id) if (mem.eql(tag, "record") or mem.eql(tag, "enum")) { enumeration :: mem.eql(tag, "enum") fields := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&fields) count :: if (enumeration) ast.enum_member_count(&state.tree, shape) else ast.record_field_count(&state.tree, shape) tuple := !enumeration for (0..count) |i| { if (!try type_step(state, id)) { try reject_evaluation(state, id) return } field_id :: if (enumeration) ast.enum_member(&state.tree, shape, i) else ast.record_field(&state.tree, shape, i) field :: ast.field(&state.tree, field_id) ordinal :: try ast.add_integer(state.builder, span, u64(i)) try refresh(state) name := ordinal if (field.name) |member| { name = member tuple = false } text :: try ast.add_string(state.builder, span, ast.literal_text(&state.tree, name)) child_type :: if (enumeration) type else field.type? child :: (try materialize(state, try evaluate(state, child_type))).node values [3]ast.NodeId :: [text, child, ordinal] try arraylist.append(&fields, try metadata_record(state, id, types[2], values[..])) } collection :: try ast.add_array_literal(state.builder, span, fields.items) if (enumeration) { values [1]ast.NodeId :: [collection] payload = try metadata_record(state, id, types[4], values[..]) } else { name := try ast.add_literal(state.builder, .null, span) try refresh(state) for (state.declarations.items) |declaration| { if (ast.node_class(&state.tree, declaration) == .statement and ast.statement_decl(&state.tree, declaration).initializer == type) { name = try ast.add_string(state.builder, span, strpool.get_str(state.strings, ast.name_id(&state.tree, declaration))?) break } } tuple_value :: try ast.add_boolean(state.builder, span, tuple) try refresh(state) layout_text :: if (ast.record_c_layout(&state.tree, type)) "c" else "auto" layout_name :: try metadata_member(state, id, layout_text) layout :: try ast.add_enum_literal(state.builder, span, layout_name) values [4]ast.NodeId :: [name, collection, tuple_value, layout] payload = try metadata_record(state, id, types[3], values[..]) } } if (payload == no_node) payload = try ast.add_literal(state.builder, .void, span) member :: try metadata_member(state, id, tag) field :: try ast.add_field(state.builder, span, member, null, payload) fields [1]ast.NodeId :: [field] literal :: try ast.add_record_literal(state.builder, span, null, fields[..]) result :: try ast.add_typed_constant(state.builder, span, types[0], literal) try replace(state, id, result) try refresh(state) } # erase only the name: ordinary selection checks the base and retains its place, # evaluation order, mutability and active-union checks through both backends. @hide:file expand_field proc(state @mut State, id ast.NodeId) void ! Error { if (ast.call_arg_count(&state.tree, id) != 2) { try reject(state, id, .argument_count_mismatch) return } base :: ast.call_arg(&state.tree, id, 0) argument :: ast.call_arg(&state.tree, id, 1) state.evaluation_diagnostic = diagnostics.no_diagnostic value :: try evaluate(state, argument) if (value.kind != .string) { if (state.evaluation_diagnostic == diagnostics.no_diagnostic) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_argument_required, ast.node_span(&state.tree, argument)) try reject_evaluation(state, id) return } try arraylist.append(&state.rewrites.constraints, argument) text :: ast.literal_text(&state.tree, value.node) bytes := arraylist.init(u8, state.allocator) defer arraylist.deinit(&bytes) cursor := usize(1) while (cursor < text.len - 1) { byte :: ast.string_next(text, &cursor) catch { try reject(state, id, .invalid_escape) return } try arraylist.append(&bytes, byte) } # only canonical decimal names denote tuple ordinals; "01" is a name. numeric := bytes.items.len > 0 and (bytes.items.len == 1 or bytes.items[0] != '0') ordinal := u64(0) for (bytes.items) |byte| { if (byte < '0' or byte > '9') { numeric = false break } digit :: u64(byte - '0') if (ordinal > divtrunc!(maxval!(u64) - digit, u64(10))) { numeric = false break } ordinal = ordinal * 10 + digit } span :: ast.node_span(&state.tree, argument) member := no_node if (numeric) member = try ast.add_integer(state.builder, span, ordinal) else { interned :: try strpool.intern(state.strings, bytes.items) member = try ast.add_generated_identifier(state.builder, span, interned, bytes.items) } selection :: try ast.add_selection(state.builder, ast.node_span(&state.tree, id), base, member) try replace(state, id, selection) try refresh(state) state.expanded.items[usize(id)] = 0 try expand(state, id) } # literal collections deliberately retain per-field context: generated records # may pair heterogeneous types and optional defaults in arrays as well as tuples. @hide:file struct_collection proc(state @mut State, input ast.NodeId) ast.NodeId ! Error { if (!try type_step(state, input)) return no_node try expand(state, input) resolved :: binding(state, input) if (resolved.kind == .invalid) { state.evaluation_diagnostic = resolved.data.diagnostic return no_node } if (resolved.kind == .declaration and ast.node_class(&state.tree, resolved.data.declaration) == .statement) { declaration :: ast.statement_decl(&state.tree, resolved.data.declaration) if (declaration.mutability != .constant) return no_node if (declaration.type) |annotation| { value :: try type_execution_value(state, declaration.initializer, annotation) if (value == no_node) return no_node return try struct_collection(state, value) } return try struct_collection(state, declaration.initializer) } kind :: ast.expr_kind(&state.tree, input) if (kind == .array_literal) return input if (kind == .record_literal) { if (ast.record_literal(&state.tree, input).type != null) { annotation :: try expression_type(state, input, 0) value :: try type_execution_value(state, input, annotation) if (value == no_node) return no_node return try struct_collection(state, value) } for (0..ast.record_field_count(&state.tree, input)) |i| { if (ast.field(&state.tree, ast.record_field(&state.tree, input, i)).name != null) return no_node } return input } if (kind == .typed_constant) { value :: ast.typed_constant(&state.tree, input) annotation :: try canonical_type(state, value.type) if (annotation == no_node) return no_node if (ast.expr_kind(&state.tree, annotation) != .array_type) { if (ast.expr_kind(&state.tree, annotation) != .record_type) return no_node for (0..ast.record_field_count(&state.tree, annotation)) |i| { if (ast.field(&state.tree, ast.record_field(&state.tree, annotation, i)).name != null) return no_node } } if (!try type_array_initialized(state, input, input)) return no_node return input } value :: try evaluate(state, input) if (value.kind != .aggregate or value.annotation == no_node) return no_node owned :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, input), value.annotation, value.node) try refresh(state) return try struct_collection(state, owned) } @hide:file struct_collection_count proc(state @State, input ast.NodeId) usize { value :: if (ast.expr_kind(&state.tree, input) == .typed_constant) ast.typed_constant(&state.tree, input).value else input return if (ast.expr_kind(&state.tree, value) == .array_literal) ast.array_element_count(&state.tree, value) else ast.record_field_count(&state.tree, value) } @hide:file struct_collection_item proc(state @mut State, input ast.NodeId, index usize) ast.NodeId ! Error { value := input annotation := no_node if (ast.expr_kind(&state.tree, input) == .typed_constant) { view :: ast.typed_constant(&state.tree, input) value = view.value type :: try canonical_type(state, view.type) if (ast.expr_kind(&state.tree, type) == .array_type) annotation = ast.array_type(&state.tree, type).element else annotation = ast.field(&state.tree, ast.record_field(&state.tree, type, index)).type? } element :: if (ast.expr_kind(&state.tree, value) == .array_literal) ast.array_element(&state.tree, value, index) else ast.field(&state.tree, ast.record_field(&state.tree, value, index)).value? if (annotation == no_node or is_type(state, annotation) or ast.expr_kind(&state.tree, element) == .typed_constant) return element result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, element), annotation, element) try refresh(state) return result } @hide:file expand_struct_type proc(state @mut State, id ast.NodeId) void ! Error { if (ast.call_arg_count(&state.tree, id) != 4) { try reject(state, id, .argument_count_mismatch) return } 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 layout_input :: ast.call_arg(&state.tree, id, 0) try expand(state, layout_input) layout := layout_input if (ast.expr_kind(&state.tree, layout) != .enum_literal) { value :: try evaluate_ir(state, layout, try expression_type(state, layout, 0)) layout = value.node } if (layout == no_node or ast.expr_kind(&state.tree, layout) != .enum_literal) { if (state.evaluation_diagnostic == diagnostics.no_diagnostic) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_struct_layout, ast.node_span(&state.tree, layout_input)) try reject_evaluation(state, id) return } layout_name :: ast.literal_text(&state.tree, ast.enum_literal(&state.tree, layout)) c_layout :: mem.eql(layout_name, "c") if (!c_layout and !mem.eql(layout_name, "auto")) { try reject(state, id, .invalid_struct_layout) return } collections [3]mut ast.NodeId := undefined for (0..usize(3)) |i| { collections[i] = try struct_collection(state, ast.call_arg(&state.tree, id, i + 1)) if (collections[i] == no_node) { try reject_evaluation(state, id) return } } count :: struct_collection_count(state, collections[0]) if (count != struct_collection_count(state, collections[1]) or count != struct_collection_count(state, collections[2])) { try reject(state, id, .array_length_mismatch) return } if (c_layout and count == 0) { try reject(state, id, .invalid_struct_layout) return } if (count > state.type_execution_remaining) { try reject(state, id, .comptime_execution_limit) return } fields := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&fields) bytes := arraylist.init(u8, state.allocator) defer arraylist.deinit(&bytes) for (0..count) |i| { if (!try type_step(state, id)) { try reject_evaluation(state, id) return } name_input :: try struct_collection_item(state, collections[0], i) name_value :: try evaluate(state, name_input) if (name_value.kind != .string) { try reject_evaluation(state, id) return } bytes.items = bytes.items[..0] text :: ast.literal_text(&state.tree, name_value.node) cursor := usize(1) while (cursor < text.len - 1) { byte :: ast.string_next(text, &cursor) catch { try reject(state, id, .invalid_escape) return } valid :: ( byte == '_' or (byte >= 'a' and byte <= 'z') or (byte >= 'A' and byte <= 'Z') or (bytes.items.len > 0 and byte >= '0' and byte <= '9') ) if (!valid) { try reject(state, id, .invalid_generated_field_name) return } try arraylist.append(&bytes, byte) } if (bytes.items.len == 0) { try reject(state, id, .invalid_generated_field_name) return } span :: ast.node_span(&state.tree, name_input) name :: try ast.add_generated_identifier(state.builder, span, try strpool.intern(state.strings, bytes.items), bytes.items) try refresh(state) for (fields.items) |previous| { if (ast.name_id(&state.tree, ast.field(&state.tree, previous).name?) == ast.name_id(&state.tree, name)) { try reject(state, id, .duplicate_field) return } } type_input :: try struct_collection_item(state, collections[1], i) type_value :: try evaluate(state, type_input) if (type_value.kind != .type) { if (state.evaluation_diagnostic == diagnostics.no_diagnostic) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_record_field_type, ast.node_span(&state.tree, type_input)) try reject_evaluation(state, id) return } field_type :: (try materialize(state, type_value)).node default_input :: try struct_collection_item(state, collections[2], i) optional :: try ast.add_optional_type(state.builder, span, field_type) contextual :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, default_input), optional, default_input) capture :: try ast.add_identifier(state.builder, span, strpool.no_str) try refresh(state) present :: try type_capture_guard(state, contextual, capture) if (present.kind != .boolean) { try reject_evaluation(state, id) return } default ?ast.NodeId := null if (present.boolean) { unwrapped :: try ast.add_unary(state.builder, ast.node_span(&state.tree, default_input), .unwrap, contextual) try refresh(state) value :: try type_execution_value(state, unwrapped, field_type) if (value == no_node) { try reject_evaluation(state, id) return } default = value } field :: try ast.add_field(state.builder, span, name, field_type, default) try refresh(state) try arraylist.append(&fields, field) } record :: try ast.add_record_type(state.builder, ast.node_span(&state.tree, id), fields.items) if (c_layout) ast.set_record_c_layout(state.builder, record) try replace(state, id, record) try refresh(state) state.expanded.items[usize(id)] = 0 try expand(state, id) } # composed struct variants use declaration IDs internally, but generated syntax # must not embed those declarations and make dependency discovery treat them as owned. @hide:file member_name proc(state @mut State, name ast.NodeId) ast.NodeId ! Error { if (ast.node_class(&state.tree, name) != .statement) return name result :: try ast.add_identifier(state.builder, ast.node_span(&state.tree, name), ast.name_id(&state.tree, name)) try refresh(state) return result } # flatten source member names and payload annotations only; ordinary checking still # validates composition, variant identity and collisions on the original sum type. @hide:file inline_sum_fields proc(state @mut State, input ast.NodeId, fields @mut arraylist.ArrayList(ast.NodeId)) bool ! Error { if (!try type_step(state, input)) return false type :: try canonical_type(state, input) if (type == no_node) return false kind :: ast.expr_kind(&state.tree, type) if (kind == .sum_type) { sum :: ast.binary_expr(&state.tree, type) if (!try inline_sum_fields(state, sum.left, fields)) return false return try inline_sum_fields(state, sum.right, fields) } if (kind == .record_type) { for (0..ast.node_count(&state.tree)) |i| { declaration :: ast.NodeId(i) if (ast.node_class(&state.tree, declaration) != .statement or ast.stmt_kind(&state.tree, declaration) != .declaration) continue if (ast.statement_decl(&state.tree, declaration).initializer != type) continue name :: try member_name(state, declaration) for (fields.items) |previous| { field :: ast.field(&state.tree, previous) if (ast.name_id(&state.tree, field.name?) != ast.name_id(&state.tree, name)) continue return try same_type(state, field.type?, type) } field :: try ast.add_field(state.builder, ast.node_span(&state.tree, type), name, type, null) try refresh(state) try arraylist.append(fields, field) return true } return false } if (kind != .enum_type and kind != .union_type) return false if (kind == .enum_type and ast.enum_type(&state.tree, type).backing != null) return false if (kind == .union_type and !ast.union_type(&state.tree, type).auto_tag and ast.union_type(&state.tree, type).tag == null) return false count :: if (kind == .enum_type) ast.enum_member_count(&state.tree, type) else ast.record_field_count(&state.tree, type) for (0..count) |i| { if (!try type_step(state, input)) return false member :: if (kind == .enum_type) ast.enum_member(&state.tree, type, i) else ast.record_field(&state.tree, type, i) field :: ast.field(&state.tree, member) if (field.name == null) return false if (kind == .union_type and field.type == null) return false duplicate := false for (fields.items) |previous| { name :: ast.field(&state.tree, previous).name? if (ast.name_id(&state.tree, name) != ast.name_id(&state.tree, field.name?)) continue if (name != field.name?) return false duplicate = true } if (duplicate) continue payload :: if (kind == .enum_type) try builtin(state, member, .void) else field.type? generated :: try ast.add_field(state.builder, field.span, field.name, payload, null) try refresh(state) try arraylist.append(fields, generated) } return true } @hide:file inline_match_shape proc(state @mut State, annotation ast.NodeId) ast.NodeId ! Error { if (ast.expr_kind(&state.tree, annotation) != .sum_type) return annotation if (state.type_execution_depth == 0) state.type_execution_remaining = 4096 state.type_execution_depth += 1 defer state.type_execution_depth -= 1 fields := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&fields) if (!try inline_sum_fields(state, annotation, &fields)) return no_node tagged := false for (fields.items) |member| { payload :: try canonical_type(state, ast.field(&state.tree, member).type?) if (payload == no_node) return no_node resolved :: binding(state, payload) if (resolved.kind != .builtin or resolved.data.builtin != .void) tagged = true } span :: ast.node_span(&state.tree, annotation) result :: if (tagged) try ast.add_union_type(state.builder, span, null, true, fields.items) else try ast.add_enum_type(state.builder, span, null, fields.items) try refresh(state) return result } # expand dispatch, not the subject: ordinary checking/lowering retains its single # evaluation and by-value payload captures. only tags are compile-time constants. @hide:file expand_inline_match proc(state @mut State, id, subject_type ast.NodeId) void ! Error { inline_arm := no_node for (0..ast.match_arm_count(&state.tree, id)) |i| { candidate :: ast.match_arm(&state.tree, id, i) if (ast.match_arm_view(&state.tree, candidate).is_inline) { inline_arm = candidate break } } if (inline_arm == no_node) return 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 arm :: ast.match_arm_view(&state.tree, inline_arm) for (0..usize(2)) |i| { capture :: if (i == 0) arm.capture else arm.tag_capture if (capture) |name| { resolved :: binding(state, name) if (resolved.kind == .invalid) { state.evaluation_diagnostic = resolved.data.diagnostic try reject_evaluation(state, id) return } } } count :: ast.match_arm_count(&state.tree, id) if (ast.match_arm(&state.tree, id, count - 1) != inline_arm) { try reject(state, id, .unreachable_match_arm) return } subject :: ast.match_expr(&state.tree, id).subject annotation := subject_type 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 } kind :: ast.expr_kind(&state.tree, annotation) tag_only :: kind == .union_tag_type members_type := annotation if (tag_only) members_type = try canonical_type(state, ast.type_operand(&state.tree, annotation)) if (members_type != no_node) members_type = try inline_match_shape(state, members_type) if (members_type == no_node) { if (state.evaluation_diagnostic != diagnostics.no_diagnostic) try reject_evaluation(state, id) else try reject(state, id, .invalid_sum_type) return } tagged := false if (!tag_only and ast.expr_kind(&state.tree, members_type) == .union_type) { view :: ast.union_type(&state.tree, members_type) tagged = view.auto_tag or view.tag != null } if (ast.expr_kind(&state.tree, members_type) != .enum_type and !tag_only and !tagged) { try reject(state, id, .invalid_inline_match_subject) return } if (!tagged and arm.tag_capture_present) { try reject(state, id, .invalid_inline_match_capture) return } arms := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&arms) for (0..(count - 1)) |i| { prior :: ast.match_arm(&state.tree, id, i) if (ast.match_arm_view(&state.tree, prior).catch_all) { try reject(state, id, .unreachable_match_arm) return } try arraylist.append(&arms, prior) } 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 = no_node, } defer arraylist.deinit(&execution.locals) defer mem.free(state.allocator, execution.mapping) span :: ast.node_span(&state.tree, inline_arm) static_capture :: if (tagged) arm.tag_capture else arm.capture tag_type := annotation if (static_capture != null) { if (tagged) { union_view :: ast.union_type(&state.tree, members_type) if (kind != .sum_type and union_view.tag != null) tag_type = try canonical_type(state, union_view.tag?) else if (type_record_storage(state, annotation, 0)) tag_type = try metadata_tags(state, members_type) else { concrete :: try materialize(state, Value{ kind = .type, node = annotation }) tag_type = try ast.add_union_tag_type(state.builder, span, concrete.node) try refresh(state) } } concrete_tag :: try materialize(state, Value{ kind = .type, node = tag_type }) tag_type = concrete_tag.node } union_members :: tagged or tag_only members :: if (union_members) ast.record_field_count(&state.tree, members_type) else ast.enum_member_count(&state.tree, members_type) for (0..members) |i| { member :: if (union_members) ast.record_field(&state.tree, members_type, i) else ast.enum_member(&state.tree, members_type, i) field :: ast.field(&state.tree, member) if (field.name == null or (tagged and field.type == null)) { try reject(state, id, .invalid_record_field_type) return } covered := false for (arms.items) |prior| { for (0..ast.match_pattern_count(&state.tree, prior)) |j| { pattern :: ast.match_pattern(&state.tree, prior, j) if (ast.expr_kind(&state.tree, pattern) == .enum_literal and ast.name_id(&state.tree, ast.enum_literal(&state.tree, pattern)) == ast.name_id(&state.tree, field.name?)) covered = true } } if (covered) continue if (!try type_step(state, inline_arm)) { try reject_evaluation(state, id) return } execution.locals.items = execution.locals.items[..0] mem.set(execution.mapping, no_node) literal :: try ast.add_enum_literal(state.builder, span, field.name?) if (static_capture) |capture| { # runtime patterns use the union's dispatch enum, not its declared tag enum. tag_literal :: try ast.add_enum_literal(state.builder, span, field.name?) tag :: try ast.add_typed_constant(state.builder, span, tag_type, tag_literal) try refresh(state) try arraylist.append(&execution.locals, TypeLocal{ declaration = capture, value = tag, annotation = tag_type, }) } payload_capture ?ast.NodeId := null if (tagged and arm.capture != null) { payload_type :: if (is_type(state, field.type?)) field.type? else try canonical_type(state, field.type?) if (payload_type == no_node) { try reject(state, id, .invalid_record_field_type) return } if (binding(state, payload_type).kind == .builtin and binding(state, payload_type).data.builtin == .void) { value :: try ast.add_literal(state.builder, .void, span) try refresh(state) try arraylist.append(&execution.locals, TypeLocal{ declaration = arm.capture?, value = value, annotation = payload_type, }) } else { name :: arm.capture? payload_capture = try ast.add_identifier(state.builder, ast.node_span(&state.tree, name), ast.name_id(&state.tree, name)) try refresh(state) state.bindings.items[usize(payload_capture?)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = payload_capture? }, } execution.mapping[usize(name)] = payload_capture? hashmap.put(&state.capture_types, payload_capture?, payload_type) catch |err| { match err { .key_exists: unreachable else: return err } } } } body :: try type_snapshot(state, &execution, arm.body) patterns [1]ast.NodeId :: [literal] generated :: try ast.add_match_arm(state.builder, span, patterns[..], payload_capture, body, arm.expression, false) try refresh(state) try arraylist.append(&arms, generated) } if (arms.items.len == count - 1) { try reject(state, id, .unreachable_match_arm) return } replacement :: try ast.add_match(state.builder, ast.node_span(&state.tree, id), subject, arms.items) try replace(state, id, replacement) try refresh(state) } # runtime bodies share the type-producer selector, but retain ordinary runtime # parameters and control transfers in the selected arm. @hide:file specialize_match proc(state @mut State, id, statement, expected ast.NodeId) bool ! Error { explicit :: ast.expr_kind(&state.tree, ast.match_expr(&state.tree, id).subject) == .comptime_expr try expand_inline_match(state, id, no_node) if (ast.expr_kind(&state.tree, id) != .match_expr) return true subject :: ast.match_expr(&state.tree, id).subject annotation := no_node for (0..ast.match_arm_count(&state.tree, id)) |i| { arm_id :: ast.match_arm(&state.tree, id, i) arm :: ast.match_arm_view(&state.tree, arm_id) if (explicit or arm.capture == null or ast.match_pattern_count(&state.tree, arm_id) == 0) continue if (hashmap.get(&state.capture_types, arm.capture?) != null) continue if (annotation == no_node) { annotation = try expression_type(state, subject, 0) if (annotation != no_node) annotation = try inline_match_shape(state, annotation) } if (annotation == no_node) break payload := no_node if (ast.match_expr(&state.tree, id).optional_capture) { if (ast.expr_kind(&state.tree, annotation) != .optional_type) break payload = ast.type_operand(&state.tree, annotation) } else { if (ast.expr_kind(&state.tree, annotation) != .union_type) break pattern :: ast.match_pattern(&state.tree, arm_id, 0) if (ast.expr_kind(&state.tree, pattern) != .enum_literal) continue slot :: type_record_slot(&state.tree, annotation, ast.enum_literal(&state.tree, pattern)) if (slot == no_index) continue field :: ast.field(&state.tree, ast.record_field(&state.tree, annotation, slot)) if (field.type) |type| payload = type } if (payload != no_node) { hashmap.put(&state.capture_types, arm.capture?, payload) catch |err| { match err { .key_exists: unreachable else: return err } } } } try expand(state, subject) metadata :: type_record_storage(state, try expression_type(state, subject, 0), 0) if (!explicit and usize(id) < state.original_count and !metadata) return false if (!explicit and !metadata and !branch_constant(state, subject, 0)) return false state.evaluation_diagnostic = diagnostics.no_diagnostic index :: try type_match_arm(state, id) if (index == no_index) { try reject_evaluation(state, id) return true } arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, id, index)) selected := try type_match_body(state, id, arm) if (selected != no_node and !arm.expression) { selected = try ast.add_block_expr(state.builder, ast.node_span(&state.tree, selected), selected) try refresh(state) } if (selected == no_node) { try reject_evaluation(state, id) return true } try expand_expected(state, selected, expected) if (statement != no_node) { replacement :: try ast.add_expression_stmt(state.builder, selected) try replace(state, statement, replacement) } else { resolved :: binding(state, selected) try remember(state, id) ast.replace_node_from_source(state.builder, id, selected) try refresh(state) state.bindings.items[usize(id)] = resolved } try refresh(state) return true } # 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 resolved :: binding(state, id) if (resolved.kind == .builtin) return true if (resolved.kind == .declaration) { target :: resolved.data.declaration if (ast.node_class(&state.tree, target) == .function) return true if (ast.node_class(&state.tree, target) != .statement) return false declaration :: ast.statement_decl(&state.tree, target) initializer :: ast.expr_kind(&state.tree, declaration.initializer) # contextual aggregates may omit defaults with runtime calls. 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) ) } match ast.expr_kind(&state.tree, id) { .literal_int, .literal_float, .literal_bool, .literal_string, .literal_null, .literal_void, .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 or (!mem.eql(name, "minval") and !mem.eql(name, "maxval") and !mem.eql(name, "sizeof") and !mem.eql(name, "alignof"))) return false return branch_constant(state, ast.call_arg(&state.tree, id, 0), depth + 1) } .record_literal: { # typed literals may omit defaults; materialized constants are complete # contextual literals inside a typed_constant wrapper. 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 or !branch_constant(state, field.value?, depth + 1)) return false } return ast.record_literal(&state.tree, id).type == null } .array_literal: { for (0..ast.array_element_count(&state.tree, id)) |i| { if (!branch_constant(state, ast.array_element(&state.tree, id, i), depth + 1)) return false } return true } .selection: return branch_constant(state, ast.selection_expr(&state.tree, id).base, depth + 1) .index: { view :: ast.index_expr(&state.tree, id) return branch_constant(state, view.base, depth + 1) and branch_constant(state, view.index, depth + 1) } .unary: { view :: ast.unary_expr(&state.tree, id) return ( (view.operator == .negate or view.operator == .logical_not or view.operator == .bit_not) and branch_constant(state, view.operand, depth + 1) ) } .binary: { view :: ast.binary_expr(&state.tree, id) return ( branch_constant(state, view.left, depth + 1) and branch_constant(state, view.right, depth + 1) ) } .pointer_type: return branch_constant(state, ast.pointer_type(&state.tree, id).element, depth + 1) .optional_type, .alias_expr: return branch_constant(state, ast.type_operand(&state.tree, id), depth + 1) .record_type, .union_type, .enum_type, .distinct_type, .array_type, .slice_type, .function_type, .fallible_type, .sum_type, .union_tag_type: return true else: return false } } @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) try refresh(state) state.bindings.items[usize(id)] = sema.Binding{ kind = .builtin, data = sema.BindingData{ builtin = kind } } return id } @hide:file reference proc(state @mut State, at, target ast.NodeId) ast.NodeId ! Error { id :: try ast.add_identifier(state.builder, ast.node_span(&state.tree, at), strpool.no_str) try refresh(state) state.bindings.items[usize(id)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = target } } return id } @hide:file is_type proc(state @State, id ast.NodeId) bool { value :: binding(state, id) return value.kind == .builtin and value.data.builtin == .type } # recognize erased storage without evaluating dependent lengths or elaborating # bodies. pointers retain the runtime materialization boundary. @hide:file type_storage_annotation proc(tree @ast.Tree, bindings []sema.Binding, input ast.NodeId, depth usize) bool { if (depth >= 128 or usize(input) >= bindings.len) return false resolved :: bindings[usize(input)] if (resolved.kind == .builtin) return resolved.data.builtin == .type if (resolved.kind == .declaration) { declaration :: resolved.data.declaration if (ast.node_class(tree, declaration) != .statement) return false view :: ast.statement_decl(tree, declaration) return (view.mutability == .constant and type_storage_annotation(tree, bindings, view.initializer, depth + 1)) } kind :: ast.expr_kind(tree, input) if (kind == .alias_expr or kind == .optional_type) return type_storage_annotation(tree, bindings, ast.type_operand(tree, input), depth + 1) if (kind == .fallible_type or kind == .sum_type) { channels :: ast.binary_expr(tree, input) return (type_storage_annotation(tree, bindings, channels.left, depth + 1) or type_storage_annotation(tree, bindings, channels.right, depth + 1)) } if (kind == .array_type) return type_storage_annotation(tree, bindings, ast.array_type(tree, input).element, depth + 1) if (kind == .slice_type) return type_storage_annotation(tree, bindings, ast.slice_type(tree, input).element, depth + 1) if (kind != .record_type and kind != .union_type) return false for (0..ast.record_field_count(tree, input)) |i| { field :: ast.field(tree, ast.record_field(tree, input, i)) if (field.type != null and type_storage_annotation(tree, bindings, field.type?, depth + 1)) return true } return false } @hide:file storage_schema proc(tree @ast.Tree, bindings []sema.Binding, input ast.NodeId, depth usize) ast.NodeId { if (depth >= 128 or usize(input) >= bindings.len) return no_node resolved :: bindings[usize(input)] if (resolved.kind == .declaration) { target :: resolved.data.declaration if (ast.node_class(tree, target) == .function) return target if (ast.node_class(tree, target) == .statement) return storage_schema(tree, bindings, ast.statement_decl(tree, target).initializer, depth + 1) } if (ast.expr_kind(tree, input) == .alias_expr) return storage_schema(tree, bindings, ast.type_operand(tree, input), depth + 1) return input } # read only existing annotations: discovery must not specialize recursive calls. @hide:file storage_expression_annotation proc( tree @ast.Tree, bindings []sema.Binding, input ast.NodeId, depth usize, ) ast.NodeId { if (depth >= 128 or usize(input) >= bindings.len) return no_node resolved :: bindings[usize(input)] if (resolved.kind == .declaration) { target :: resolved.data.declaration class :: ast.node_class(tree, target) if (class == .parameter) return ast.param(tree, target).type if (class == .statement) { declaration :: ast.statement_decl(tree, target) if (declaration.type) |annotation| return annotation return storage_expression_annotation(tree, bindings, declaration.initializer, depth + 1) } return no_node } kind :: ast.expr_kind(tree, input) if (kind == .typed_constant) return ast.typed_constant(tree, input).type if (kind == .union_constant) return ast.union_constant(tree, input).type if (kind == .slice_constant) return ast.slice_constant(tree, input).type if (kind == .record_literal) { if (ast.record_literal(tree, input).type) |type| return type return no_node } if (kind == .call) { callee :: storage_schema(tree, bindings, ast.call_expr(tree, input).callee, depth + 1) if (callee != no_node and ast.node_class(tree, callee) == .function) return ast.function_decl(tree, callee).return_type return no_node } base := no_node if (kind == .selection) base = ast.selection_expr(tree, input).base else if (kind == .index) base = ast.index_expr(tree, input).base else if (kind == .slice) base = ast.slice_expr(tree, input).base else if (kind == .unary) base = ast.unary_expr(tree, input).operand else if (kind == .try_expr) base = ast.type_operand(tree, input) else if (kind == .catch_expr) base = ast.catch_expr(tree, input).operand else if (kind == .comptime_expr) return storage_expression_annotation(tree, bindings, ast.comptime_operand(tree, input), depth + 1) if (base == no_node) return no_node annotation :: storage_schema(tree, bindings, storage_expression_annotation(tree, bindings, base, depth + 1), depth + 1) if (annotation == no_node) return no_node shape :: ast.expr_kind(tree, annotation) if (kind == .slice) return annotation if (kind == .try_expr or kind == .catch_expr) return if (shape == .fallible_type) ast.binary_expr(tree, annotation).left else no_node if (kind == .unary) return if (shape == .optional_type and ast.unary_expr(tree, input).operator == .unwrap) ast.type_operand(tree, annotation) else no_node if (kind == .index) { if (shape == .array_type) return ast.array_type(tree, annotation).element if (shape == .slice_type) return ast.slice_type(tree, annotation).element return no_node } if (shape != .record_type and shape != .union_type) return no_node member :: ast.selection_expr(tree, input).member slot :: type_record_slot(tree, annotation, member) if (slot == no_index) return no_node if (ast.field(tree, ast.record_field(tree, annotation, slot)).type) |type| return type return no_node } @hide:file type_storage_yields proc( tree @ast.Tree, bindings []sema.Binding, id ast.NodeId, scalar_type bool, depth usize, ) bool { if (depth >= 128) return false class :: ast.node_class(tree, id) if (class != .block and class != .statement) return false if (class == .statement and ast.stmt_kind(tree, id) == .yield_stmt) { value :: ast.yield_stmt(tree, id).value return value != null and type_storage_initializer(tree, bindings, value?, scalar_type, depth + 1) } for (0..ast.child_count(tree, id)) |i| { if (type_storage_yields(tree, bindings, ast.child(tree, id, i), scalar_type, depth + 1)) return true } return false } # discover inferred storage without elaborating calls or executing initializers. # immutable scalar type bindings remain aliases in ordinary runtime procedures. @hide:file type_storage_initializer proc( tree @ast.Tree, bindings []sema.Binding, input ast.NodeId, scalar_type bool, depth usize, ) bool { if (depth >= 128 or usize(input) >= bindings.len) return false annotation :: storage_schema(tree, bindings, storage_expression_annotation(tree, bindings, input, depth + 1), depth + 1) if (annotation != no_node) { if (!scalar_type and bindings[usize(annotation)].kind == .builtin and bindings[usize(annotation)].data.builtin == .type) return false return type_storage_annotation(tree, bindings, annotation, depth + 1) } resolved :: bindings[usize(input)] if (resolved.kind == .builtin) return scalar_type if (resolved.kind == .declaration) { target :: resolved.data.declaration class :: ast.node_class(tree, target) if (class == .statement) { declaration :: ast.statement_decl(tree, target) if (declaration.type != null) return false return type_storage_initializer(tree, bindings, declaration.initializer, scalar_type, depth + 1) } return false } kind :: ast.expr_kind(tree, input) match kind { .record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type, .enum_type, .union_type, .union_tag_type, .distinct_type, .fallible_type, .sum_type, .alias_expr: return scalar_type .record_literal: { for (0..ast.record_field_count(tree, input)) |i| { field :: ast.field(tree, ast.record_field(tree, input, i)) if (field.value != null and type_storage_initializer(tree, bindings, field.value?, true, depth + 1)) return true } return false } .array_literal: { for (0..ast.array_element_count(tree, input)) |i| { if (type_storage_initializer(tree, bindings, ast.array_element(tree, input, i), true, depth + 1)) return true } return false } .comptime_expr: return type_storage_initializer(tree, bindings, ast.comptime_operand(tree, input), scalar_type, depth + 1) .match_expr: { for (0..ast.match_arm_count(tree, input)) |i| { arm :: ast.match_arm_view(tree, ast.match_arm(tree, input, i)) if (arm.expression) { if (type_storage_initializer(tree, bindings, arm.body, scalar_type, depth + 1)) return true } else if (type_storage_yields(tree, bindings, arm.body, scalar_type, depth + 1)) return true } return false } .block_expr: return type_storage_yields(tree, bindings, ast.block_expr(tree, input), scalar_type, depth + 1) .builtin_call: { name :: ast.literal_text(tree, ast.builtin_call(tree, input).name) if (mem.eql(name, "typeinfo")) return true if (mem.eql(name, "struct_type")) return scalar_type if (mem.eql(name, "some") and ast.call_arg_count(tree, input) == 1) return type_storage_initializer(tree, bindings, ast.call_arg(tree, input, 0), true, depth + 1) return false } else: return false } } @hide:file type_local_storage proc(tree @ast.Tree, bindings []sema.Binding, id ast.NodeId, depth usize) bool { if (depth >= 128) return false if (ast.node_class(tree, id) == .statement and ast.stmt_kind(tree, id) == .declaration) { declaration :: ast.statement_decl(tree, id) if (declaration.type) |annotation| { if (type_storage_annotation(tree, bindings, annotation, 0)) return true } else if (type_storage_initializer(tree, bindings, declaration.initializer, declaration.mutability == .variable, 0)) return true } for (0..ast.child_count(tree, id)) |i| { if (type_local_storage(tree, bindings, ast.child(tree, id, i), depth + 1)) return true } 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 } 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 inference_contextual proc(state @State, id ast.NodeId) bool { kind :: ast.expr_kind(&state.tree, id) if (kind == .literal_null or kind == .literal_string) return true if (ast.expr_kind(&state.tree, id) == .record_literal) { annotation :: ast.record_literal(&state.tree, id).type return annotation == null or ast.expr_kind(&state.tree, annotation?) == .enum_literal } return flexible(state, id) or contextual_literal(state, id) } @hide:file inference_phase proc(state @State, id ast.NodeId) usize { if (inference_contextual(state, id)) return 2 if (ast.expr_kind(&state.tree, id) == .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 and template(state, callee.data.declaration) and ast.call_arg_count(&state.tree, id) != ast.function_param_count(&state.tree, callee.data.declaration) ) return 1 } return 0 } @hide:file annotation_ready proc( state @State, function, id ast.NodeId, arguments []Value, depth usize, ) bool { if (depth >= 128) return false resolved :: binding(state, id) if (resolved.kind == .declaration) { index :: parameter_index(state, function, resolved.data.declaration) if (index != no_index) return arguments[index].kind != .invalid } for (0..ast.child_count(&state.tree, id)) |i| { if (!annotation_ready(state, function, ast.child(&state.tree, id, i), arguments, depth + 1)) return false } return true } @hide:file specialize_call proc(state @mut State, call, function, expected ast.NodeId) void ! Error { if (state.depth >= 128 or state.instances.items.len >= 4096) { try reject(state, call, .specialization_limit) return } state.depth += 1 defer state.depth -= 1 count :: ast.function_param_count(&state.tree, 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 (!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 (metadata_result or supplied < runtime or supplied > count)) { try reject(state, call, .argument_count_mismatch) return } arguments :: try mem.alloc(Value, state.allocator, count) defer mem.free(state.allocator, arguments) mem.set(arguments, Value{}) 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 (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 !metadata_result) { if (comptime_position >= supplied_comptime) continue comptime_position += 1 } argument :: ast.call_arg(&state.tree, call, position) position += 1 if (type_result or view.comptime) { context_annotation :: try argument_annotation(state, function, view.type, arguments) try expand_expected(state, argument, context_annotation) state.evaluation_diagnostic = diagnostics.no_diagnostic annotation := view.type contextual := contextual_literal(state, argument) if (!contextual and !is_type(state, annotation)) { type :: try canonical_type(state, annotation) contextual = type != no_node and ast.expr_kind(&state.tree, type) == .fallible_type } if (contextual or type_record_storage(state, annotation, 0)) annotation = try argument_annotation(state, function, view.type, arguments) arguments[i] = try evaluate_expected(state, argument, annotation) if (arguments[i].kind == .invalid) { try reject_evaluation(state, call) return } if (is_type(state, view.type) != (arguments[i].kind == .type)) { try reject(state, call, .type_mismatch) return } } if (!view.comptime and !metadata_result) try arraylist.append(&runtime_arguments, argument) } if (!explicit) { # rigid arguments constrain first; result context then types nested generic # calls before flexible literals choose their default widths. for (0..usize(3)) |phase| { if (phase == 1 and expected != no_node) { result :: ast.function_decl(&state.tree, function).return_type if (!try infer_result(state, function, result, expected, arguments)) { try reject(state, call, .cannot_infer_generic) return } } position = 0 for (0..count) |i| { parameter :: ast.function_param(&state.tree, function, i) view :: ast.param(&state.tree, parameter) if (view.comptime) continue argument :: runtime_arguments.items[position] position += 1 contextual :: inference_contextual(state, argument) if (inference_phase(state, argument) != phase) continue if (annotation_ready(state, function, view.type, arguments, 0)) { annotation :: try argument_annotation(state, function, view.type, arguments) try expand_expected(state, argument, annotation) } actual :: try expression_type(state, argument, 0) # contextual literals need not have an independent type; the rewritten # call checks them against the substituted signature after inference. if ( (actual == no_node or !try infer(state, function, view.type, actual, arguments, contextual)) and !contextual ) { try reject(state, call, .cannot_infer_generic) return } } } } for (0..count) |i| { parameter :: ast.function_param(&state.tree, function, i) if (ast.param(&state.tree, parameter).comptime and arguments[i].kind == .invalid) { try reject(state, call, .cannot_infer_generic) return } } position = 0 for (0..count) |i| { view :: ast.param(&state.tree, ast.function_param(&state.tree, function, i)) if (view.comptime or type_result) continue argument :: runtime_arguments.items[position] position += 1 if (state.expanded.items[usize(argument)] != 0) continue annotation :: try argument_annotation(state, function, view.type, arguments) try expand_expected(state, argument, annotation) } 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)) } head := no_index if (hashmap.get(&state.cache, key)) |found| head = found current := head instance := no_index while (current != no_index) { entry :: state.instances.items[current] 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])) { equal = false break } } } if (equal) { instance = current break } current = entry.next } if (instance == no_index) { for (0..count) |i| { if (arguments[i].kind != .invalid) arguments[i] = try materialize(state, arguments[i]) } 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, demanded = demanded, }) catch |err| { mem.free(state.allocator, values) return err } # cache heads are replaced without reallocating a second entry for the same hash. if (head != no_index) { for (state.cache.entries) |@entry| { if (entry.hash != 0 and entry.key == key) { entry.value = instance break } } } else { hashmap.put(&state.cache, key, instance) catch |err| { match err { .key_exists: unreachable else: return err } } } try instantiate(state, instance, call) if (binding(state, call).kind == .invalid) { state.instances.items[instance].diagnostic = binding(state, call).data.diagnostic return } } diagnostic :: state.instances.items[instance].diagnostic if (diagnostic != diagnostics.no_diagnostic) { 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 } } return } target :: state.instances.items[instance].declaration if (target == no_node and state.instances.items[instance].value == no_node) { try reject(state, call, .specialization_cycle) return } if (type_result or supplied_comptime != 0) { signature :: state.instances.items[instance].signature position = 0 comptime_position = 0 for (0..count) |i| { parameter :: ast.function_param(&state.tree, signature, i) view :: ast.param(&state.tree, parameter) if (view.comptime and !metadata_result) { if (comptime_position >= supplied_comptime) continue comptime_position += 1 } original :: ast.call_arg(&state.tree, call, position) position += 1 if ( (!type_result and !view.comptime) or is_type(state, view.type) or type_record_storage(state, view.type, 0) ) continue constraint :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, original), view.type, original) try refresh(state) try arraylist.append(&state.rewrites.constraints, constraint) } } replacement := state.instances.items[instance].value if (replacement == no_node) { callee :: try reference(state, call, target) if (state.instances.items[instance].type != no_node) replacement = try ast.add_type_operand(state.builder, ast.node_span(&state.tree, call), callee, true) else replacement = try ast.add_call(state.builder, ast.node_span(&state.tree, call), callee, runtime_arguments.items) } try replace(state, call, replacement) try refresh(state) } @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) or state.instances.items[index].demanded count :: state.original_count clone_start :: ast.node_count(&state.tree) mapping :: state.mapping mem.set(mapping, no_node) parameters :: ast.function_param_count(&state.tree, function) for (0..parameters) |i| { parameter :: ast.function_param(&state.tree, function, i) if (!type_result and !ast.param(&state.tree, parameter).comptime) continue value :: state.instances.items[index].arguments[i] replacement := value.node annotation :: ast.param(&state.tree, parameter).type if (!is_type(state, annotation)) { concrete_type :: try ast.clone_subtree(state.builder, annotation, mapping) replacement = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, parameter), concrete_type, value.node) try refresh(state) } for (0..count) |n| { resolved :: binding(state, ast.NodeId(n)) if ( resolved.kind == .declaration and resolved.data.declaration == parameter and ast.node_class(&state.tree, ast.NodeId(n)) == .expression ) mapping[n] = replacement } } clone :: try ast.clone_subtree(state.builder, function, mapping) try refresh(state) # copy original resolution, remapping references to cloned locals and runtime parameters. for (0..count) |i| { target :: mapping[i] if (target == no_node or usize(target) < clone_start) continue if (ast.node_class(&state.tree, target) == .expression and ast.expr_kind(&state.tree, target) == .typed_constant) continue value := state.bindings.items[i] if (value.kind == .declaration) { original :: value.data.declaration if (original != function and usize(original) < count and mapping[usize(original)] != no_node) value.data.declaration = mapping[usize(original)] } state.bindings.items[usize(target)] = value } state.instances.items[index].signature = try ast.copy_node(state.builder, clone) try refresh(state) view :: ast.function_decl(&state.tree, clone) for (0..parameters) |i| { parameter :: ast.function_param(&state.tree, clone, i) annotation :: ast.param(&state.tree, parameter).type if ((type_result or ast.param(&state.tree, parameter).comptime) and !is_type(state, annotation)) { if (!try valid_value(state, annotation, state.instances.items[index].arguments[i])) { value :: state.instances.items[index].arguments[i] code :: if (value.kind == .integer and try integer_type(state, annotation)) diagnostics.Code.literal_out_of_range else diagnostics.Code.type_mismatch try reject(state, call, code) return } } } 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 state.evaluation_diagnostic = diagnostics.no_diagnostic constraints_start :: state.rewrites.constraints.items.len returned :: try return_type_value(state, view.body, ast.NodeId(clone_start), view.return_type) if (returned == no_node) { try reject_type_result(state, call, returned, constraints_start) return } if (!is_type(state, view.return_type)) { state.instances.items[index].value = returned constraint_count :: state.rewrites.constraints.items.len for (constraints_start..constraint_count) |i| { constraint :: state.rewrites.constraints.items[i] if (!try expand_type_matches(state, constraint)) { try reject_type_result(state, call, returned, constraints_start) return } try expand(state, constraint) } return } result := returned kind :: ast.expr_kind(&state.tree, returned) if (kind != .record_type and kind != .union_type and kind != .enum_type and kind != .distinct_type) { result = try ast.add_type_operand(state.builder, ast.node_span(&state.tree, returned), returned, true) try refresh(state) } declaration :: try ast.add_statement_decl(state.builder, view.span, view.name, ast.name_id(&state.tree, function), null, .constant, result) try refresh(state) state.instances.items[index].declaration = declaration state.instances.items[index].type = returned state.bindings.items[usize(declaration)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = declaration } } try arraylist.append(&state.declarations, declaration) if (!try expand_type_matches(state, declaration)) { try reject_type_result(state, call, result, constraints_start) return } try expand(state, declaration) constraint_count :: state.rewrites.constraints.items.len for (constraints_start..constraint_count) |i| { constraint :: state.rewrites.constraints.items[i] if (!try expand_type_matches(state, constraint)) { try reject_type_result(state, call, result, constraints_start) return } try expand(state, constraint) } } else { runtime := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&runtime) for (0..parameters) |i| { parameter :: ast.function_param(&state.tree, clone, i) if (!ast.param(&state.tree, parameter).comptime) try arraylist.append(&runtime, parameter) } concrete :: try ast.add_function_decl(state.builder, view.span, view.name, ast.name_id(&state.tree, function), view.return_type, runtime.items, view.body) ast.replace_node(state.builder, clone, concrete) try refresh(state) state.instances.items[index].declaration = clone state.bindings.items[usize(clone)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = clone } } try arraylist.append(&state.declarations, clone) try expand(state, clone) } } @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 } } } # failed deferred elaboration must not leave unchecked type expressions in the package. @hide:file reject_type_result proc(state @mut State, call, result ast.NodeId, constraints_start usize) void ! Error { try reject_evaluation(state, call) state.evaluation_diagnostic = binding(state, call).data.diagnostic if (result != no_node) try reject_evaluation(state, result) for (constraints_start..(state.rewrites.constraints.items.len)) |i| try reject_evaluation(state, state.rewrites.constraints.items[i]) } @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 if (ast.node_class(&state.tree, id) == .expression and ast.expr_kind(&state.tree, id) == .typed_constant) { if (!try type_array_initialized(state, id, id)) return false } resolved :: binding(state, id) if (resolved.kind == .declaration) { target :: resolved.data.declaration if ((flags[usize(target)] & 8) == 0 and (flags[usize(target)] & 2) == 0) { class :: ast.node_class(&state.tree, target) if (class == .parameter) return false if (class == .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 evaluation_owned proc(state @State, id ast.NodeId, flags []mut u8) void { if ((flags[usize(id)] & 8) != 0) return flags[usize(id)] = flags[usize(id)] | 8 for (0..ast.child_count(&state.tree, id)) |i| evaluation_owned(state, ast.child(&state.tree, id, i), flags) } @hide:file contextual_literal proc(state @State, id ast.NodeId) bool { kind :: ast.expr_kind(&state.tree, id) if (kind == .builtin_call) return mem.eql(ast.literal_text(&state.tree, ast.builtin_call(&state.tree, id).name), "some") if (kind == .unary) return ast.unary_expr(&state.tree, id).operator == .bit_not and flexible(state, id) if (kind == .binary) { operator :: ast.binary_expr(&state.tree, id).operator return ( (operator == .bit_and or operator == .bit_or or operator == .bit_xor or operator == .shift_left or operator == .shift_right or operator == .shift_left_saturating) and flexible(state, id) ) } return kind == .record_literal or kind == .array_literal or kind == .enum_literal or kind == .block_expr } @hide:file annotation_generic proc(state @State, function, id ast.NodeId, depth usize) bool { if (depth >= 128) return true resolved :: binding(state, id) if ( resolved.kind == .declaration and parameter_index(state, function, resolved.data.declaration) != no_index ) return true for (0..ast.child_count(&state.tree, id)) |i| { if (annotation_generic(state, function, ast.child(&state.tree, id, i), depth + 1)) return true } return false } @hide:file argument_annotation proc( state @mut State, function, annotation ast.NodeId, arguments []Value, ) ast.NodeId ! Error { if (!annotation_generic(state, function, annotation, 0)) return annotation mapping :: try mem.alloc(ast.NodeId, state.allocator, state.original_count) defer mem.free(state.allocator, mapping) mem.set(mapping, no_node) for (0..ast.function_param_count(&state.tree, function)) |i| { if (arguments[i].kind == .invalid) continue parameter :: ast.function_param(&state.tree, function, i) value := try materialize(state, arguments[i]) if (value.kind != .type and value.annotation != no_node) { value.node = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, value.node), value.annotation, value.node) try refresh(state) } for (0..mapping.len) |n| { resolved :: binding(state, ast.NodeId(n)) if ( resolved.kind == .declaration and resolved.data.declaration == parameter and ast.node_class(&state.tree, ast.NodeId(n)) == .expression ) mapping[n] = value.node } } start :: ast.node_count(&state.tree) result :: try ast.clone_subtree(state.builder, annotation, mapping) try refresh(state) for (0..mapping.len) |i| { target :: mapping[i] if (target != no_node and usize(target) >= start) state.bindings.items[usize(target)] = state.bindings.items[i] } return result } @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 state.type_execution_depth += 1 defer state.type_execution_depth -= 1 constant :: try type_execution_value(state, id, annotation) if (constant == no_node or !try type_array_initialized(state, constant, id)) return Value{} value :: ast.typed_constant(&state.tree, constant) return Value{ kind = .aggregate, node = value.value, annotation = value.type } } if (!is_type(state, annotation)) { type :: try canonical_type(state, annotation) if (type != no_node and ast.expr_kind(&state.tree, type) == .array_type and !try type_array_initialized(state, id, id)) return Value{} if (type != no_node and (contextual_literal(state, id) or ast.expr_kind(&state.tree, id) == .block_expr or ast.expr_kind(&state.tree, id) == .slice or ast.expr_kind(&state.tree, type) == .fallible_type)) return try evaluate_ir(state, id, type) } return try evaluate(state, id) } # raw union snapshots cannot distinguish addresses from overlapping integer bits. # require every view to be address-free before retaining the complete representation. @hide:file union_bytes_materializable proc(checked @typecheck.Result, type typecheck.Type, depth usize) bool { if (depth >= 128) return false match type.kind { .int, .i8, .i16, .i32, .isize, .u8, .u16, .u32, .u64, .usize, .float, .bool, .void, .enumeration: return true .array, .optional: { return union_bytes_materializable(checked, typecheck.array_type(checked, type.array_type).element, depth + 1) } .fallible: { channels :: typecheck.array_type(checked, type.array_type) return (union_bytes_materializable(checked, channels.element, depth + 1) and union_bytes_materializable(checked, channels.error_type, depth + 1)) } .record: { for (typecheck.record_type(checked, type.record_type).fields) |field| { if (!union_bytes_materializable(checked, field.type, depth + 1)) return false } return true } else: return false } } # rebuild exact slice backing annotations without retaining temporary checker IDs. @hide:file serialized_type proc( state @mut State, checked @typecheck.Result, type typecheck.Type, span ast.Span, depth usize, ) ast.NodeId ! Error { if (depth >= 128) return no_node source := type.nominal if (source == no_node and type.kind == .record) { record :: typecheck.record_type(checked, type.record_type) source = if (record.declaration != no_node) record.declaration else record.source } if (source == no_node and type.kind == .enumeration) source = typecheck.enum_type(checked, type.enum_type).declaration if (source == no_node and type.kind == .function) source = type.function if (source != no_node) { if (ast.node_class(&state.tree, source) == .function) { params := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(¶ms) for (0..ast.function_param_count(&state.tree, source)) |i| try arraylist.append(¶ms, ast.function_param(&state.tree, source, i)) source = try ast.add_function_type(state.builder, span, ast.callable_result(&state.tree, source), params.items) try refresh(state) } return (try materialize(state, Value{ kind = .type, node = source })).node } result := no_node if (type.kind == .array or type.kind == .slice or type.kind == .optional or type.kind == .fallible) { descriptor :: typecheck.array_type(checked, type.array_type) child :: try serialized_type(state, checked, descriptor.element, span, depth + 1) if (child == no_node) return no_node if (type.kind == .array) { count :: try ast.add_integer(state.builder, span, u64(descriptor.len)) result = try ast.add_array_type(state.builder, span, count, child, descriptor.mutable) } else if (type.kind == .slice) { sentinel ?ast.NodeId := null if (descriptor.sentinel) |value| { unsigned :: (descriptor.element.kind == .u8 or descriptor.element.kind == .u16 or descriptor.element.kind == .u32 or descriptor.element.kind == .u64 or descriptor.element.kind == .usize) negative :: value < 0 and !unsigned magnitude :: if (negative) u64(-(value + 1)) + 1 else ptrcast!(@u64, &value)^ literal := try ast.add_integer(state.builder, span, magnitude) if (negative) literal = try ast.add_unary(state.builder, span, .negate, literal) sentinel = literal } result = try ast.add_slice_type(state.builder, span, child, descriptor.mutable, sentinel) } else if (type.kind == .optional) result = try ast.add_optional_type(state.builder, span, child) else { error_type :: try serialized_type(state, checked, descriptor.error_type, span, depth + 1) if (error_type == no_node) return no_node result = try ast.add_binary_type(state.builder, span, child, error_type, true) } } else if (type.kind == .pointer) { descriptor :: typecheck.pointer_type(checked, type.pointer_type) child :: try serialized_type(state, checked, descriptor.element, span, depth + 1) if (child == no_node) return no_node result = try ast.add_pointer_type(state.builder, span, child, descriptor.mutable, descriptor.many) } else { kind sema.BuiltinType :: match type.kind { .int: .i64 .i8: .i8 .i16: .i16 .i32: .i32 .isize: .isize .u8: .u8 .u16: .u16 .u32: .u32 .u64: .u64 .usize: .usize .float: .f64 .bool: .bool .string: .string .void: .void .anyopaque: .anyopaque else: return no_node } result = try ast.add_identifier(state.builder, span, strpool.no_str) try refresh(state) state.bindings.items[usize(result)] = sema.Binding{ kind = .builtin, data = sema.BindingData{ builtin = kind } } } try refresh(state) return result } # values leaving temporary evaluation must own no interpreter addresses or module IDs. # snapshot readonly storage and rebuild source literals/references before releasing their backing. @hide:file serialize_value proc( state @mut State, module @ir.Module, checked @typecheck.Result, outcome @interpreter.Outcome, functions []ast.NodeId, type typecheck.Type, value ir.Value, span ast.Span, remaining @mut usize, ) ast.NodeId ! Error { if (remaining^ == 0 or state.depth >= 128) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_execution_limit, span) return no_node } remaining^ -= 1 state.depth += 1 defer state.depth -= 1 uninitialized := value.uninitialized if (ir.aggregate(value.type)) { if (module.arrays.items[usize(value.array_type)].is_union) uninitialized = ir.union_tag(module, value).uninitialized else if (type.kind == .optional) 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 } result := no_node if (type.kind == .slice) { slice :: typecheck.array_type(checked, type.array_type) if (slice.mutable) return no_node pointer :: ir.aggregate_item(module, value, 0) length :: ir.aggregate_item(module, value, 1) if (pointer.uninitialized or length.uninitialized) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span) return no_node } if (slice.element.kind != .u8 or slice.element.nominal != no_node) { shape :: module.arrays.items[usize(value.array_type)] count :: usize(length.data.unsigned) extra :: if (slice.sentinel != null) usize(1) else usize(0) if (count > remaining^ or extra > remaining^ - count) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_execution_limit, span) return no_node } if (count != 0 and interpreter.evaluation_element(outcome, module, pointer, count - 1, shape.element, shape.element_array) == null) return no_node elements := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&elements) for (0..(count + extra)) |index| { borrowed :: interpreter.evaluation_element(outcome, module, pointer, index, shape.element, shape.element_array) if (borrowed == null) return no_node child := borrowed? # recursive slice reads may overwrite the accessor's raw-view scratch. scratch_backed :: ir.aggregate(child.type) and child.raw owned []mut ir.Value := &[] if (scratch_backed) { owned = try mem.alloc(ir.Value, state.allocator, child.data.elements.len) memcopy!(owned, child.data.elements) child.data = ir.ValueData{ elements = owned } } defer if (scratch_backed) mem.free(state.allocator, owned) node :: try serialize_value(state, module, checked, outcome, functions, slice.element, child, span, remaining) if (node == no_node) return no_node if (index == count and slice.sentinel != null) { # checker sentinels retain full-width integer bits, including u64 maxima. if (child.data.integer != slice.sentinel?) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .sentinel_mismatch, span) return no_node } } try arraylist.append(&elements, node) } annotation :: try serialized_type(state, checked, type, span, 0) if (annotation == no_node) return no_node child_type :: if (type.nominal == no_node) ast.slice_type(&state.tree, annotation).element else try serialized_type(state, checked, slice.element, span, 0) if (child_type == no_node) return no_node length_node :: try ast.add_integer(state.builder, span, u64(count + extra)) array_type :: try ast.add_array_type(state.builder, span, length_node, child_type, false) literal :: try ast.add_array_literal(state.builder, span, elements.items) backing :: try ast.add_typed_constant(state.builder, span, array_type, literal) result = try ast.add_slice_constant(state.builder, span, annotation, backing) try refresh(state) return result } if ( length.data.unsigned != 0 and interpreter.evaluation_byte(outcome, module, pointer, usize(length.data.unsigned - 1)) == null ) return no_node nonzero_sentinel :: slice.sentinel != null and slice.sentinel? != 0 length_bytes :: usize(length.data.unsigned) if (nonzero_sentinel and length_bytes == maxval!(usize)) return no_node extra :: if (nonzero_sentinel) usize(1) else usize(0) bytes :: try mem.alloc(u8, state.allocator, length_bytes + extra) defer mem.free(state.allocator, bytes) for (0..length_bytes) |index| { byte :: interpreter.evaluation_byte(outcome, module, pointer, index) if (byte == null) return no_node if (byte?.uninitialized) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span) return no_node } bytes[index] = u8(byte?.data.unsigned) } if (slice.sentinel != null) { sentinel :: interpreter.evaluation_byte(outcome, module, pointer, length_bytes) if (sentinel == null) return no_node if (sentinel?.uninitialized) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span) return no_node } if (sentinel?.data.unsigned != u64(slice.sentinel?)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .sentinel_mismatch, span) return no_node } if (nonzero_sentinel) bytes[length_bytes] = u8(slice.sentinel?) } result = try ast.add_string(state.builder, span, bytes) if (nonzero_sentinel) { end :: try ast.add_integer(state.builder, span, u64(length_bytes)) sentinel :: try ast.add_integer(state.builder, span, u64(slice.sentinel?)) result = try ast.add_slice(state.builder, span, result, null, end, sentinel) } } else if (type.kind == .array) { array :: typecheck.array_type(checked, type.array_type) elements := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&elements) for (0..array.len) |i| { child :: ir.aggregate_item(module, value, i) node :: try serialize_value( state, module, checked, outcome, functions, array.element, child, span, remaining, ) if (node == no_node) return no_node try arraylist.append(&elements, node) } result = try ast.add_array_literal(state.builder, span, elements.items) } else if (type.kind == .record) { record :: typecheck.record_type(checked, type.record_type) if (record.is_union and record.tag_type.kind != .enumeration) { if (!union_bytes_materializable(checked, type, 0)) return no_node bytes :: try mem.alloc(u8, state.allocator, value.data.elements.len) defer mem.free(state.allocator, bytes) for (value.data.elements) |byte, index| { if (byte.uninitialized) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span) return no_node } bytes[index] = u8(byte.data.unsigned) } annotation :: if (type.nominal != no_node) type.nominal else if (record.declaration != no_node) record.declaration else record.source concrete :: try materialize(state, Value{ kind = .type, node = annotation }) literal :: try ast.add_string(state.builder, span, bytes) result = try ast.add_union_constant(state.builder, span, concrete.node, literal) try refresh(state) return result } fields := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&fields) for (record.fields) |field, i| { if (record.is_union and !ir.union_active(module, value, i)) continue if (record.is_union and field.type.kind == .void) { name :: try member_name(state, field.name?) result = try ast.add_enum_literal(state.builder, span, name) try refresh(state) return result } child :: ir.aggregate_item(module, value, i) node :: try serialize_value(state, module, checked, outcome, functions, field.type, child, span, remaining) if (node == no_node) return no_node name := field.name if (name != null) name = try member_name(state, name?) generated :: try ast.add_field(state.builder, span, name, null, node) try refresh(state) try arraylist.append(&fields, generated) } result = try ast.add_record_literal(state.builder, span, null, fields.items) } else if (type.kind == .fallible) { container :: typecheck.array_type(checked, type.array_type) failed :: ir.union_tag(module, value).data.integer == 1 index :: if (failed) usize(1) else usize(0) channel :: if (failed) container.error_type else container.element child :: ir.aggregate_item(module, value, index) result = try serialize_value(state, module, checked, outcome, functions, channel, child, span, remaining) if (result == no_node) return no_node # keep named success/error types distinct even when their members overlap. annotation := no_node if (channel.nominal != no_node) annotation = channel.nominal else if (channel.kind == .enumeration) annotation = typecheck.enum_type(checked, channel.enum_type).declaration else if (channel.kind == .record) annotation = typecheck.record_type(checked, channel.record_type).declaration if (annotation != no_node) { concrete :: try materialize(state, Value{ kind = .type, node = annotation }) result = try ast.add_typed_constant(state.builder, span, concrete.node, result) } } else if (type.kind == .enumeration) { enumeration :: typecheck.enum_type(checked, type.enum_type) negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0 magnitude :: if (negative) u64(-(value.data.integer + 1)) + 1 else value.data.unsigned for (enumeration.members) |member| { if (member.magnitude == magnitude and member.negative == negative) { name :: try member_name(state, member.name) result = try ast.add_enum_literal(state.builder, span, name) break } } } else if (type.kind == .optional) { container :: typecheck.array_type(checked, type.array_type) if (container.element.kind == .pointer or container.element.kind == .function) { absent :: if (container.element.kind == .function) value.data.function == maxval!(ir.FunctionId) else value.data.unsigned == maxval!(u64) if (absent) result = try ast.add_literal(state.builder, .null, span) else result = try serialize_value(state, module, checked, outcome, functions, container.element, value, span, remaining) } else if (!ir.aggregate_item(module, value, 0).data.boolean) { result = try ast.add_literal(state.builder, .null, span) } else { child :: ir.aggregate_item(module, value, 1) payload :: try serialize_value(state, module, checked, outcome, functions, container.element, child, span, remaining) if (payload == no_node) return no_node name :: try ast.add_generated_identifier(state.builder, span, try strpool.intern(state.strings, "some"), "some") arguments [1]ast.NodeId :: [payload] result = try ast.add_builtin_call(state.builder, span, name, arguments[..]) } } else if (type.kind == .function) { index :: usize(value.data.function) if (index >= functions.len or functions[index] == no_node) return no_node result = try reference(state, functions[index], functions[index]) } else if (ir.integer_bits(value.type) != 0 and type.kind != .pointer) { negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0 magnitude :: if (negative) u64(-(value.data.integer + 1)) + 1 else value.data.unsigned result = try ast.add_integer(state.builder, span, magnitude) if (negative) result = try ast.add_unary(state.builder, span, .negate, result) } else if (type.kind == .float) { result = try ast.add_float(state.builder, span, value.data.real) } else if (type.kind == .bool) { result = try ast.add_boolean(state.builder, span, value.data.boolean) } else if (type.kind == .string) { result = try ast.add_string(state.builder, span, ir.string_bytes(module, value.data.string)) } else if (type.kind == .void) { result = try ast.add_literal(state.builder, .void, span) } try refresh(state) return result } @hide:file constant_equal proc(state @mut State, left, right ast.NodeId) bool ! Error { kind :: ast.expr_kind(&state.tree, left) if (kind != ast.expr_kind(&state.tree, right)) return false if (kind == .literal_void) return true if (kind == .typed_constant or kind == .union_constant or kind == .slice_constant) { a :: if (kind == .union_constant) ast.union_constant(&state.tree, left) else if (kind == .slice_constant) ast.slice_constant(&state.tree, left) else ast.typed_constant(&state.tree, left) b :: if (kind == .union_constant) ast.union_constant(&state.tree, right) else if (kind == .slice_constant) ast.slice_constant(&state.tree, right) else ast.typed_constant(&state.tree, right) return try same_type_context(state, a.type, b.type, true) and try constant_equal(state, a.value, b.value) } if (kind == .slice) { a :: ast.slice_expr(&state.tree, left) b :: ast.slice_expr(&state.tree, right) # serialized nonzero-sentinel views have a canonical owned string and explicit end. return ( try constant_equal(state, a.base, b.base) and try constant_equal(state, a.end?, b.end?) and try constant_equal(state, a.sentinel?, b.sentinel?) ) } if (kind == .array_literal) { count :: ast.array_element_count(&state.tree, left) if (count != ast.array_element_count(&state.tree, right)) return false for (0..count) |i| { if (!try constant_equal(state, ast.array_element(&state.tree, left, i), ast.array_element(&state.tree, right, i))) return false } return true } if (kind == .record_literal) { count :: ast.record_field_count(&state.tree, left) if (count != ast.record_field_count(&state.tree, right)) return false for (0..count) |i| { a :: ast.field(&state.tree, ast.record_field(&state.tree, left, i)) b :: ast.field(&state.tree, ast.record_field(&state.tree, right, i)) if ((a.name == null) != (b.name == null)) return false if (a.name != null and ast.name_id(&state.tree, a.name?) != ast.name_id(&state.tree, b.name?)) return false if (!try constant_equal(state, a.value?, b.value?)) return false } return true } if (kind == .builtin_call and contextual_literal(state, left) and contextual_literal(state, right)) { return try constant_equal(state, ast.call_arg(&state.tree, left, 0), ast.call_arg(&state.tree, right, 0)) } if (kind == .enum_literal) return ast.name_id(&state.tree, ast.enum_literal(&state.tree, left)) == ast.name_id(&state.tree, ast.enum_literal(&state.tree, right)) return try value_equal(state, try evaluate(state, left), try evaluate(state, right)) } @hide:file constant_hash proc(state @mut State, id ast.NodeId) usize ! Error { kind :: ast.expr_kind(&state.tree, id) hash :: enum_index(kind) if (kind == .literal_void) return hash if (kind == .typed_constant or kind == .union_constant or kind == .slice_constant) { value :: if (kind == .union_constant) ast.union_constant(&state.tree, id) else if (kind == .slice_constant) ast.slice_constant(&state.tree, id) else ast.typed_constant(&state.tree, id) return mix(mix(hash, try type_hash_context(state, value.type, true)), try constant_hash(state, value.value)) } if (kind == .slice) { view :: ast.slice_expr(&state.tree, id) return mix(mix(mix(hash, try constant_hash(state, view.base)), try constant_hash(state, view.end?)), try constant_hash(state, view.sentinel?)) } if (kind == .array_literal) { result := hash for (0..ast.array_element_count(&state.tree, id)) |i| result = mix(result, try constant_hash(state, ast.array_element(&state.tree, id, i))) return result } if (kind == .record_literal) { result := hash for (0..ast.record_field_count(&state.tree, id)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i)) if (field.name) |name| result = mix(result, usize(ast.name_id(&state.tree, name))) result = mix(result, try constant_hash(state, field.value?)) } return result } if (kind == .builtin_call and contextual_literal(state, id)) return mix(hash, try constant_hash(state, ast.call_arg(&state.tree, id, 0))) if (kind == .enum_literal) return mix(hash, usize(ast.name_id(&state.tree, ast.enum_literal(&state.tree, id)))) return try value_hash(state, try evaluate(state, id)) } @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 function := no_node if (callee.kind == .function) { resolved :: binding(state, callee.node) if (resolved.kind != .declaration) return Value{} function = resolved.data.declaration 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| { argument :: ast.call_arg(&state.tree, call, i) value := Value{} if (function != no_node and i < ast.function_param_count(&state.tree, function)) { annotation :: ast.param(&state.tree, ast.function_param(&state.tree, function, i)).type value = try evaluate_expected(state, argument, annotation) } else value = try evaluate(state, argument) 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) try refresh(state) return try evaluate_ir(state, invocation, result_type) } @hide:file evaluate_ir proc(state @mut State, expression, result_type ast.NodeId) Value ! Error { if (result_type == no_node) return Value{} span :: ast.node_span(&state.tree, expression) returned :: try ast.add_return_stmt(state.builder, span, expression) statements [1]ast.NodeId :: [returned] body :: try ast.add_block(state.builder, span, statements[..]) try refresh(state) return try evaluate_ir_body(state, expression, body, result_type) } @hide:file evaluate_ir_body proc(state @mut State, at, body, result_type ast.NodeId) Value ! Error { span :: ast.node_span(&state.tree, at) 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) 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)) flags.items[usize(declaration)] = flags.items[usize(declaration)] | 2 evaluation_owned(state, declaration, flags.items) 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| { if (diagnostic.custom_message) |text| { _ = try diagnostics.add_message(&diagnostic_store, diagnostic.code, diagnostic.span, text) } else _ = try diagnostics.add(&diagnostic_store, diagnostic.code, diagnostic.span) } 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) { diagnostic :: diagnostic_store.list.items[original_diagnostics] state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, diagnostic.code, diagnostic.span) return Value{} } functions := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&functions) module := try lower.lower_evaluation(state.allocator, &tree, state.resolution, &checked, wrapper, &functions, &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 source_type :: typecheck.type_of(&checked, result_type) remaining := usize(100000) node :: try serialize_value(state, &module, &checked, &outcome, functions.items, source_type, value, span, &remaining) if (node == no_node) { if (state.evaluation_diagnostic == diagnostics.no_diagnostic) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span) return Value{} } if ( (ir.aggregate(value.type) and source_type.kind != .slice) or (source_type.kind == .slice and (ast.expr_kind(&state.tree, node) == .slice or ast.expr_kind(&state.tree, node) == .slice_constant)) or source_type.kind == .enumeration or source_type.kind == .void ) return Value{ kind = .aggregate, node = node, annotation = result_type } result := try evaluate(state, node) result.annotation = result_type return result } @hide:file 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 TypeExecution :: struct { locals arraylist.ArrayList(TypeLocal) mapping []mut ast.NodeId first_owned ast.NodeId returned ast.NodeId = no_node result_type ast.NodeId failed bool = false yielded ast.NodeId = no_node yield_type ast.NodeId = no_node yield_depth usize = 0 cleanup_yield_depth usize = 0 cleanup_depth usize = 0 cleanup_loop_depth usize = 0 evaluate_values bool = false pending_flow TypeFlow = .next 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 return_type_value proc(state @mut State, block, first_owned, result_type ast.NodeId) ast.NodeId ! Error { # ponytail: snapshots recheck scalar IR; cap type-control work at 4096 steps # until type values can share one persistent checked execution frame. execution := TypeExecution{ locals = arraylist.init(state.allocator), mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)), first_owned = first_owned, result_type = result_type, evaluate_values = true, } defer arraylist.deinit(&execution.locals) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) flow :: try execute_type_block(state, &execution, block) if (flow == .returned) return execution.returned if (flow == .next) return try type_void_completion(state, result_type, block) return no_node } @hide:file type_void_completion proc(state @mut State, annotation, at ast.NodeId) ast.NodeId ! Error { type :: try canonical_type(state, annotation) if (type == no_node) return no_node child :: if (ast.expr_kind(&state.tree, type) == .fallible_type) try canonical_type(state, ast.binary_expr(&state.tree, type).left) else type resolved :: binding(state, child) if (resolved.kind != .builtin or resolved.data.builtin != .void) return no_node value :: try ast.add_literal(state.builder, .void, ast.node_span(&state.tree, at)) try refresh(state) return try type_execution_value(state, value, annotation) } @hide:file evaluate_type_value_block proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { if (state.type_execution_depth == 0) state.type_execution_remaining = 4096 state.type_execution_depth += 1 defer state.type_execution_depth -= 1 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 = no_node, evaluate_values = true, } defer arraylist.deinit(&execution.locals) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) return try execute_type_value_block(state, &execution, ast.block_expr(&state.tree, expression), annotation) } @hide:file execute_type_value_block proc( state @mut State, execution @mut TypeExecution, block, annotation ast.NodeId, ) ast.NodeId ! Error { saved_value :: execution.yielded saved_type :: execution.yield_type execution.yielded = no_node execution.yield_type = annotation execution.yield_depth += 1 defer { execution.yielded = saved_value execution.yield_type = saved_type execution.yield_depth -= 1 } flow :: try execute_type_block(state, execution, block) if (flow == .yielded) return execution.yielded if (flow != .next) execution.pending_flow = flow else { inferred :: if (annotation == no_node) try value_block_type(state, block, 0) else annotation result :: binding(state, inferred) if (result.kind == .builtin and result.data.builtin == .void) { value :: try ast.add_literal(state.builder, .void, ast.node_span(&state.tree, block)) try refresh(state) return value } state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .missing_yield, ast.node_span(&state.tree, block)) execution.pending_flow = .invalid } return no_node } @hide:file type_step proc(state @mut State, at ast.NodeId) bool ! Error { if (state.type_execution_remaining == 0) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_execution_limit, ast.node_span(&state.tree, at)) return false } state.type_execution_remaining -= 1 return true } @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 return } execution.mapping[usize(id)] = no_node resolved :: binding(state, id) if (resolved.kind == .declaration) { for (execution.locals.items) |local| { if (local.declaration == resolved.data.declaration) { 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 } } } if (execution.evaluate_values and ast.node_class(&state.tree, id) == .field) { field :: ast.field(&state.tree, id) if (field.type != null and field.value != null) { annotation :: try type_snapshot(state, execution, field.type?) if (annotation == no_node) return value :: try type_snapshot_expected(state, execution, field.value?, annotation) execution.mapping[usize(field.value?)] = value if (value == no_node and execution.pending_flow == .next) execution.pending_flow = .invalid return } } 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 while (true) { root_kind :: ast.expr_kind(&state.tree, root) if (root_kind == .selection) root = ast.selection_expr(&state.tree, root).base else if (root_kind == .index) root = ast.index_expr(&state.tree, root).base else if (root_kind == .unary and ast.unary_expr(&state.tree, root).operator == .unwrap) root = ast.unary_expr(&state.tree, root).operand else if (root_kind == .builtin_call and mem.eql(ast.literal_text(&state.tree, ast.builtin_call(&state.tree, root).name), "field") and ast.call_arg_count(&state.tree, root) == 2) root = ast.call_arg(&state.tree, root, 0) else break } place :: binding(state, root) if (place.kind == .declaration and ast.node_class(&state.tree, place.data.declaration) == .statement and ast.statement_decl(&state.tree, place.data.declaration).mutability == .variable) { annotation :: try expression_type(state, base, 0) if (type_record_storage(state, annotation, 0) and ast.expr_kind(&state.tree, annotation) == .array_type) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, ast.node_span(&state.tree, id)) execution.pending_flow = .invalid return } } } if (kind == .block_expr) { execution.mapping[usize(id)] = try execute_type_value_block(state, execution, ast.block_expr(&state.tree, id), no_node) if (execution.mapping[usize(id)] == no_node and execution.pending_flow == .next) execution.pending_flow = .invalid return } if (kind == .match_expr) { execution.mapping[usize(id)] = try type_snapshot_match(state, execution, id, no_node) if (execution.mapping[usize(id)] == no_node and execution.pending_flow == .next) execution.pending_flow = .invalid return } if (kind == .binary) { view :: ast.binary_expr(&state.tree, id) if (view.operator == .logical_and or view.operator == .logical_or) { boolean :: try builtin(state, id, .bool) left :: try type_snapshot_expected(state, execution, view.left, boolean) if (left == no_node) return value :: try evaluate_ir(state, left, boolean) if (value.kind != .boolean) { execution.pending_flow = .invalid return } right := no_node if (value.boolean == (view.operator == .logical_or)) { saved :: execution.evaluate_values execution.evaluate_values = false right = try type_snapshot(state, execution, view.right) if (right != no_node and !try skip_type_value_blocks(state, right)) { execution.pending_flow = .invalid return } execution.evaluate_values = saved } else right = try type_snapshot_expected(state, execution, view.right, boolean) if (right == no_node and execution.pending_flow == .next) { execution.pending_flow = .invalid return } # retain the operator and skipped operand for ordinary type validation. execution.mapping[usize(view.left)] = left execution.mapping[usize(view.right)] = right return } } if (kind == .try_expr) { execution.mapping[usize(id)] = try type_snapshot_try(state, execution, id) if (execution.mapping[usize(id)] == no_node and execution.pending_flow == .next) execution.pending_flow = .invalid return } if (kind == .catch_expr) { execution.mapping[usize(id)] = try type_snapshot_catch(state, execution, id, no_node) if (execution.mapping[usize(id)] == no_node and execution.pending_flow == .next) execution.pending_flow = .invalid return } } for (0..ast.child_count(&state.tree, id)) |i| try prepare_type_snapshot(state, execution, ast.child(&state.tree, id, i)) } @hide:file bind_type_snapshot proc(state @mut State, execution @TypeExecution, id, start ast.NodeId) void ! Error { if (usize(id) >= execution.mapping.len) return target :: execution.mapping[usize(id)] if (target == no_node or target < start) return resolved := binding(state, id) if (resolved.kind == .declaration) { declaration :: resolved.data.declaration if (usize(declaration) < execution.mapping.len and execution.mapping[usize(declaration)] != no_node) resolved.data.declaration = execution.mapping[usize(declaration)] } state.bindings.items[usize(target)] = resolved if (hashmap.get(&state.capture_types, id)) |annotation| { hashmap.put(&state.capture_types, target, annotation) catch |err| { match err { .key_exists: {} else: return err } } } for (0..ast.child_count(&state.tree, id)) |i| try bind_type_snapshot(state, execution, ast.child(&state.tree, id, i), start) } @hide:file skip_type_value_blocks proc(state @mut State, id ast.NodeId) bool ! Error { if (ast.node_class(&state.tree, id) == .expression and ast.expr_kind(&state.tree, id) == .block_expr) { annotation :: try expression_type(state, id, 0) if (annotation == no_node) return false span :: ast.node_span(&state.tree, id) # skipped type-control bodies retain only their result type for checked IR. placeholder :: try ast.add_literal(state.builder, .undefined, span) local :: try ast.add_statement_decl(state.builder, span, span, strpool.no_str, annotation, .variable, placeholder) try refresh(state) yielded :: try ast.add_yield_stmt(state.builder, span, try reference(state, id, local)) statements [2]ast.NodeId :: [local, yielded] body :: try ast.add_block(state.builder, span, statements[..]) replacement :: try ast.add_block_expr(state.builder, span, body) ast.replace_node_from_source(state.builder, id, replacement) try refresh(state) return true } for (0..ast.child_count(&state.tree, id)) |i| if (!try skip_type_value_blocks(state, ast.child(&state.tree, id, i))) return false return true } @hide:file type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error { if (id == no_node or execution.pending_flow != .next) return no_node if (execution.mapping.len < ast.node_count(&state.tree)) { previous :: execution.mapping.len execution.mapping = try mem.realloc(state.allocator, execution.mapping, ast.node_count(&state.tree)) mem.set(execution.mapping[previous..], no_node) } try prepare_type_snapshot(state, execution, id) if (execution.pending_flow != .next) return no_node start :: ast.NodeId(ast.node_count(&state.tree)) result :: try ast.clone_subtree(state.builder, id, execution.mapping) try refresh(state) try bind_type_snapshot(state, execution, id, start) return result } @hide:file type_snapshot_expected proc( state @mut State, execution @mut TypeExecution, id, annotation ast.NodeId, ) ast.NodeId ! Error { if (id == no_node or execution.pending_flow != .next) return no_node if (execution.evaluate_values and ast.expr_kind(&state.tree, id) == .block_expr) return try execute_type_value_block(state, execution, ast.block_expr(&state.tree, id), annotation) if (execution.evaluate_values and ast.expr_kind(&state.tree, id) == .match_expr) return try type_snapshot_match(state, execution, id, annotation) if (execution.evaluate_values and ast.expr_kind(&state.tree, id) == .catch_expr) return try type_snapshot_catch(state, execution, id, annotation) return try type_snapshot(state, execution, id) } @hide:file type_fallible_failed proc( state @mut State, owned, type, at ast.NodeId, ) ?bool ! Error { missing ?bool :: null if (type_record_storage(state, type, 0)) { if (!try type_array_initialized(state, owned, at)) return missing return !try same_type(state, ast.typed_constant(&state.tree, ast.typed_constant(&state.tree, owned).value).type, ast.binary_expr(&state.tree, type).left) } span :: ast.node_span(&state.tree, at) boolean :: try builtin(state, at, .bool) yes :: try ast.add_boolean(state.builder, span, true) no :: try ast.add_boolean(state.builder, span, false) returned :: try ast.add_return_stmt(state.builder, span, yes) failure_statements [1]ast.NodeId :: [returned] handler :: try ast.add_block(state.builder, span, failure_statements[..]) caught :: try ast.add_catch_expr(state.builder, span, owned, null, handler, true) discarded :: try ast.add_expression_stmt(state.builder, caught) success :: try ast.add_return_stmt(state.builder, span, no) statements [2]ast.NodeId :: [discarded, success] body :: try ast.add_block(state.builder, span, statements[..]) try refresh(state) guard :: try evaluate_ir_body(state, at, body, boolean) if (guard.kind != .boolean) return missing return guard.boolean } @hide:file type_snapshot_catch proc( state @mut State, execution @mut TypeExecution, id, annotation ast.NodeId, ) ast.NodeId ! Error { view :: ast.catch_expr(&state.tree, id) operand :: try type_snapshot(state, execution, view.operand) if (operand == no_node) return no_node try expand_expected(state, operand, annotation) operand_type :: try expression_type(state, operand, 0) if (operand_type == no_node or ast.expr_kind(&state.tree, operand_type) != .fallible_type) return no_node channels :: ast.binary_expr(&state.tree, operand_type) owned :: try type_execution_value(state, operand, operand_type) if (owned == no_node) return no_node failure :: try type_fallible_failed(state, owned, operand_type, id) if (failure == null) return no_node failed :: failure? payload :: ast.typed_constant(&state.tree, owned).value if (!failed) return try type_execution_value(state, payload, channels.left) saved :: execution.locals.items.len defer execution.locals.items = execution.locals.items[..saved] if (view.capture) |capture| { error :: try type_execution_value(state, payload, channels.right) if (error == no_node) return no_node try arraylist.append(&execution.locals, TypeLocal{ declaration = capture, value = error, annotation = channels.right, }) } result_type := channels.left if (annotation != no_node and !is_type(state, annotation)) { contextual :: try canonical_type(state, annotation) if (contextual != no_node) { kind :: ast.expr_kind(&state.tree, contextual) if (kind == .optional_type and try same_type(state, ast.type_operand(&state.tree, contextual), channels.left)) result_type = annotation if (kind == .fallible_type and try same_type(state, ast.binary_expr(&state.tree, contextual).left, channels.left)) result_type = annotation } } if (view.handler_is_block) return try execute_type_value_block(state, execution, view.handler, result_type) selected :: try type_snapshot_expected(state, execution, view.handler, result_type) return try type_execution_value(state, selected, result_type) } @hide:file specialize_type_catch proc(state @mut State, id, annotation, expected ast.NodeId) void ! Error { view :: ast.catch_expr(&state.tree, id) value :: try evaluate_expected(state, view.operand, annotation) if (value.kind != .aggregate) { try reject_evaluation(state, id) return } channels :: ast.binary_expr(&state.tree, annotation) payload :: value.node success :: try same_type(state, ast.typed_constant(&state.tree, payload).type, channels.left) selected := no_node if (success) selected = try type_execution_value(state, payload, channels.left) else { 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 = no_node, } defer arraylist.deinit(&execution.locals) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) if (view.capture) |capture| { resolved :: binding(state, capture) if (resolved.kind == .invalid) { state.evaluation_diagnostic = resolved.data.diagnostic try reject_evaluation(state, id) return } error :: try type_execution_value(state, payload, channels.right) if (error == no_node) { try reject_evaluation(state, id) return } try arraylist.append(&execution.locals, TypeLocal{ declaration = capture, value = error, annotation = channels.right, }) } selected = try type_snapshot(state, &execution, view.handler) if (view.handler_is_block and selected != no_node) { selected = try ast.add_block_expr(state.builder, ast.node_span(&state.tree, id), selected) try refresh(state) } } if (selected == no_node) { try reject_evaluation(state, id) return } context :: if (expected != no_node) expected else channels.left try expand_expected(state, selected, context) if (is_type(state, channels.left) or type_record_storage(state, channels.left, 0)) { selected = try type_execution_value(state, selected, channels.left) if (selected == no_node) { try reject_evaluation(state, id) return } } else { result_type := channels.left contextual :: try canonical_type(state, context) if (contextual != no_node) { kind :: ast.expr_kind(&state.tree, contextual) if (kind == .optional_type and try same_type(state, ast.type_operand(&state.tree, contextual), channels.left)) result_type = context if (kind == .fallible_type and try same_type(state, ast.binary_expr(&state.tree, contextual).left, channels.left)) result_type = context } selected = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, id), result_type, selected) try refresh(state) } if (is_type(state, try expression_type(state, selected, 0))) { selected = try ast.add_type_operand(state.builder, ast.node_span(&state.tree, id), selected, true) try refresh(state) } try replace(state, id, selected) try refresh(state) state.bindings.items[usize(id)] = binding(state, selected) } @hide:file type_snapshot_try proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error { operand :: try type_snapshot(state, execution, ast.type_operand(&state.tree, id)) if (operand == no_node) return no_node try expand(state, operand) annotation :: try expression_type(state, operand, 0) if (annotation == no_node or ast.expr_kind(&state.tree, annotation) != .fallible_type) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_fallible_operand, ast.node_span(&state.tree, id)) return no_node } result :: try canonical_type(state, execution.result_type) if (execution.cleanup_depth > 0 or result == no_node or ast.expr_kind(&state.tree, result) != .fallible_type) { code diagnostics.Code :: if (execution.cleanup_depth > 0) .cleanup_control_flow else .try_outside_fallible state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, ast.node_span(&state.tree, id)) return no_node } channels :: ast.binary_expr(&state.tree, annotation) if (!try type_subset(state, channels.right, ast.binary_expr(&state.tree, result).right)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .incompatible_propagated_error, ast.node_span(&state.tree, id)) return no_node } # propagation is a catch whose error branch returns from the enclosing frame. span :: ast.node_span(&state.tree, id) capture :: try ast.add_identifier(state.builder, span, strpool.no_str) try refresh(state) value :: try reference(state, id, capture) returned :: try ast.add_return_stmt(state.builder, span, value) statements [1]ast.NodeId :: [returned] handler :: try ast.add_block(state.builder, span, statements[..]) caught :: try ast.add_catch_expr(state.builder, span, operand, capture, handler, true) try refresh(state) return try type_snapshot_catch(state, execution, caught, channels.left) } @hide:file 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 snapshot :: try type_snapshot(state, execution, id) if (snapshot == no_node) return no_node arms := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&arms) for (0..ast.match_arm_count(&state.tree, snapshot)) |i| try arraylist.append(&arms, ast.match_arm(&state.tree, snapshot, i)) optional_capture :: ast.match_expr(&state.tree, id).optional_capture result :: try ast.add_match(state.builder, ast.node_span(&state.tree, id), subject, arms.items) if (optional_capture) ast.set_match_optional(state.builder, result) try refresh(state) return result } @hide:file type_snapshot_match proc( state @mut State, execution @mut TypeExecution, id, annotation ast.NodeId, ) ast.NodeId ! Error { snapshot :: try type_dispatch_snapshot(state, execution, id) if (snapshot == no_node) return no_node index :: try type_match_arm(state, snapshot) if (index == no_index) return no_node arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, id, index)) saved :: execution.locals.items.len defer execution.locals.items = execution.locals.items[..saved] if (arm.capture) |capture| { value :: try type_match_capture(state, snapshot) if (value == no_node) return no_node constant :: ast.typed_constant(&state.tree, value) payload :: if (is_type(state, constant.type)) constant.value else value try arraylist.append(&execution.locals, TypeLocal{ declaration = capture, value = payload, annotation = constant.type, }) } if (!arm.expression) return try execute_type_value_block(state, execution, arm.body, annotation) return try type_snapshot_expected(state, execution, arm.body, annotation) } @hide:file nominal_type_value proc(state @State, id ast.NodeId) bool { kind :: ast.expr_kind(&state.tree, id) return kind == .record_type or kind == .union_type or kind == .enum_type or kind == .distinct_type } # transfers targeting an inline loop must be selected before checking runtime # bodies. nested loops own their transfers and are elaborated independently. @hide:file inline_has_transfer proc(state @State, id ast.NodeId) bool { class :: ast.node_class(&state.tree, id) if (class == .function) return false if (class == .statement) { kind :: ast.stmt_kind(&state.tree, id) if (kind == .for_stmt or kind == .while_stmt) return false if (kind == .break_stmt or kind == .continue_stmt) return true } for (0..ast.child_count(&state.tree, id)) |i| { if (inline_has_transfer(state, ast.child(&state.tree, id, i))) return true } return false } @hide:file inline_statement proc(state @mut State, id ast.NodeId) TypeFlow ! Error { kind :: ast.stmt_kind(&state.tree, id) if (kind == .break_stmt) return .break if (kind == .continue_stmt) return .continue if (kind == .unreachable_stmt) return .returned if (kind == .yield_stmt or kind == .return_stmt) { try expand(state, id) return if (kind == .yield_stmt) TypeFlow.yielded else TypeFlow.returned } if (kind == .block_stmt) return try inline_block(state, ast.statement_block(&state.tree, id)) selected := no_node value_source := false if (kind == .if_stmt) { conditional :: ast.if_stmt(&state.tree, id) explicit :: ast.expr_kind(&state.tree, conditional.condition) == .comptime_expr condition :: if (explicit) ast.comptime_operand(&state.tree, conditional.condition) else conditional.condition try expand(state, condition) if (conditional.capture == null and (explicit or branch_constant(state, condition, 0))) { value :: try evaluate_ir(state, condition, try builtin(state, condition, .bool)) if (value.kind != .boolean) return .invalid if (value.boolean) selected = conditional.then_block else if (conditional.else_block) |other| selected = other else selected = try ast.add_block(state.builder, conditional.span, &[]) } } if (kind == .expression) { expression :: ast.statement_expr(&state.tree, id) if (ast.expr_kind(&state.tree, expression) == .match_expr) { subject :: ast.match_expr(&state.tree, expression).subject explicit :: ast.expr_kind(&state.tree, subject) == .comptime_expr try expand(state, subject) if (explicit or branch_constant(state, subject, 0)) { index :: try type_match_arm(state, expression) if (index == no_index) return .invalid arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, index)) body :: try type_match_body(state, expression, arm) if (body == no_node) return .invalid if (!arm.expression) { selected = body value_source = true } else { statement :: try ast.add_expression_stmt(state.builder, body) statements [1]ast.NodeId :: [statement] selected = try ast.add_block(state.builder, ast.node_span(&state.tree, id), statements[..]) } } } } if (selected != no_node) { try refresh(state) flow := try inline_block(state, selected) replacement := no_node if (value_source) { value :: try ast.add_block_expr(state.builder, ast.node_span(&state.tree, selected), selected) replacement = try ast.add_expression_stmt(state.builder, value) if (flow == .yielded) flow = .next } else replacement = try ast.add_block_stmt(state.builder, selected) try replace(state, id, replacement) try refresh(state) return flow } if (inline_has_transfer(state, id)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .inline_loop_control, ast.node_span(&state.tree, id)) return .invalid } try expand(state, id) return .next } @hide:file inline_block proc(state @mut State, block ast.NodeId) TypeFlow ! Error { statements := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&statements) flow := TypeFlow.next 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 .invalid flow = try inline_statement(state, statement) if (flow == .invalid) return flow kind :: ast.stmt_kind(&state.tree, statement) if (kind != .break_stmt and kind != .continue_stmt) try arraylist.append(&statements, statement) if (flow != .next) break } replacement :: try ast.add_block(state.builder, ast.node_span(&state.tree, block), statements.items) try replace(state, block, replacement) try refresh(state) return flow } @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 defer state.type_execution_depth -= 1 state.evaluation_diagnostic = diagnostics.no_diagnostic blocks := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&blocks) 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 = no_node, inline_body = loop.body, inline_blocks = &blocks, } defer arraylist.deinit(&execution.locals) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) for (0..usize(2)) |i| { capture :: if (i == 0) loop.capture else loop.index if (capture) |node| { resolved :: binding(state, node) if (resolved.kind == .invalid) state.evaluation_diagnostic = resolved.data.diagnostic } } if (loop.pointer_capture) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, loop.span) flow := TypeFlow.invalid if (state.evaluation_diagnostic == diagnostics.no_diagnostic) flow = try execute_type_for(state, &execution, id) if ((flow != .next and flow != .yielded and flow != .returned) or state.evaluation_diagnostic != diagnostics.no_diagnostic) { if (state.evaluation_diagnostic == diagnostics.no_diagnostic) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_argument_required, loop.span) invalid :: try ast.add_invalid_stmt(state.builder, loop.span, state.evaluation_diagnostic) try replace(state, id, invalid) try refresh(state) return } statements := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&statements) for (blocks.items) |block| { statement :: try ast.add_block_stmt(state.builder, block) try arraylist.append(&statements, statement) } block :: try ast.add_block(state.builder, loop.span, statements.items) replacement :: try ast.add_block_stmt(state.builder, block) try replace(state, id, replacement) try refresh(state) } # select with ordinary checked IR so patterns, overlap and exhaustiveness have # the same rules as runtime matching, without checking unexecuted type bodies. @hide:file type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error { try expand_inline_match(state, id, no_node) if (ast.expr_kind(&state.tree, id) != .match_expr) return no_index optional_capture :: ast.match_expr(&state.tree, id).optional_capture if (!try expand_type_matches(state, ast.match_expr(&state.tree, id).subject)) return no_index try expand(state, ast.match_expr(&state.tree, id).subject) arms := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&arms) patterns := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&patterns) subject :: ast.match_expr(&state.tree, id).subject annotation :: try expression_type(state, subject, 0) metadata :: type_record_storage(state, annotation, 0) dispatch := subject if (metadata) { value :: try evaluate_expected(state, subject, annotation) if (value.kind != .aggregate) return no_index span :: ast.node_span(&state.tree, subject) if (ast.expr_kind(&state.tree, annotation) == .optional_type) { present :: ast.expr_kind(&state.tree, value.node) != .literal_null boolean :: try ast.add_boolean(state.builder, span, true) payload :: if (present) boolean else try ast.add_literal(state.builder, .null, span) optional :: try ast.add_optional_type(state.builder, span, try builtin(state, subject, .bool)) dispatch = try ast.add_typed_constant(state.builder, span, optional, payload) } else { if (ast.expr_kind(&state.tree, value.node) != .record_literal) return no_index active :: ast.field(&state.tree, ast.record_field(&state.tree, value.node, 0)) tags :: try metadata_variant_tags(state, try inline_match_shape(state, annotation)) tag :: try ast.add_enum_literal(state.builder, span, active.name?) dispatch = try ast.add_typed_constant(state.builder, span, tags, tag) } try refresh(state) } for (0..ast.match_arm_count(&state.tree, id)) |i| { arm_id :: ast.match_arm(&state.tree, id, i) arm :: ast.match_arm_view(&state.tree, arm_id) # selector bodies omit source capture uses, but must retain their resolver failures. if (arm.capture) |capture| { resolved :: binding(state, capture) if (resolved.kind == .invalid) { state.evaluation_diagnostic = resolved.data.diagnostic return no_index } } patterns.items = patterns.items[..0] for (0..ast.match_pattern_count(&state.tree, arm_id)) |j| { pattern :: ast.match_pattern(&state.tree, arm_id, j) if (!try expand_type_matches(state, pattern)) return no_index try expand(state, pattern) try arraylist.append(&patterns, pattern) } if (metadata and !optional_capture and arm.capture != null) { expected := no_node valid := !arm.catch_all and patterns.items.len > 0 for (patterns.items) |pattern| { if (ast.expr_kind(&state.tree, pattern) != .enum_literal) { valid = false continue } shape :: try inline_match_shape(state, annotation) slot :: type_record_slot(&state.tree, shape, ast.enum_literal(&state.tree, pattern)) if (slot == no_index) { valid = false continue } payload :: ast.field(&state.tree, ast.record_field(&state.tree, shape, slot)).type? if (binding(state, payload).kind == .builtin and binding(state, payload).data.builtin == .void) valid = false if (expected != no_node and !try same_type(state, expected, payload)) valid = false expected = payload } if (!valid) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_match_capture, ast.node_span(&state.tree, arm_id)) return no_index } } span :: ast.node_span(&state.tree, arm_id) index :: try ast.add_integer(state.builder, span, u64(i)) capture :: if (metadata) null else arm.capture selector :: try ast.add_match_arm(state.builder, span, patterns.items, capture, index, true, arm.catch_all) try refresh(state) try arraylist.append(&arms, selector) } selector :: try ast.add_match(state.builder, ast.node_span(&state.tree, id), dispatch, arms.items) if (optional_capture) ast.set_match_optional(state.builder, selector) try refresh(state) # selector arms contain only indices. do not specialize this synthetic match # again while its checked evaluation expands the wrapper. state.expanded.items[usize(selector)] = 1 selected :: try evaluate_ir(state, selector, try builtin(state, id, .usize)) if (selected.kind != .integer) return no_index return usize(selected.magnitude) } # match checking has already validated every capture. copy the active payload # out of owned constant storage, never an address in the temporary interpreter. @hide:file type_match_capture proc(state @mut State, id ast.NodeId) ast.NodeId ! Error { subject :: ast.match_expr(&state.tree, id).subject if (ast.match_expr(&state.tree, id).optional_capture) { unwrapped :: try ast.add_unary(state.builder, ast.node_span(&state.tree, subject), .unwrap, subject) try refresh(state) child :: try expression_type(state, unwrapped, 0) payload :: try type_execution_value(state, unwrapped, child) if (payload == no_node or !is_type(state, child)) return payload result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, subject), child, payload) try refresh(state) return result } annotation :: try expression_type(state, subject, 0) value :: if (type_record_storage(state, annotation, 0)) try evaluate_expected(state, subject, annotation) else try evaluate_ir(state, subject, annotation) if (value.kind != .aggregate or ast.expr_kind(&state.tree, value.node) != .record_literal) return no_node payload :: ast.field(&state.tree, ast.record_field(&state.tree, value.node, 0)) shape :: try inline_match_shape(state, annotation) if (shape == no_node) return no_node for (0..ast.record_field_count(&state.tree, shape)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, shape, i)) if ( field.name != null and payload.name != null and ast.name_id(&state.tree, field.name?) == ast.name_id(&state.tree, payload.name?) ) { if ( ast.expr_kind(&state.tree, payload.value?) == .typed_constant and try same_type(state, field.type?, ast.typed_constant(&state.tree, payload.value?).type) ) return payload.value? result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, subject), field.type?, payload.value?) try refresh(state) return result } } return no_node } @hide:file type_match_body proc(state @mut State, id ast.NodeId, arm ast.MatchArmView) ast.NodeId ! Error { if (arm.capture == null) return arm.body value :: try type_match_capture(state, id) if (value == no_node) return no_node snapshot :: ast.typed_constant(&state.tree, value) 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 = no_node, } defer arraylist.deinit(&execution.locals) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) captured :: if (is_type(state, snapshot.type)) snapshot.value else value try arraylist.append(&execution.locals, TypeLocal{ declaration = arm.capture?, value = captured, annotation = snapshot.type, }) return try type_snapshot(state, &execution, arm.body) } @hide:file type_value_expression proc(state @mut State, input ast.NodeId) ast.NodeId ! Error { if (input == no_node) return no_node expression := input while (ast.expr_kind(&state.tree, expression) == .match_expr) { if (!try type_step(state, expression)) return no_node index :: try type_match_arm(state, expression) if (index == no_index) return no_node arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, index)) expression = try type_match_body(state, expression, arm) if (!arm.expression and expression != no_node) { expression = try ast.add_block_expr(state.builder, ast.node_span(&state.tree, expression), expression) try refresh(state) } if (expression == no_node) return no_node } if (expression != no_node and ast.expr_kind(&state.tree, expression) == .block_expr) { annotation :: try value_block_type(state, ast.block_expr(&state.tree, expression), 0) if (annotation != no_node and (is_type(state, annotation) or type_record_storage(state, annotation, 0))) return try evaluate_type_value_block(state, expression, annotation) } return expression } # normalize selected value sources before ordinary expansion can visit their bodies. # nominal results enter here after their instance declaration is cached. @hide:file expand_type_matches proc(state @mut State, id ast.NodeId) bool ! Error { if (id == no_node) return false if (ast.node_class(&state.tree, id) == .expression and ast.expr_kind(&state.tree, id) == .match_expr) { selected :: try type_value_expression(state, id) if (selected == no_node) return false resolved :: binding(state, selected) try remember(state, id) ast.replace_node_from_source(state.builder, id, selected) try refresh(state) state.bindings.items[usize(id)] = resolved } for (0..ast.child_count(&state.tree, id)) |i| { if (!try expand_type_matches(state, ast.child(&state.tree, id, i))) return false } return try type_array_projection(state, id) } @hide:file type_array_projection proc(state @mut State, id ast.NodeId) bool ! Error { if (ast.node_class(&state.tree, id) != .expression) return true kind :: ast.expr_kind(&state.tree, id) if (kind == .slice) { annotation :: try expression_type(state, id, 0) if (!try metadata_sequence(state, annotation)) return true value :: try type_slice_value(state, id, annotation) if (value == no_node) return false try replace(state, id, value) try refresh(state) return true } if (kind == .unary and ast.unary_expr(&state.tree, id).operator == .unwrap) { operand :: ast.unary_expr(&state.tree, id).operand annotation :: try expression_type(state, operand, 0) if (!type_record_storage(state, annotation, 0)) return true type :: try canonical_type(state, annotation) if (type == no_node or ast.expr_kind(&state.tree, type) != .optional_type) return true value :: try type_execution_value(state, operand, type) if (value == no_node or !try type_array_initialized(state, value, id)) return false payload :: ast.typed_constant(&state.tree, value).value if (ast.expr_kind(&state.tree, payload) == .literal_null) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .null_unwrap, ast.node_span(&state.tree, id)) return false } selected :: ast.call_arg(&state.tree, payload, 0) resolved :: binding(state, selected) try remember(state, id) ast.replace_node_from_source(state.builder, id, selected) try refresh(state) state.bindings.items[usize(id)] = resolved return true } if (kind == .index or kind == .selection) { base :: if (kind == .index) ast.index_expr(&state.tree, id).base else ast.selection_expr(&state.tree, id).base base_binding :: binding(state, base) if (base_binding.kind == .invalid) { state.evaluation_diagnostic = base_binding.data.diagnostic return false } if (ast.expr_kind(&state.tree, base) != .typed_constant) return true array :: ast.typed_constant(&state.tree, base) type :: try canonical_type(state, array.type) record :: (type != no_node and (ast.expr_kind(&state.tree, type) == .record_type or (type_record_storage(state, type, 0) and ast.expr_kind(&state.tree, type) == .union_type))) if ( !record and !try execution_array_storage(state, array.type) and !try metadata_sequence(state, array.type) ) return true if ( (try metadata_sequence(state, array.type) or record) and ast.expr_kind(&state.tree, array.value) == .literal_undefined ) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, ast.node_span(&state.tree, id)) return false } selected := no_node selected_type := no_node if (record) { if (kind != .selection) return true member :: ast.selection_expr(&state.tree, id).member slot :: type_record_slot(&state.tree, type, member) if (slot == no_index) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .unknown_field, ast.node_span(&state.tree, member)) return false } if (ast.expr_kind(&state.tree, type) == .union_type) { active :: ast.field(&state.tree, ast.record_field(&state.tree, array.value, 0)) if (active.name == null or ast.name_id(&state.tree, active.name?) != ast.name_id(&state.tree, member)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .inactive_union_field, ast.node_span(&state.tree, id)) return false } selected = active.value? } else selected = ast.field(&state.tree, ast.record_field(&state.tree, array.value, slot)).value? if (!type_record_storage(state, array.type, 0)) selected_type = ast.field(&state.tree, ast.record_field(&state.tree, type, slot)).type? } else if (kind == .index) { index :: try type_array_index(state, ast.index_expr(&state.tree, id).index, ast.array_element_count(&state.tree, array.value), false) if (index == no_index) return false selected = ast.array_element(&state.tree, array.value, index) if (!type_record_storage(state, array.type, 0)) selected_type = ast.array_type(&state.tree, type).element } else { member :: ast.selection_expr(&state.tree, id).member if (!type_record_storage(state, array.type, 0) and !mem.eql(ast.literal_text(&state.tree, member), "len")) return true if (!mem.eql(ast.literal_text(&state.tree, member), "len")) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .unknown_field, ast.node_span(&state.tree, member)) return false } count :: try ast.add_integer(state.builder, ast.node_span(&state.tree, id), u64(ast.array_element_count(&state.tree, array.value))) selected = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, id), try builtin(state, id, .usize), count) try refresh(state) } if (ast.expr_kind(&state.tree, selected) == .literal_undefined) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, ast.node_span(&state.tree, id)) return false } if (selected_type != no_node and ast.expr_kind(&state.tree, selected) != .typed_constant) { selected = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, id), selected_type, selected) try refresh(state) } # copying a nominal definition into the projection would create a new # identity on every read. retain its original node behind a type alias. if (nominal_type_value(state, selected)) { selected = try ast.add_type_operand(state.builder, ast.node_span(&state.tree, id), selected, true) try refresh(state) } resolved :: binding(state, selected) try remember(state, id) ast.replace_node_from_source(state.builder, id, selected) try refresh(state) state.bindings.items[usize(id)] = resolved } return true } # type-control arrays are persistent AST snapshots, including uninitialized leaves. # only fully initialized ordinary values may enter checked runtime IR. @hide:file execution_array_storage proc(state @mut State, annotation ast.NodeId) bool ! Error { current :: try canonical_type(state, annotation) return current != no_node and ast.expr_kind(&state.tree, current) == .array_type } @hide:file metadata_sequence proc(state @mut State, annotation ast.NodeId) bool ! Error { type :: try canonical_type(state, annotation) return (type != no_node and ast.expr_kind(&state.tree, type) == .slice_type and type_record_storage(state, type, 0)) } @hide:file type_record_storage proc(state @State, annotation ast.NodeId, depth usize) bool { return type_storage_annotation(&state.tree, state.bindings.items, annotation, depth) } @hide:file type_record_slot proc(tree @ast.Tree, record, member ast.NodeId) usize { if (ast.expr_kind(tree, member) != .literal_int) { for (0..ast.record_field_count(tree, record)) |i| { field :: ast.field(tree, ast.record_field(tree, record, i)) if (field.name != null and ast.name_id(tree, field.name?) == ast.name_id(tree, member)) return i } return no_index } ordinal := usize(0) for (ast.literal_text(tree, member)) |byte| { if (byte < '0' or byte > '9') return no_index digit :: usize(byte - '0') if (ordinal > divtrunc!(maxval!(usize) - digit, usize(10))) return no_index ordinal = ordinal * 10 + digit } if (ordinal >= ast.record_field_count(tree, record)) return no_index if (ast.field(tree, ast.record_field(tree, record, ordinal)).name != null) return no_index return ordinal } @hide:file type_union_value proc(state @mut State, expression, type ast.NodeId) ast.NodeId ! Error { if (!try type_record_check(state, type)) return no_node if (ast.expr_kind(&state.tree, expression) == .literal_undefined) { result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, expression), type, expression) try refresh(state) return result } if (ast.expr_kind(&state.tree, expression) == .typed_constant) { value :: ast.typed_constant(&state.tree, expression) if (try same_type(state, type, value.type)) return expression state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, ast.node_span(&state.tree, expression)) return no_node } name := no_node input := no_node kind :: ast.expr_kind(&state.tree, expression) if (kind == .enum_literal) name = ast.enum_literal(&state.tree, expression) else if (kind == .record_literal) { literal :: ast.record_literal(&state.tree, expression) if (literal.type != null and ast.expr_kind(&state.tree, literal.type?) == .enum_literal) { name = ast.enum_literal(&state.tree, literal.type?) count :: ast.record_field_count(&state.tree, expression) if (count == 1 and ast.field(&state.tree, ast.record_field(&state.tree, expression, 0)).name == null) { input = ast.field(&state.tree, ast.record_field(&state.tree, expression, 0)).value? } else { fields := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&fields) for (0..count) |i| try arraylist.append(&fields, ast.record_field(&state.tree, expression, i)) input = try ast.add_record_literal(state.builder, ast.node_span(&state.tree, expression), null, fields.items) try refresh(state) } } else { if (literal.type != null and !try same_type(state, type, literal.type?)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, ast.node_span(&state.tree, expression)) return no_node } if (ast.record_field_count(&state.tree, expression) != 1) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .union_initializer_count, ast.node_span(&state.tree, expression)) return no_node } field :: ast.field(&state.tree, ast.record_field(&state.tree, expression, 0)) if (field.name) |member| name = member if (field.value) |value| input = value } } slot :: if (name != no_node) type_record_slot(&state.tree, type, name) else no_index if (slot == no_index) { code diagnostics.Code :: if (name != no_node) .unknown_field else .union_initializer_count state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, ast.node_span(&state.tree, expression)) return no_node } field :: ast.field(&state.tree, ast.record_field(&state.tree, type, slot)) payload_type :: try canonical_type(state, field.type?) empty :: (binding(state, payload_type).kind == .builtin and binding(state, payload_type).data.builtin == .void) span :: ast.node_span(&state.tree, expression) if ( empty and kind == .record_literal and ast.record_literal(&state.tree, expression).type != null and ast.expr_kind(&state.tree, ast.record_literal(&state.tree, expression).type?) == .enum_literal and ast.record_field_count(&state.tree, expression) == 0 ) input = no_node if (input == no_node and empty) input = try ast.add_literal(state.builder, .void, span) if (input == no_node) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .missing_field, span) return no_node } try refresh(state) value :: try type_execution_value(state, input, field.type?) if (value == no_node) return no_node member :: try ast.add_field(state.builder, span, field.name, null, value) fields [1]ast.NodeId :: [member] literal :: try ast.add_record_literal(state.builder, span, null, fields[..]) result :: try ast.add_typed_constant(state.builder, span, type, literal) try refresh(state) return result } # keep ordinary field/default diagnostics even though the enclosing metadata record # has no runtime layout. type leaves are checked here; value leaves use the checker. @hide:file type_record_check proc(state @mut State, annotation ast.NodeId) bool ! Error { if (is_type(state, annotation)) return true type :: try canonical_type(state, annotation) if (type == no_node) return false if (!type_record_storage(state, type, 0)) { constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, type), type, true) try refresh(state) try arraylist.append(&state.rewrites.constraints, constraint) return true } if (ast.expr_kind(&state.tree, type) == .optional_type) return try type_record_check(state, ast.type_operand(&state.tree, type)) if (ast.expr_kind(&state.tree, type) == .fallible_type) { channels :: ast.binary_expr(&state.tree, type) error_type :: try canonical_type(state, channels.right) if (error_type == no_node) return false kind :: ast.expr_kind(&state.tree, error_type) if (kind != .enum_type and kind != .record_type and kind != .union_type and kind != .sum_type) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_fallible_type, ast.node_span(&state.tree, type)) return false } if (!type_record_storage(state, error_type, 0)) { checked :: try ast.add_binary_type(state.builder, ast.node_span(&state.tree, type), try builtin(state, type, .void), error_type, true) constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, type), checked, true) try refresh(state) try arraylist.append(&state.rewrites.constraints, constraint) } else if (kind == .union_type and !ast.union_type(&state.tree, error_type).auto_tag and ast.union_type(&state.tree, error_type).tag == null) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_fallible_type, ast.node_span(&state.tree, type)) return false } if (!try type_record_check(state, channels.left)) return false return try type_record_check(state, channels.right) } if (ast.expr_kind(&state.tree, type) == .sum_type) { members :: ast.binary_expr(&state.tree, type) if (!try type_record_check(state, members.left) or !try type_record_check(state, members.right)) return false if (try inline_match_shape(state, type) != no_node) return true if (state.evaluation_diagnostic == diagnostics.no_diagnostic) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_sum_type, ast.node_span(&state.tree, type)) return false } if (ast.expr_kind(&state.tree, type) == .slice_type) { descriptor :: ast.slice_type(&state.tree, type) if (descriptor.mutable or descriptor.sentinel != null) { code diagnostics.Code :: if (descriptor.mutable) .comptime_value_escape else .invalid_operand state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, ast.node_span(&state.tree, type)) return false } return try type_record_check(state, descriptor.element) } if (ast.expr_kind(&state.tree, type) == .array_type) { if (!try type_step(state, type)) return false descriptor :: ast.array_type(&state.tree, type) if (descriptor.count) |count| { value :: try evaluate(state, count) if (value.kind != .integer or value.negative) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_array_length, ast.node_span(&state.tree, count)) return false } } return try type_record_check(state, descriptor.element) } tagged :: ast.expr_kind(&state.tree, type) == .union_type tag := no_node if (tagged) { descriptor :: ast.union_type(&state.tree, type) if (!descriptor.auto_tag and descriptor.tag == null) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_record_field_type, ast.node_span(&state.tree, type)) return false } if (descriptor.tag) |tag_annotation| { tag = try canonical_type(state, tag_annotation) if (tag == no_node or ast.expr_kind(&state.tree, tag) != .enum_type) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_union_tag, ast.node_span(&state.tree, tag_annotation)) return false } if (!try type_record_check(state, tag)) return false } } for (0..ast.record_field_count(&state.tree, type)) |i| { field_id :: ast.record_field(&state.tree, type, i) field :: ast.field(&state.tree, field_id) if (!try type_step(state, field_id)) return false if (tagged) { if (field.type == null or field.value != null) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_record_field_type, field.span) return false } if (field.name == null) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .duplicate_field, field.span) return false } if (tag != no_node) { found := false for (0..ast.enum_member_count(&state.tree, tag)) |j| { member :: ast.field(&state.tree, ast.enum_member(&state.tree, tag, j)) if (member.name != null and ast.name_id(&state.tree, member.name?) == ast.name_id(&state.tree, field.name?)) { found = true break } } if (!found) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_union_tag, field.span) return false } } } if (field.name != null and type_record_slot(&state.tree, type, field.name?) != i) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .duplicate_field, field.span) return false } if (ast.expr_kind(&state.tree, type) == .union_type) { payload :: try canonical_type(state, field.type?) if ( payload != no_node and binding(state, payload).kind == .builtin and binding(state, payload).data.builtin == .void ) continue } if (type_record_storage(state, field.type?, 0)) { if (!try type_record_check(state, field.type?)) return false if (field.value != null and try type_execution_value(state, field.value?, field.type?) == no_node) return false } else { fields [1]ast.NodeId :: [field_id] record :: try ast.add_record_type(state.builder, field.span, fields[..]) constraint :: try ast.add_type_operand(state.builder, field.span, record, true) try refresh(state) try arraylist.append(&state.rewrites.constraints, constraint) if (field.value != null and try type_execution_value(state, field.value?, field.type?) == no_node) return false } } return true } # normalize record snapshots in declaration order through their declared context. # undefined records retain per-leaf initialization state without applying defaults. @hide:file type_record_value proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { type :: try canonical_type(state, annotation) if (ast.expr_kind(&state.tree, type) == .union_type) return try type_union_value(state, expression, type) if (ast.expr_kind(&state.tree, expression) == .literal_undefined) return try type_storage_undefined(state, expression, annotation) if (ast.expr_kind(&state.tree, expression) == .typed_constant) { actual :: ast.typed_constant(&state.tree, expression) if (try same_type(state, type, actual.type)) return expression } else if (ast.expr_kind(&state.tree, expression) == .record_literal) { if (!try type_record_check(state, type)) return no_node literal :: ast.record_literal(&state.tree, expression) if (literal.type != null and !try same_type(state, type, literal.type?)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, ast.node_span(&state.tree, expression)) return no_node } count :: ast.record_field_count(&state.tree, type) values :: try mem.alloc(ast.NodeId, state.allocator, count) defer mem.free(state.allocator, values) mem.set(values, no_node) for (0..ast.record_field_count(&state.tree, expression)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, expression, i)) slot := i if (field.name) |name| slot = type_record_slot(&state.tree, type, name) if (slot >= count or values[slot] != no_node) { code :: if (slot < count) diagnostics.Code.duplicate_field else if (field.name != null) diagnostics.Code.unknown_field else diagnostics.Code.tuple_arity_mismatch state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, field.span) return no_node } values[slot] = field.value? } fields := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&fields) for (0..count) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i)) if (field.name) |name| { if (type_record_slot(&state.tree, type, name) != i) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .duplicate_field, field.span) return no_node } } input := values[i] if (input == no_node and field.value != null) input = field.value? if (input == no_node) { code :: if (field.name == null) diagnostics.Code.tuple_arity_mismatch else diagnostics.Code.missing_field state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, ast.node_span(&state.tree, expression)) return no_node } if (!try type_step(state, input)) return no_node value :: try type_execution_value(state, input, field.type?) if (value == no_node) return no_node generated :: try ast.add_field(state.builder, field.span, field.name, null, value) try refresh(state) try arraylist.append(&fields, generated) } span :: ast.node_span(&state.tree, expression) normalized :: try ast.add_record_literal(state.builder, span, null, fields.items) result :: try ast.add_typed_constant(state.builder, span, type, normalized) try refresh(state) return result } state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, ast.node_span(&state.tree, expression)) return no_node } @hide:file type_array_index proc(state @mut State, expression ast.NodeId, count usize, boundary bool) usize ! Error { usize_type :: try builtin(state, expression, .usize) actual :: try expression_type(state, expression, 0) if (!flexible(state, expression) and (actual == no_node or !try same_type(state, actual, usize_type))) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_index_type, ast.node_span(&state.tree, expression)) return no_index } value :: try evaluate_ir(state, expression, usize_type) if (value.kind != .integer) return no_index if (value.magnitude > u64(count) or (!boundary and value.magnitude == u64(count))) { code diagnostics.Code :: if (boundary) .invalid_slice_bounds else .index_out_of_bounds state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, ast.node_span(&state.tree, expression)) return no_index } return usize(value.magnitude) } @hide:file type_array_compatible proc(state @mut State, expected, actual ast.NodeId) bool ! Error { if (is_type(state, expected) or is_type(state, actual)) return is_type(state, expected) and is_type(state, actual) left :: try canonical_type(state, expected) right :: try canonical_type(state, actual) if (left == no_node or right == no_node) return false if (ast.expr_kind(&state.tree, left) != .array_type or ast.expr_kind(&state.tree, right) != .array_type) return try same_type(state, left, right) a :: ast.array_type(&state.tree, left) b :: ast.array_type(&state.tree, right) if (a.mutable != b.mutable or b.count == null) return false if (a.count) |count| { if (!try value_equal(state, try evaluate(state, count), try evaluate(state, b.count?))) return false } return try type_array_compatible(state, a.element, b.element) } # undefined leaves are initialization state, not type values. snapshots may copy # that state locally; procedure boundaries require every leaf to be initialized. @hide:file type_array_initialized proc(state @mut State, value, at ast.NodeId) bool ! Error { kind :: ast.expr_kind(&state.tree, value) if (kind == .literal_undefined) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, ast.node_span(&state.tree, at)) return false } if (kind == .typed_constant) return try type_array_initialized(state, ast.typed_constant(&state.tree, value).value, at) if (kind == .builtin_call and contextual_literal(state, value)) return try type_array_initialized(state, ast.call_arg(&state.tree, value, 0), at) if (kind == .array_literal) { for (0..ast.array_element_count(&state.tree, value)) |i| { if (!try type_array_initialized(state, ast.array_element(&state.tree, value, i), at)) return false } } if (kind == .record_literal) { for (0..ast.record_field_count(&state.tree, value)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, value, i)) if (field.value != null and !try type_array_initialized(state, field.value?, at)) return false } } return true } # bound the expanded snapshot, not just its dimensions, so later reads and # initialization checks cannot traverse an exponentially shared empty shape. @hide:file type_storage_undefined proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { if (is_type(state, annotation)) return expression type :: try canonical_type(state, annotation) if ( type != no_node and ast.expr_kind(&state.tree, type) == .union_type and type_record_storage(state, type, 0) ) { if (!try type_record_check(state, type)) return no_node result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, expression), type, expression) try refresh(state) return result } if (type != no_node and ast.expr_kind(&state.tree, type) == .record_type) { if (!try type_record_check(state, type)) return no_node fields := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&fields) for (0..ast.record_field_count(&state.tree, type)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, type, i)) if (!try type_step(state, expression)) return no_node value :: try type_storage_undefined(state, expression, field.type?) if (value == no_node) return no_node generated :: try ast.add_field(state.builder, field.span, field.name, null, value) try refresh(state) try arraylist.append(&fields, generated) } span :: ast.node_span(&state.tree, expression) literal :: try ast.add_record_literal(state.builder, span, null, fields.items) result :: try ast.add_typed_constant(state.builder, span, type, literal) try refresh(state) return result } if (type == no_node or ast.expr_kind(&state.tree, type) != .array_type) return expression descriptor :: ast.array_type(&state.tree, type) length := Value{} if (descriptor.count) |count| length = try evaluate(state, count) if (length.kind != .integer or length.negative) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_array_length, ast.node_span(&state.tree, annotation)) return no_node } # validate child dimensions even when this dimension is empty. child :: try type_storage_undefined(state, expression, descriptor.element) if (child == no_node) return no_node elements := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&elements) for (0..length.magnitude) |i| { if (!try type_step(state, expression)) return no_node element := child if (i != 0) element = try type_storage_undefined(state, expression, descriptor.element) if (element == no_node) return no_node try arraylist.append(&elements, element) } span :: ast.node_span(&state.tree, expression) type_span :: ast.node_span(&state.tree, annotation) element_type :: if (ast.expr_kind(&state.tree, child) == .typed_constant) ast.typed_constant(&state.tree, child).type else descriptor.element count :: try ast.add_integer(state.builder, span, length.magnitude) concrete :: try ast.add_array_type(state.builder, type_span, count, element_type, descriptor.mutable) literal :: try ast.add_array_literal(state.builder, span, elements.items) result :: try ast.add_typed_constant(state.builder, span, concrete, literal) try refresh(state) return result } @hide:file type_array_value proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { if (ast.expr_kind(&state.tree, expression) == .literal_undefined) return try type_storage_undefined(state, expression, annotation) type :: try canonical_type(state, annotation) array :: ast.array_type(&state.tree, type) if (ast.expr_kind(&state.tree, expression) == .typed_constant) { actual :: ast.typed_constant(&state.tree, expression) if (try type_array_compatible(state, type, actual.type)) return expression if (!try type_array_initialized(state, expression, expression)) return no_node } else if (ast.expr_kind(&state.tree, expression) == .array_literal) { count :: ast.array_element_count(&state.tree, expression) if (array.count) |length| { value :: try evaluate(state, length) if (value.kind != .integer or value.negative) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_array_length, ast.node_span(&state.tree, length)) return no_node } if (value.magnitude != u64(count)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .array_length_mismatch, ast.node_span(&state.tree, expression)) return no_node } } elements := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&elements) element_type := array.element for (0..count) |i| { element :: try type_execution_value(state, ast.array_element(&state.tree, expression, i), element_type) if (element == no_node) return no_node if (ast.expr_kind(&state.tree, element) == .typed_constant) element_type = ast.typed_constant(&state.tree, element).type try arraylist.append(&elements, element) } # empty outer arrays still need concrete, valid child dimensions. if (count == 0) { child := element_type while (!is_type(state, child)) { child = try canonical_type(state, child) if (child == no_node or ast.expr_kind(&state.tree, child) != .array_type) break descriptor :: ast.array_type(&state.tree, child) value := Value{} if (descriptor.count) |length| value = try evaluate(state, length) if (value.kind != .integer or value.negative) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_array_length, ast.node_span(&state.tree, child)) return no_node } child = descriptor.element } if (type_record_storage(state, child, 0) and !try type_record_check(state, child)) return no_node } span :: ast.node_span(&state.tree, expression) length :: try ast.add_integer(state.builder, span, u64(count)) concrete :: try ast.add_array_type(state.builder, ast.node_span(&state.tree, annotation), length, element_type, array.mutable) literal :: try ast.add_array_literal(state.builder, span, elements.items) result :: try ast.add_typed_constant(state.builder, span, concrete, literal) try refresh(state) return result } if (type_record_storage(state, annotation, 0)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, ast.node_span(&state.tree, expression)) return no_node } value :: try evaluate_ir(state, expression, annotation) if (value.kind == .invalid) return no_node result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, expression), annotation, value.node) try refresh(state) return result } # readonly metadata views own their leaves. mutable backing needs borrowing semantics, # not a snapshot that silently stops observing later element writes. @hide:file type_slice_value proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { if (state.type_execution_depth == 0) state.type_execution_remaining = 4096 state.type_execution_depth += 1 defer state.type_execution_depth -= 1 type :: try canonical_type(state, annotation) if (!try type_record_check(state, type)) return no_node descriptor :: ast.slice_type(&state.tree, type) span :: ast.node_span(&state.tree, expression) kind :: ast.expr_kind(&state.tree, expression) if (kind == .literal_undefined) { result :: try ast.add_typed_constant(state.builder, span, type, expression) try refresh(state) return result } if (kind == .typed_constant) { actual :: ast.typed_constant(&state.tree, expression) if (try same_type(state, type, actual.type)) return expression } source := expression start := usize(0) end := no_index if (kind == .slice) { view :: ast.slice_expr(&state.tree, expression) if (view.sentinel != null) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_operand, span) return no_node } source = view.base } if (ast.expr_kind(&state.tree, source) == .array_literal) { for (0..ast.array_element_count(&state.tree, source)) |_| { if (!try type_step(state, expression)) return no_node } length :: try ast.add_integer(state.builder, span, u64(ast.array_element_count(&state.tree, source))) array :: try ast.add_array_type(state.builder, span, length, descriptor.element, false) try refresh(state) source = try type_array_value(state, source, array) } else { actual :: try expression_type(state, source, 0) if (actual == no_node) return no_node actual_kind :: ast.expr_kind(&state.tree, actual) if ((actual_kind != .array_type and actual_kind != .slice_type) or (actual_kind == .array_type and kind != .slice)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, span) return no_node } mutable :: if (actual_kind == .array_type) ast.array_type(&state.tree, actual).mutable else ast.slice_type(&state.tree, actual).mutable element :: if (actual_kind == .array_type) ast.array_type(&state.tree, actual).element else ast.slice_type(&state.tree, actual).element if (mutable) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span) return no_node } if (!try same_type(state, descriptor.element, element)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, span) return no_node } source = try type_execution_value(state, source, actual) } if (source == no_node) return no_node values :: ast.typed_constant(&state.tree, source).value if (ast.expr_kind(&state.tree, values) == .literal_undefined) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span) return no_node } count :: ast.array_element_count(&state.tree, values) end = count if (kind == .slice) { view :: ast.slice_expr(&state.tree, expression) if (view.start) |bound| start = try type_array_index(state, bound, count, true) if (start == no_index) return no_node if (view.end) |bound| end = try type_array_index(state, bound, count, true) if (end == no_index) return no_node if (start > end) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_slice_bounds, span) return no_node } } elements := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&elements) for (start..end) |i| { if (!try type_step(state, expression)) return no_node try arraylist.append(&elements, ast.array_element(&state.tree, values, i)) } literal :: try ast.add_array_literal(state.builder, span, elements.items) result :: try ast.add_typed_constant(state.builder, span, type, literal) try refresh(state) return result } # rebuild only the field/index path so previous snapshots retain their owned values. @hide:file type_array_update proc( state @mut State, execution @mut TypeExecution, target, root, current, input ast.NodeId, ) ast.NodeId ! Error { # find the outermost projection; rebuild its child before copying this parent. first := target while (true) { kind :: ast.expr_kind(&state.tree, first) if (kind != .index and kind != .selection) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_assignment_target, ast.node_span(&state.tree, target)) return no_node } base :: if (kind == .index) ast.index_expr(&state.tree, first).base else ast.selection_expr(&state.tree, first).base if (base == root) break first = base } array :: ast.typed_constant(&state.tree, current) type :: try canonical_type(state, array.type) if ( type == no_node or (ast.expr_kind(&state.tree, type) != .record_type and ast.expr_kind(&state.tree, type) != .array_type and (ast.expr_kind(&state.tree, type) != .union_type or !type_record_storage(state, type, 0))) ) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_index_assignment, ast.node_span(&state.tree, target)) return no_node } tagged :: ast.expr_kind(&state.tree, type) == .union_type record :: ast.expr_kind(&state.tree, type) == .record_type or tagged if (tagged and ast.expr_kind(&state.tree, array.value) == .literal_undefined) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, ast.node_span(&state.tree, target)) return no_node } index := no_index element_type := no_node count := usize(0) previous := no_node if (record) { if (ast.expr_kind(&state.tree, first) == .selection) index = type_record_slot(&state.tree, type, ast.selection_expr(&state.tree, first).member) if (index == no_index) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .unknown_field, ast.node_span(&state.tree, first)) return no_node } element_type = ast.field(&state.tree, ast.record_field(&state.tree, type, index)).type? count = ast.record_field_count(&state.tree, array.value) if (tagged) { active :: ast.field(&state.tree, ast.record_field(&state.tree, array.value, 0)) member :: ast.field(&state.tree, ast.record_field(&state.tree, type, index)).name? if (active.name == null or ast.name_id(&state.tree, active.name?) != ast.name_id(&state.tree, member)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .inactive_union_field, ast.node_span(&state.tree, first)) return no_node } index = 0 } previous = ast.field(&state.tree, ast.record_field(&state.tree, array.value, index)).value? } else { descriptor :: ast.array_type(&state.tree, type) if (!descriptor.mutable or ast.expr_kind(&state.tree, first) != .index) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_index_assignment, ast.node_span(&state.tree, target)) return no_node } expression :: try type_snapshot(state, execution, ast.index_expr(&state.tree, first).index) if (!try expand_type_matches(state, expression)) return no_node count = ast.array_element_count(&state.tree, array.value) index = try type_array_index(state, expression, count, false) if (index == no_index) return no_node element_type = descriptor.element previous = ast.array_element(&state.tree, array.value, index) } updated := no_node if (first == target) updated = try type_execution_value(state, input, element_type) else { if (ast.expr_kind(&state.tree, previous) != .typed_constant) { previous = try type_execution_value(state, previous, element_type) if (previous == no_node) return no_node } updated = try type_array_update(state, execution, target, first, previous, input) } if (updated == no_node) return no_node elements := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&elements) for (0..count) |i| { element := no_node if (record) { field :: ast.field(&state.tree, ast.record_field(&state.tree, array.value, i)) value :: if (i == index) updated else field.value? element = try ast.add_field(state.builder, field.span, field.name, null, value) try refresh(state) } else element = if (i == index) updated else ast.array_element(&state.tree, array.value, i) try arraylist.append(&elements, element) } span :: ast.node_span(&state.tree, target) literal := no_node if (record) literal = try ast.add_record_literal(state.builder, span, null, elements.items) else literal = try ast.add_array_literal(state.builder, span, elements.items) result :: try ast.add_typed_constant(state.builder, span, array.type, literal) try refresh(state) return result } # optional metadata uses the same explicit presence encoding as serialized values. # an inner null is a payload only when wrapped in some!, never outer absence. @hide:file type_optional_value proc(state @mut State, expression, type ast.NodeId) ast.NodeId ! Error { if (!try type_record_check(state, type)) return no_node kind :: ast.expr_kind(&state.tree, expression) if (kind == .literal_undefined or kind == .literal_null) { result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, expression), type, expression) try refresh(state) return result } child :: ast.type_operand(&state.tree, type) input := expression if (kind == .typed_constant) { actual :: ast.typed_constant(&state.tree, expression) if (try same_type(state, type, actual.type)) return expression } else if (kind == .builtin_call and mem.eql(ast.literal_text(&state.tree, ast.builtin_call(&state.tree, expression).name), "some")) { if (ast.call_arg_count(&state.tree, expression) != 1) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .argument_count_mismatch, ast.node_span(&state.tree, expression)) return no_node } input = ast.call_arg(&state.tree, expression, 0) } payload :: try type_execution_value(state, input, child) if (payload == no_node) return no_node span :: ast.node_span(&state.tree, expression) name :: try metadata_member(state, expression, "some") arguments [1]ast.NodeId :: [payload] present :: try ast.add_builtin_call(state.builder, span, name, arguments[..]) result :: try ast.add_typed_constant(state.builder, span, type, present) try refresh(state) return result } @hide:file type_variant proc(state @mut State, annotation, name ast.NodeId) bool ! Error { type :: try canonical_type(state, annotation) if (type == no_node) return false kind :: ast.expr_kind(&state.tree, type) if (kind == .sum_type) { members :: ast.binary_expr(&state.tree, type) return try type_variant(state, members.left, name) or try type_variant(state, members.right, name) } if (kind == .union_type) return type_record_slot(&state.tree, type, name) != no_index if (kind != .enum_type) return false for (0..ast.enum_member_count(&state.tree, type)) |i| { member :: ast.field(&state.tree, ast.enum_member(&state.tree, type, i)).name? if (ast.name_id(&state.tree, member) == ast.name_id(&state.tree, name)) return true } return false } # the payload's declared channel is retained even when success and error values # have overlapping shapes. no type-bearing snapshot enters runtime IR. @hide:file type_fallible_value proc(state @mut State, expression, type ast.NodeId) ast.NodeId ! Error { if (!try type_record_check(state, type)) return no_node span :: ast.node_span(&state.tree, expression) kind :: ast.expr_kind(&state.tree, expression) if (kind == .literal_undefined) { result :: try ast.add_typed_constant(state.builder, span, type, expression) try refresh(state) return result } channels :: ast.binary_expr(&state.tree, type) actual :: try expression_type(state, expression, 0) if (actual != no_node and ast.expr_kind(&state.tree, actual) == .fallible_type) { if (try same_type(state, actual, type)) return expression source :: ast.binary_expr(&state.tree, actual) if (!try same_type(state, source.left, channels.left) or !try type_subset(state, source.right, channels.right)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, span) return no_node } return try type_fallible_value(state, ast.typed_constant(&state.tree, expression).value, type) } channel := channels.left if (actual != no_node and !try same_type(state, actual, channels.left) and try type_subset(state, actual, channels.right)) channel = channels.right name := no_node if (kind == .enum_literal) name = ast.enum_literal(&state.tree, expression) if (kind == .record_literal and ast.record_literal(&state.tree, expression).type != null) { tag :: ast.record_literal(&state.tree, expression).type? if (ast.expr_kind(&state.tree, tag) == .enum_literal) name = ast.enum_literal(&state.tree, tag) } if (name != no_node and try type_variant(state, channels.right, name) and !try type_variant(state, channels.left, name)) channel = channels.right if (kind == .record_literal and ast.record_literal(&state.tree, expression).type == null) { success :: try canonical_type(state, channels.left) failure :: try canonical_type(state, channels.right) if ((success == no_node or ast.expr_kind(&state.tree, success) != .record_type) and ast.expr_kind(&state.tree, failure) == .record_type) channel = channels.right } payload := try type_execution_value(state, expression, channel) if (payload == no_node) return no_node if (ast.expr_kind(&state.tree, payload) != .typed_constant) { payload = try ast.add_typed_constant(state.builder, span, channel, payload) try refresh(state) } result :: try ast.add_typed_constant(state.builder, span, type, payload) try refresh(state) return result } @hide:file type_sum_value proc(state @mut State, expression, type ast.NodeId) ast.NodeId ! Error { if (!try type_record_check(state, type)) return no_node actual :: try expression_type(state, expression, 0) if (actual != no_node and try same_type(state, actual, type)) return expression shape :: try inline_match_shape(state, type) if (shape == no_node) return no_node span :: ast.node_span(&state.tree, expression) input := expression if (actual != no_node and try type_subset(state, actual, type)) { source :: try canonical_type(state, actual) if (ast.expr_kind(&state.tree, source) == .record_type) { for (0..ast.record_field_count(&state.tree, shape)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, shape, i)) if (!try same_type(state, field.type?, actual)) continue member :: try ast.add_field(state.builder, span, field.name, null, expression) fields [1]ast.NodeId :: [member] input = try ast.add_record_literal(state.builder, span, null, fields[..]) try refresh(state) break } } else if (ast.expr_kind(&state.tree, expression) == .typed_constant) input = ast.typed_constant(&state.tree, expression).value } value :: try type_union_value(state, input, shape) if (value == no_node) return no_node result :: try ast.add_typed_constant(state.builder, span, type, ast.typed_constant(&state.tree, value).value) try refresh(state) return result } @hide:file type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.NodeId ! Error { if (input == no_node or annotation == no_node) return no_node if (ast.expr_kind(&state.tree, input) == .block_expr and (is_type(state, annotation) or type_record_storage(state, annotation, 0))) return try evaluate_type_value_block(state, input, annotation) expression :: try type_value_expression(state, input) if (expression == no_node) return no_node if (!try expand_type_matches(state, annotation)) return no_node # a recursive record needs its cached declaration before expanding its fields. if (is_type(state, annotation) and nominal_type_value(state, expression)) { concrete :: try materialize(state, Value{ kind = .type, node = expression }) if (type_record_storage(state, expression, 0)) { if (!try type_record_check(state, expression)) return no_node return concrete.node } constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, expression), concrete.node, true) try refresh(state) try arraylist.append(&state.rewrites.constraints, constraint) return concrete.node } if (!try expand_type_matches(state, expression)) return no_node try expand_expected(state, expression, annotation) resolved :: binding(state, expression) if (resolved.kind == .invalid) { state.evaluation_diagnostic = resolved.data.diagnostic return no_node } if (!is_type(state, annotation) and type_record_storage(state, annotation, 0) and resolved.kind == .declaration and ast.node_class(&state.tree, resolved.data.declaration) == .statement) { declaration :: ast.statement_decl(&state.tree, resolved.data.declaration) if (declaration.mutability != .constant or !try type_step(state, expression)) return no_node if (declaration.type != null and !try same_type(state, annotation, declaration.type?)) { if (try metadata_sequence(state, annotation)) return try type_slice_value(state, expression, annotation) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, ast.node_span(&state.tree, expression)) return no_node } return try type_execution_value(state, declaration.initializer, annotation) } if (try execution_array_storage(state, annotation)) { value :: try type_array_value(state, expression, annotation) if (value != no_node and !type_record_storage(state, annotation, 0)) { # erased snapshots still owe ordinary array-element/layout checking. constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, expression), ast.typed_constant(&state.tree, value).type, true) try refresh(state) try arraylist.append(&state.rewrites.constraints, constraint) } return value } if (try metadata_sequence(state, annotation)) return try type_slice_value(state, expression, annotation) if (!is_type(state, annotation) and type_record_storage(state, annotation, 0)) { type :: try canonical_type(state, annotation) if (type != no_node and ast.expr_kind(&state.tree, type) == .optional_type) return try type_optional_value(state, expression, type) if (type != no_node and ast.expr_kind(&state.tree, type) == .fallible_type) return try type_fallible_value(state, expression, type) if (type != no_node and ast.expr_kind(&state.tree, type) == .sum_type) return try type_sum_value(state, expression, type) } if (!is_type(state, annotation) and type_record_storage(state, annotation, 0)) return try type_record_value(state, expression, annotation) if (is_type(state, annotation)) { if (ast.expr_kind(&state.tree, expression) == .literal_undefined) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, ast.node_span(&state.tree, expression)) return no_node } value :: try evaluate(state, expression) if (value.kind != .type) { if (state.evaluation_diagnostic == diagnostics.no_diagnostic) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, ast.node_span(&state.tree, expression)) return no_node } concrete :: try materialize(state, value) if (type_record_storage(state, concrete.node, 0)) { if (!try type_record_check(state, concrete.node)) return no_node return concrete.node } constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, expression), concrete.node, true) try refresh(state) try arraylist.append(&state.rewrites.constraints, constraint) 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 ( value.kind == .string and try valid_value(state, annotation, value) and (value.annotation == no_node or try same_type(state, annotation, value.annotation)) ) { result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, expression), annotation, value.node) try refresh(state) return result } } # check the leaf as an assignment, not as the evaluation wrapper's return: # exact value-type failures belong to the source leaf and use type_mismatch. contextual :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, expression), annotation, expression) try refresh(state) value :: try evaluate_ir(state, contextual, annotation) if (value.kind == .invalid) return no_node concrete :: try materialize(state, value) result :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, expression), annotation, concrete.node) try refresh(state) return result } # check capture guards with ordinary IR without elaborating either source body. @hide:file type_capture_guard proc(state @mut State, expression, capture ast.NodeId) Value ! Error { resolved :: binding(state, capture) if (resolved.kind == .invalid) { state.evaluation_diagnostic = resolved.data.diagnostic return Value{} } annotation :: try expression_type(state, expression, 0) if (type_record_storage(state, annotation, 0)) { type :: try canonical_type(state, annotation) if (type != no_node and ast.expr_kind(&state.tree, type) == .optional_type) { value :: try type_execution_value(state, expression, type) if (value == no_node or !try type_array_initialized(state, value, expression)) return Value{} return Value{ kind = .boolean, node = expression, boolean = ast.expr_kind(&state.tree, ast.typed_constant(&state.tree, value).value) != .literal_null } } } span :: ast.node_span(&state.tree, expression) yes :: try ast.add_boolean(state.builder, span, true) no :: try ast.add_boolean(state.builder, span, false) taken :: try ast.add_return_stmt(state.builder, span, yes) skipped :: try ast.add_return_stmt(state.builder, span, no) taken_statements [1]ast.NodeId :: [taken] skipped_statements [1]ast.NodeId :: [skipped] then_block :: try ast.add_block(state.builder, span, taken_statements[..]) else_block :: try ast.add_block(state.builder, span, skipped_statements[..]) selector :: try ast.add_if_stmt(state.builder, span, expression, then_block, else_block) ast.set_capture(state.builder, selector, capture) statements [1]ast.NodeId :: [selector] body :: try ast.add_block(state.builder, span, statements[..]) try refresh(state) return try evaluate_ir_body(state, expression, body, try builtin(state, expression, .bool)) } # update one owned aggregate through ordinary assignment checking and execution. # only the target's root becomes a temporary local; indices and RHS retain their # pre-assignment snapshots, and no interpreter storage survives the evaluation. @hide:file type_assignment_value proc( state @mut State, execution @mut TypeExecution, statement ast.NodeId, index usize, ) ast.NodeId ! Error { assignment :: ast.assignment_stmt(&state.tree, statement) local :: execution.locals.items[index] span :: ast.node_span(&state.tree, statement) value :: try type_snapshot(state, execution, assignment.value) if (value == no_node) 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 temporary :: try ast.add_statement_decl(state.builder, span, span, strpool.no_str, local.annotation, mutability, local.value) try refresh(state) target := no_node { execution.locals.items[index].value = try reference(state, assignment.target, temporary) defer execution.locals.items[index].value = local.value target = try type_snapshot(state, execution, assignment.target) } if (!try expand_type_matches(state, target) or !try expand_type_matches(state, value)) return no_node update :: try ast.add_assignment_stmt(state.builder, span, target, value) returned :: try ast.add_return_stmt(state.builder, span, try reference(state, statement, temporary)) statements [3]ast.NodeId :: [temporary, update, returned] body :: try ast.add_block(state.builder, span, statements[..]) try refresh(state) result :: try evaluate_ir_body(state, statement, body, local.annotation) if (result.kind == .invalid) return no_node concrete :: try materialize(state, result) owned :: try ast.add_typed_constant(state.builder, span, local.annotation, concrete.node) try refresh(state) 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 (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 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) if (flow == .invalid) return flow try arraylist.append(execution.inline_blocks?, snapshot) return flow } saved :: execution.locals.items.len defer execution.locals.items = execution.locals.items[..saved] reached := usize(0) flow := TypeFlow.next 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 .invalid reached = i + 1 flow = try execute_type_statement(state, execution, statement) if (flow != .next) break } if (flow == .invalid) return flow # yielded/returned values already own their snapshots before scope cleanup runs. while (reached > 0) { reached -= 1 statement :: ast.block_statement(&state.tree, block, reached) kind :: ast.stmt_kind(&state.tree, statement) if (kind != .defer_stmt and !(kind == .errdefer_stmt and execution.failed)) continue saved_depth :: execution.cleanup_yield_depth saved_loop :: execution.cleanup_loop_depth execution.cleanup_loop_depth = execution.loop_depth execution.cleanup_yield_depth = execution.yield_depth execution.cleanup_depth += 1 cleanup_view :: ast.cleanup_stmt(&state.tree, statement) if (kind == .errdefer_stmt and cleanup_view.capture != null) { annotation :: ast.binary_expr(&state.tree, try canonical_type(state, execution.result_type)).right value :: ast.typed_constant(&state.tree, execution.returned).value try arraylist.append(&execution.locals, TypeLocal{ declaration = cleanup_view.capture?, value = value, annotation = annotation, }) } cleanup :: try execute_type_block(state, execution, cleanup_view.body) if (kind == .errdefer_stmt and cleanup_view.capture != null) execution.locals.items = execution.locals.items[..execution.locals.items.len - 1] execution.cleanup_depth -= 1 execution.cleanup_yield_depth = saved_depth execution.cleanup_loop_depth = saved_loop if (cleanup != .next) return .invalid } return flow } # substitute selector names without snapshotting away the assignment's root place. @hide:file type_assignment_target proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error { kind :: ast.expr_kind(&state.tree, id) if (kind == .builtin_call) { name :: ast.builtin_call(&state.tree, id).name if (!mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "field")) return id if (ast.call_arg_count(&state.tree, id) != 2) { try reject(state, id, .argument_count_mismatch) return no_node } base :: try type_assignment_target(state, execution, ast.call_arg(&state.tree, id, 0)) if (base == no_node) return no_node argument :: try type_snapshot(state, execution, ast.call_arg(&state.tree, id, 1)) if (argument == no_node) return no_node arguments [2]ast.NodeId :: [base, argument] result :: try ast.add_builtin_call(state.builder, ast.node_span(&state.tree, id), name, arguments[..]) try refresh(state) try expand(state, result) if (binding(state, result).kind == .invalid) return no_node return result } if (kind != .selection and kind != .index) return id base :: if (kind == .selection) ast.selection_expr(&state.tree, id).base else ast.index_expr(&state.tree, id).base selected :: try type_assignment_target(state, execution, base) if (selected == no_node or selected == base) return if (selected == no_node) no_node else id result := no_node if (kind == .selection) result = try ast.add_selection(state.builder, ast.node_span(&state.tree, id), selected, ast.selection_expr(&state.tree, id).member) else result = try ast.add_index(state.builder, ast.node_span(&state.tree, id), selected, ast.index_expr(&state.tree, id).index) try refresh(state) return result } @hide:file execute_type_statement proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error { if (execution.cleanup_depth > 0) { kind :: ast.stmt_kind(&state.tree, statement) if (kind == .return_stmt or ((kind == .break_stmt or kind == .continue_stmt) and execution.loop_depth <= execution.cleanup_loop_depth)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .cleanup_control_flow, ast.node_span(&state.tree, statement)) return .invalid } } flow :: try execute_type_statement_body(state, execution, statement) if (execution.pending_flow == .next) return flow pending :: execution.pending_flow execution.pending_flow = .next return pending } @hide:file execute_type_statement_body proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error { match ast.stmt_kind(&state.tree, statement) { .unreachable_stmt: { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .unreachable_executed, ast.node_span(&state.tree, statement)) return .invalid } .defer_stmt: {} .errdefer_stmt: { result :: try canonical_type(state, execution.result_type) if (result == no_node or ast.expr_kind(&state.tree, result) != .fallible_type) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .errdefer_outside_fallible, ast.node_span(&state.tree, statement)) return .invalid } } .yield_stmt: { if (execution.yield_depth == 0) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .yield_outside_block, ast.node_span(&state.tree, statement)) return .invalid } if (execution.cleanup_depth > 0 and execution.yield_depth <= execution.cleanup_yield_depth) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .cleanup_control_flow, ast.node_span(&state.tree, statement)) return .invalid } value :: ast.yield_stmt(&state.tree, statement).value? expression :: try type_snapshot_expected(state, execution, value, execution.yield_type) if (expression == no_node) return .invalid annotation := execution.yield_type if (annotation == no_node) annotation = try expression_type(state, expression, 0) execution.yielded = try type_execution_value(state, expression, annotation) if (execution.yielded == no_node or !try type_array_initialized(state, execution.yielded, value)) return .invalid return .yielded } .return_stmt: { if (ast.return_stmt(&state.tree, statement).value) |value| { expression :: try type_snapshot_expected(state, execution, value, execution.result_type) if (expression == no_node) return .invalid execution.returned = try type_execution_value(state, expression, execution.result_type) if (execution.returned != no_node and !try type_array_initialized(state, execution.returned, value)) return .invalid if (execution.returned != no_node) { type :: try canonical_type(state, execution.result_type) if (type != no_node and ast.expr_kind(&state.tree, type) == .fallible_type) { failed :: try type_fallible_failed(state, execution.returned, type, value) if (failed == null) return .invalid execution.failed = failed? } } if (execution.returned != no_node) return .returned } else { execution.returned = try type_void_completion(state, execution.result_type, statement) if (execution.returned != no_node) return .returned } return .invalid } .declaration: { declaration :: ast.statement_decl(&state.tree, statement) annotation := no_node if (declaration.type) |type| annotation = try type_snapshot(state, execution, type) snapshot :: try type_snapshot_expected(state, execution, declaration.initializer, annotation) expression :: try type_value_expression(state, snapshot) if (expression == no_node) return .invalid if (annotation == no_node and nominal_type_value(state, expression)) annotation = try builtin(state, expression, .type) else if (annotation == no_node) { if (!try expand_type_matches(state, expression)) return .invalid try expand(state, expression) annotation = try expression_type(state, expression, 0) if (annotation == no_node) { value :: try evaluate(state, expression) if (value.kind == .type) annotation = try builtin(state, expression, .type) } } if (annotation == no_node) return .invalid value :: try type_execution_value(state, expression, annotation) if (value == no_node) return .invalid if (ast.expr_kind(&state.tree, value) == .typed_constant) 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: { assignment :: ast.assignment_stmt(&state.tree, statement) target :: try type_assignment_target(state, execution, assignment.target) if (target == no_node) return .invalid if (target != assignment.target) { normalized :: try ast.add_assignment_stmt(state.builder, ast.node_span(&state.tree, statement), target, assignment.value) if (assignment.compound) ast.set_assignment_compound(state.builder, normalized) try refresh(state) # synthesized places still need local substitution in snapshots. previous :: execution.mapping.len execution.mapping = try mem.realloc(state.allocator, execution.mapping, ast.node_count(&state.tree)) mem.set(execution.mapping[previous..], no_node) return try execute_type_statement(state, execution, normalized) } root := assignment.target indexed := true while (true) { kind :: ast.expr_kind(&state.tree, root) if (kind == .selection and binding(state, root).kind == .none) { indexed = false root = ast.selection_expr(&state.tree, root).base } else if (kind == .index) root = ast.index_expr(&state.tree, root).base else break } resolved :: binding(state, root) index := no_index if (resolved.kind == .declaration) { for (execution.locals.items) |local, i| { if (local.declaration == resolved.data.declaration) index = i } } if ( index == no_index or (root == assignment.target and (ast.node_class(&state.tree, execution.locals.items[index].declaration) != .statement or ast.statement_decl(&state.tree, execution.locals.items[index].declaration).mutability != .variable)) ) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_assignment_target, ast.node_span(&state.tree, assignment.target)) return .invalid } if (root != assignment.target) { if ((indexed and try execution_array_storage(state, execution.locals.items[index].annotation)) or type_record_storage(state, execution.locals.items[index].annotation, 0)) { local :: execution.locals.items[index] if ( ast.node_class(&state.tree, local.declaration) != .statement or ast.statement_decl(&state.tree, local.declaration).mutability != .variable ) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_index_assignment, ast.node_span(&state.tree, assignment.target)) return .invalid } input :: try type_snapshot(state, execution, assignment.value) if (!try expand_type_matches(state, input)) return .invalid value :: try type_array_update(state, execution, assignment.target, root, local.value, input) if (value == no_node) return .invalid execution.locals.items[index].value = value return .next } value :: try type_assignment_value(state, execution, statement, index) if (value == no_node) return .invalid execution.locals.items[index].value = value return .next } 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 condition := no_node body := no_node other := no_node capture ?ast.NodeId := null if (looping) { view :: ast.while_stmt(&state.tree, statement) capture = view.capture condition = view.condition body = view.body } else { view :: ast.if_stmt(&state.tree, statement) capture = view.capture condition = view.condition body = view.then_block if (view.else_block) |branch| other = branch } if (looping) execution.loop_depth += 1 defer if (looping) execution.loop_depth -= 1 saved :: execution.locals.items.len defer execution.locals.items = execution.locals.items[..saved] invariant :: branch_constant(state, condition, 0) value := Value{} while (true) { if (!try type_step(state, condition)) return .invalid if (!invariant or value.kind == .invalid) { expression :: try type_snapshot(state, execution, condition) if (expression == no_node) return .invalid if (capture) |name| value = try type_capture_guard(state, expression, name) else { boolean :: try builtin(state, expression, .bool) value = try evaluate_ir(state, expression, boolean) } } if (value.kind != .boolean) return .invalid if (!value.boolean) { if (!looping and other != no_node) return try execute_type_block(state, execution, other) return .next } execution.locals.items = execution.locals.items[..saved] if (capture) |name| { expression :: try type_snapshot(state, execution, condition) if (expression == no_node) return .invalid unwrapped :: try ast.add_unary(state.builder, ast.node_span(&state.tree, condition), .unwrap, expression) try refresh(state) annotation :: try expression_type(state, unwrapped, 0) payload :: try type_execution_value(state, unwrapped, annotation) if (payload == no_node) return .invalid try arraylist.append(&execution.locals, TypeLocal{ declaration = name, value = payload, annotation = annotation, }) } flow :: try execute_type_block(state, execution, body) if (!looping or flow == .returned or flow == .yielded or flow == .invalid) return flow if (flow == .break) return .next } } .expression: { expression :: ast.statement_expr(&state.tree, statement) if (ast.expr_kind(&state.tree, expression) != .match_expr) { snapshot :: try type_snapshot(state, execution, expression) if (snapshot == no_node) return .invalid annotation :: try expression_type(state, snapshot, 0) if (try type_execution_value(state, snapshot, annotation) == no_node) return .invalid return .next } subject_type := no_node arms :: ast.match_arm_count(&state.tree, expression) if (arms > 0 and ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, arms - 1)).is_inline) { subject :: try type_snapshot(state, execution, ast.match_expr(&state.tree, expression).subject) if (subject == no_node) return .invalid subject_type = try expression_type(state, subject, 0) } before :: ast.node_count(&state.tree) try expand_inline_match(state, expression, subject_type) if (ast.expr_kind(&state.tree, expression) != .match_expr) return .invalid if (ast.node_count(&state.tree) != before) { previous :: execution.mapping.len execution.mapping = try mem.realloc(state.allocator, execution.mapping, ast.node_count(&state.tree)) mem.set(execution.mapping[previous..], no_node) } snapshot :: try type_dispatch_snapshot(state, execution, expression) if (snapshot == no_node) return .invalid index :: try type_match_arm(state, snapshot) if (index == no_index) return .invalid arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, expression, index)) saved :: execution.locals.items.len defer execution.locals.items = execution.locals.items[..saved] if (arm.capture) |capture| { value :: try type_match_capture(state, snapshot) if (value == no_node) return .invalid constant :: ast.typed_constant(&state.tree, value) captured :: if (is_type(state, constant.type)) constant.value else value try arraylist.append(&execution.locals, TypeLocal{ declaration = capture, value = captured, annotation = constant.type, }) } if (!arm.expression) { if (try execute_type_value_block(state, execution, arm.body, no_node) == no_node) return .invalid return .next } body :: try type_snapshot(state, execution, arm.body) value :: try type_value_expression(state, body) if (value == no_node) return .invalid annotation :: try expression_type(state, value, 0) if (try type_execution_value(state, value, annotation) == no_node) return .invalid } .for_stmt: return try execute_type_for(state, execution, statement) .block_stmt: return try execute_type_block(state, execution, ast.statement_block(&state.tree, statement)) .break_stmt, .continue_stmt: { if (execution.loop_depth == 0) { code :: if (ast.stmt_kind(&state.tree, statement) == .break_stmt) diagnostics.Code.break_outside_loop else diagnostics.Code.continue_outside_loop state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, ast.node_span(&state.tree, statement)) return .invalid } return if (ast.stmt_kind(&state.tree, statement) == .break_stmt) TypeFlow.break else TypeFlow.continue } else: return .invalid } return .next } # retain a collection place, but freeze its indices at loop entry. snapshots then read # the current owned root after body assignments without re-evaluating the place. @hide:file type_collection_place proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error { resolved :: binding(state, id) if (resolved.kind == .declaration) { for (execution.locals.items) |local| { if (local.declaration == resolved.data.declaration) return id } return no_node } kind :: ast.expr_kind(&state.tree, id) if (kind != .selection and kind != .index) return no_node base :: if (kind == .selection) ast.selection_expr(&state.tree, id).base else ast.index_expr(&state.tree, id).base place :: try type_collection_place(state, execution, base) if (place == no_node) return no_node result := no_node if (kind == .selection) { result = try ast.add_selection(state.builder, ast.node_span(&state.tree, id), place, ast.selection_expr(&state.tree, id).member) } else { index :: try type_snapshot(state, execution, ast.index_expr(&state.tree, id).index) if (index == no_node) return no_node value :: try type_execution_value(state, index, try builtin(state, index, .usize)) if (value == no_node) return no_node result = try ast.add_index(state.builder, ast.node_span(&state.tree, id), place, value) } try refresh(state) return result } @hide:file type_collection_read proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error { if (binding(state, id).kind == .declaration) return try type_snapshot(state, execution, id) kind :: ast.expr_kind(&state.tree, id) base :: if (kind == .selection) ast.selection_expr(&state.tree, id).base else ast.index_expr(&state.tree, id).base value :: try type_collection_read(state, execution, base) result := no_node if (kind == .selection) result = try ast.add_selection(state.builder, ast.node_span(&state.tree, id), value, ast.selection_expr(&state.tree, id).member) else result = try ast.add_index(state.builder, ast.node_span(&state.tree, id), value, ast.index_expr(&state.tree, id).index) try refresh(state) return result } @hide:file execute_type_collection proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error { loop :: ast.for_stmt(&state.tree, statement) span :: ast.node_span(&state.tree, statement) if (loop.capture) |capture| { if (binding(state, capture).kind == .invalid) { state.evaluation_diagnostic = binding(state, capture).data.diagnostic return .invalid } } if (loop.index) |index| { if (binding(state, index).kind == .invalid) { state.evaluation_diagnostic = binding(state, index).data.diagnostic return .invalid } } snapshot :: try type_snapshot(state, execution, loop.iterable) if (!try expand_type_matches(state, snapshot)) return .invalid try expand(state, snapshot) annotation :: try expression_type(state, snapshot, 0) if (annotation == no_node) return .invalid tuple :: loop.is_inline and ast.expr_kind(&state.tree, annotation) == .record_type if tuple { for (0..ast.record_field_count(&state.tree, annotation)) |i| { if (ast.field(&state.tree, ast.record_field(&state.tree, annotation, i)).name != null) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_for_iterable, span) return .invalid } } } collection :: try type_execution_value(state, snapshot, annotation) if (collection == no_node) return .invalid count := Value{} if tuple { count = Value{ kind = .integer, magnitude = u64(ast.record_field_count(&state.tree, annotation)) } } else if (try execution_array_storage(state, annotation) or try metadata_sequence(state, annotation)) { count = Value{ kind = .integer, magnitude = u64(ast.array_element_count(&state.tree, ast.typed_constant(&state.tree, collection).value)) } } else { # validate the loop contract even for empty collections, without executing # its type-valued body. the synthetic body exits before reading an element. stop :: try ast.add_break_stmt(state.builder, span) stops [1]ast.NodeId :: [stop] body :: try ast.add_block(state.builder, span, stops[..]) selector :: try ast.add_for_stmt(state.builder, span, collection, loop.capture, loop.index, loop.pointer_capture, loop.index_capture, body) try refresh(state) length := no_node if (ast.expr_kind(&state.tree, annotation) == .array_type) length = ast.array_type(&state.tree, annotation).count? else { value :: try evaluate(state, collection) if (value.kind == .string) { text :: ast.literal_text(&state.tree, value.node) cursor := usize(1) size := u64(0) while (cursor < text.len - 1) { _ = ast.string_next(text, &cursor) catch { return .invalid } size += 1 } length = try ast.add_integer(state.builder, span, size) } else length = try ast.add_integer(state.builder, span, 0) } returned :: try ast.add_return_stmt(state.builder, span, length) statements [2]ast.NodeId :: [selector, returned] wrapper :: try ast.add_block(state.builder, span, statements[..]) try refresh(state) count = try evaluate_ir_body(state, statement, wrapper, try builtin(state, statement, .usize)) if (count.kind != .integer) return .invalid } if (loop.pointer_capture and loop.capture != null) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span) return .invalid } place := no_node if (tuple or ast.expr_kind(&state.tree, annotation) == .array_type) place = try type_collection_place(state, execution, loop.iterable) if (state.evaluation_diagnostic != diagnostics.no_diagnostic) return .invalid saved :: execution.locals.items.len defer execution.locals.items = execution.locals.items[..saved] execution.loop_depth += 1 defer execution.loop_depth -= 1 for (0..usize(count.magnitude)) |i| { if (!try type_step(state, statement)) return .invalid execution.locals.items = execution.locals.items[..saved] index :: try ast.add_integer(state.builder, span, u64(i)) try refresh(state) if loop.capture |capture| { current := collection if (place != no_node) current = try type_collection_read(state, execution, place) element :: if (tuple) try ast.add_selection(state.builder, span, current, index) else try ast.add_index(state.builder, span, current, index) try refresh(state) element_type :: if (tuple) ast.field(&state.tree, ast.record_field(&state.tree, annotation, i)).type? else try expression_type(state, element, 0) value :: try type_execution_value(state, element, element_type) if (value == no_node) return .invalid try arraylist.append(&execution.locals, TypeLocal{ declaration = capture, value = value, annotation = element_type, }) } if (loop.index) |capture| { type :: try builtin(state, capture, .usize) value :: try type_execution_value(state, index, type) if (value == no_node) return .invalid try arraylist.append(&execution.locals, TypeLocal{ declaration = capture, value = value, annotation = type, }) } flow :: try execute_type_block(state, execution, loop.body) if (flow == .returned or flow == .yielded or flow == .invalid) return flow if (flow == .break) return .next } return .next } @hide:file execute_type_for proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error { loop :: ast.for_stmt(&state.tree, statement) if (ast.expr_kind(&state.tree, loop.iterable) != .range) return try execute_type_collection(state, execution, statement) if (loop.pointer_capture or loop.index_capture) return .invalid bounds :: ast.range_expr(&state.tree, loop.iterable) start :: try type_snapshot(state, execution, bounds.start) end :: try type_snapshot(state, execution, bounds.end) if (start == no_node or end == no_node) return .invalid typed_bound :: if (flexible(state, start)) end else start annotation :: try expression_type(state, typed_bound, 0) if (annotation == no_node or !try integer_type(state, annotation)) return .invalid current := try type_execution_value(state, start, annotation) limit :: try type_execution_value(state, end, annotation) if (current == no_node or limit == no_node) return .invalid saved :: execution.locals.items.len defer execution.locals.items = execution.locals.items[..saved] if (loop.capture) |capture| try arraylist.append(&execution.locals, TypeLocal{ declaration = capture, value = current, annotation = annotation, }) execution.loop_depth += 1 defer execution.loop_depth -= 1 while (true) { if (!try type_step(state, statement)) return .invalid span :: ast.node_span(&state.tree, statement) operator ast.BinaryOperator :: if (bounds.inclusive) .less_equal else .less comparison :: try ast.add_binary(state.builder, span, operator, current, limit) try refresh(state) keep :: try evaluate(state, comparison) if (keep.kind != .boolean) return .invalid if (!keep.boolean) return .next if (loop.capture != null) execution.locals.items[saved].value = current flow :: try execute_type_block(state, execution, loop.body) if (flow == .returned or flow == .yielded or flow == .invalid) return flow if (flow == .break) return .next if (try value_equal(state, try evaluate(state, current), try evaluate(state, limit))) return .next one :: try ast.add_integer(state.builder, span, 1) increment :: try ast.add_binary(state.builder, span, .add, current, one) try refresh(state) current = try type_execution_value(state, increment, annotation) if (current == no_node) return .invalid } } @hide:file mix proc(left, right usize) usize { return (left << 7) xor (left >> (sizeof!(usize) * 8 - 7)) xor right } @hide:file evaluate proc(state @mut State, id ast.NodeId) Value ! Error { if (id == no_node) return Value{} if (state.depth >= 128) return Value{} state.depth += 1 defer state.depth -= 1 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 class :: ast.node_class(&state.tree, target) if (class == .function) { if (template(state, target)) return Value{} return Value{ kind = .function, node = id } } if (class == .statement) { declaration :: ast.statement_decl(&state.tree, target) if (declaration.mutability != .constant) return Value{} value := try evaluate(state, declaration.initializer) if (declaration.type) |annotation| value.annotation = annotation return value } return Value{} } match kind { .union_constant, .slice_constant: { value :: if (kind == .union_constant) ast.union_constant(&state.tree, id) else ast.slice_constant(&state.tree, id) return Value{ kind = .aggregate, node = id, annotation = value.type } } .typed_constant: { view :: ast.typed_constant(&state.tree, id) if (!is_type(state, view.type) and type_record_storage(state, view.type, 0)) { if (!try type_array_initialized(state, id, id)) return Value{} return Value{ kind = .aggregate, node = view.value, annotation = view.type } } value := try evaluate_expected(state, view.value, view.type) value.annotation = view.type return value } .literal_int: { value := u64(0) for ast.literal_text(&state.tree, id) |byte| { if (byte < '0' or byte > '9') return Value{} digit :: u64(byte - '0') if (value > divtrunc!(maxval!(u64) - digit, u64(10))) return Value{} value = value * 10 + digit } return Value{ kind = .integer, node = id, magnitude = value } } .literal_float: { if (ast.float_value(&state.tree, id)) |value| return Value{ kind = .real, node = id, magnitude = ptrcast!(@u64, &value)^, } return try evaluate_ir(state, id, try builtin(state, id, .float)) } .literal_bool: return Value{ kind = .boolean, node = id, boolean = mem.eql(ast.literal_text(&state.tree, id), "true") } .literal_string: { text :: ast.literal_text(&state.tree, id) cursor := usize(1) while (cursor < text.len - 1) { _ = ast.string_next(text, &cursor) catch { try reject(state, id, .invalid_escape) return Value{} } } return Value{ kind = .string, node = id } } .literal_null: return Value{ kind = .null, node = id } .literal_void: return Value{ kind = .aggregate, node = id, annotation = try builtin(state, id, .void) } .record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type, .enum_type, .union_type, .union_tag_type, .distinct_type, .fallible_type, .sum_type: return Value{ kind = .type, node = id } .alias_expr: return try evaluate(state, ast.type_operand(&state.tree, id)) .unary: { view :: ast.unary_expr(&state.tree, id) if (view.operator == .bit_not) return try evaluate_ir(state, id, try expression_type(state, id, 0)) if (view.operator == .unwrap) { # source-known callbacks already own declaration identities, not interpreter handles. value := try evaluate(state, view.operand) annotation :: try expression_type(state, view.operand, 0) if (value.kind == .function and annotation != no_node and ast.expr_kind(&state.tree, annotation) == .optional_type) { try arraylist.append(&state.rewrites.constraints, id) value.annotation = ast.type_operand(&state.tree, annotation) return value } return try evaluate_ir(state, id, try expression_type(state, id, 0)) } value := try evaluate(state, view.operand) if (view.operator == .negate and value.kind == .integer) value.negative = value.magnitude != 0 and !value.negative else if (view.operator == .negate and value.kind == .real) value.magnitude = value.magnitude xor (u64(1) << 63) else if (view.operator == .logical_not and value.kind == .boolean) value.boolean = !value.boolean else return Value{} value.node = id return value } .binary: return try evaluate_binary(state, id) .call: return try evaluate_call(state, id) .block_expr: { annotation :: try expression_type(state, id, 0) if (annotation == no_node) return Value{} if (is_type(state, annotation) or type_record_storage(state, annotation, 0)) { result :: try evaluate_type_value_block(state, id, annotation) if (result == no_node) return Value{} return try evaluate_expected(state, result, annotation) } return try evaluate_ir(state, id, annotation) } .match_expr: { annotation :: try expression_type(state, id, 0) if (annotation != no_node and (is_type(state, annotation) or type_record_storage(state, annotation, 0))) { if (state.type_execution_depth == 0) state.type_execution_remaining = 4096 state.type_execution_depth += 1 defer state.type_execution_depth -= 1 selected :: try type_value_expression(state, id) if (selected == no_node or selected == id) return Value{} return try evaluate_expected(state, selected, annotation) } return try evaluate_ir(state, id, annotation) } .record_literal, .array_literal, .selection, .index, .builtin_call, .catch_expr: return try evaluate_ir(state, id, try expression_type(state, id, 0)) else: return Value{} } } @hide:file evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error { view :: ast.binary_expr(&state.tree, id) if (view.operator == .divide) return try evaluate_ir(state, id, try builtin(state, id, .float)) if (view.operator == .bit_and or view.operator == .bit_or or view.operator == .bit_xor or view.operator == .shift_left or view.operator == .shift_right or view.operator == .shift_left_saturating) return try evaluate_ir(state, id, try expression_type(state, id, 0)) left :: try evaluate(state, view.left) if (left.kind == .invalid) return Value{} if (left.kind == .boolean) { 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 } } right := try evaluate(state, view.right) if (left.kind == .invalid or right.kind == .invalid) return Value{} if (left.kind == .real or right.kind == .real) { boolean :: ( view.operator == .equal or view.operator == .not_equal or view.operator == .less or view.operator == .less_equal or view.operator == .greater or view.operator == .greater_equal ) kind sema.BuiltinType :: if (boolean) .bool else .float result_type :: try builtin(state, id, kind) return try evaluate_ir(state, id, result_type) } if ( left.annotation != no_node and right.annotation != no_node and left.kind == .integer and right.kind == .integer and !try same_type(state, left.annotation, right.annotation) ) return Value{} if (view.operator == .equal or view.operator == .not_equal) { if (left.kind != right.kind) return Value{} equal :: try value_equal(state, left, right) if (left.kind == .type) { operands [2]ast.NodeId :: [view.left, view.right] for (operands) |operand| { if (type_record_storage(state, operand, 0)) continue constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, operand), operand, true) try refresh(state) try arraylist.append(&state.rewrites.constraints, constraint) } folded :: try ast.add_boolean(state.builder, ast.node_span(&state.tree, id), equal == (view.operator == .equal)) try replace(state, id, folded) try refresh(state) } return Value{ kind = .boolean, node = id, boolean = equal == (view.operator == .equal) } } if (left.kind == .boolean and right.kind == .boolean) { if (view.operator == .logical_and) return Value{ kind = .boolean, node = id, boolean = left.boolean and right.boolean } if (view.operator == .logical_or) return Value{ kind = .boolean, node = id, boolean = left.boolean or right.boolean } return Value{} } if (left.kind != .integer or right.kind != .integer) return Value{} annotation :: if (left.annotation != no_node) left.annotation else right.annotation less :: if (left.negative != right.negative) left.negative else if (left.negative) left.magnitude > right.magnitude else left.magnitude < right.magnitude equal :: left.magnitude == right.magnitude and left.negative == right.negative match view.operator { .less: return Value{ kind = .boolean, node = id, boolean = less } .less_equal: return Value{ kind = .boolean, node = id, boolean = less or equal } .greater: return Value{ kind = .boolean, node = id, boolean = !less and !equal } .greater_equal: return Value{ kind = .boolean, node = id, boolean = !less } .multiply: { if (right.magnitude != 0 and left.magnitude > divtrunc!(maxval!(u64), right.magnitude)) return Value{} magnitude :: left.magnitude * right.magnitude return try integer_value(state, id, magnitude, magnitude != 0 and left.negative != right.negative, annotation) } .add, .subtract: { if (view.operator == .subtract and right.magnitude != 0) right.negative = !right.negative if (left.negative == right.negative) { if (left.magnitude > maxval!(u64) - right.magnitude) return Value{} return try integer_value(state, id, left.magnitude + right.magnitude, left.negative, annotation) } if (left.magnitude < right.magnitude) return try integer_value(state, id, right.magnitude - left.magnitude, right.negative, annotation) return try integer_value(state, id, left.magnitude - right.magnitude, left.magnitude != right.magnitude and left.negative, annotation) } else: return Value{} } } @hide:file canonical_type proc(state @mut State, id ast.NodeId) ast.NodeId ! Error { value :: try evaluate(state, id) return if (value.kind == .type) value.node else no_node } # metadata storage compares anonymous annotation shapes, including ordinary nested # fields. named definitions and nominal type results retain their original identity. @hide:file # ponytail: scan declaration syntax for nominal identity; index record origins if metadata-heavy builds make this hot. structural_type_record proc(state @State, id ast.NodeId) bool { for (state.instances.items) |instance| { if (instance.type == id) return false } for (0..ast.node_count(&state.tree)) |i| { declaration :: ast.NodeId(i) if (ast.node_class(&state.tree, declaration) != .statement or ast.stmt_kind(&state.tree, declaration) != .declaration) continue if (ast.statement_decl(&state.tree, declaration).initializer == id) return false } return true } @hide:file same_type proc(state @mut State, left, right ast.NodeId) bool ! Error { return try same_type_context(state, left, right, false) } @hide:file same_type_context proc(state @mut State, left, right ast.NodeId, metadata bool) bool ! Error { if (is_type(state, left) or is_type(state, right)) return is_type(state, left) and is_type(state, right) a :: try canonical_type(state, left) b :: try canonical_type(state, right) if (a == no_node or b == no_node) return false if (a == b) return true x :: binding(state, a) y :: binding(state, b) if (x.kind == .builtin or y.kind == .builtin) return x.kind == .builtin and y.kind == .builtin and builtin_identity(x.data.builtin) == builtin_identity(y.data.builtin) kind :: ast.expr_kind(&state.tree, a) if (kind == .sum_type or ast.expr_kind(&state.tree, b) == .sum_type) return try type_subset(state, a, b) and try type_subset(state, b, a) if (kind != ast.expr_kind(&state.tree, b)) return false match kind { .union_tag_type: return try same_type_context(state, ast.type_operand(&state.tree, a), ast.type_operand(&state.tree, b), metadata) .record_type: { if (!metadata and (!type_record_storage(state, a, 0) or !type_record_storage(state, b, 0))) return false if (!structural_type_record(state, a) or !structural_type_record(state, b)) return false count :: ast.record_field_count(&state.tree, a) if (count != ast.record_field_count(&state.tree, b)) return false for (0..count) |i| { p :: ast.field(&state.tree, ast.record_field(&state.tree, a, i)) q :: ast.field(&state.tree, ast.record_field(&state.tree, b, i)) if ((p.name == null) != (q.name == null)) return false if (p.name != null and ast.name_id(&state.tree, p.name?) != ast.name_id(&state.tree, q.name?)) return false if (p.type == null or q.type == null or !try same_type_context(state, p.type?, q.type?, true)) return false } return true } .pointer_type: { p :: ast.pointer_type(&state.tree, a) q :: ast.pointer_type(&state.tree, b) return p.mutable == q.mutable and p.many == q.many and try same_type_context(state, p.element, q.element, metadata) } .array_type: { p :: ast.array_type(&state.tree, a) q :: ast.array_type(&state.tree, b) if (p.mutable != q.mutable or p.count == null or q.count == null) return false pv :: try evaluate(state, p.count?) qv :: try evaluate(state, q.count?) return try value_equal(state, pv, qv) and try same_type_context(state, p.element, q.element, metadata) } .slice_type: { p :: ast.slice_type(&state.tree, a) q :: ast.slice_type(&state.tree, b) if (p.mutable != q.mutable or (p.sentinel == null) != (q.sentinel == null)) return false if (p.sentinel) |sentinel| { if (!try value_equal(state, try evaluate(state, sentinel), try evaluate(state, q.sentinel?))) return false } return try same_type_context(state, p.element, q.element, metadata) } .optional_type: return try same_type_context(state, ast.type_operand(&state.tree, a), ast.type_operand(&state.tree, b), metadata) .fallible_type: { p :: ast.binary_expr(&state.tree, a) q :: ast.binary_expr(&state.tree, b) return try same_type_context(state, p.left, q.left, metadata) and try same_type_context(state, p.right, q.right, metadata) } .function_type: { count :: ast.function_param_count(&state.tree, a) if (count != ast.function_param_count(&state.tree, b)) return false if (!try same_type_context(state, ast.callable_result(&state.tree, a), ast.callable_result(&state.tree, b), metadata)) return false for (0..count) |i| { p :: ast.param(&state.tree, ast.function_param(&state.tree, a, i)).type q :: ast.param(&state.tree, ast.function_param(&state.tree, b, i)).type if (!try same_type_context(state, p, q, metadata)) return false } return true } else: return false } } @hide:file type_subset proc(state @mut State, left, right ast.NodeId) bool ! Error { a :: try canonical_type(state, left) b :: try canonical_type(state, right) if (a == no_node or b == no_node) return false if (ast.expr_kind(&state.tree, a) == .sum_type) { view :: ast.binary_expr(&state.tree, a) return try type_subset(state, view.left, b) and try type_subset(state, view.right, b) } if (ast.expr_kind(&state.tree, b) == .sum_type) { view :: ast.binary_expr(&state.tree, b) return try type_subset(state, a, view.left) or try type_subset(state, a, view.right) } return try same_type(state, a, b) } @hide:file value_equal proc(state @mut State, left, right Value) bool ! Error { if (left.kind != right.kind) return false return match left.kind { .invalid, .null: true .type: try same_type(state, left.node, right.node) .integer: left.magnitude == right.magnitude and left.negative == right.negative .boolean: left.boolean == right.boolean .function: binding(state, left.node).data.declaration == binding(state, right.node).data.declaration .real: left.magnitude == right.magnitude .aggregate: ( try same_type(state, left.annotation, right.annotation) and try constant_equal(state, left.node, right.node) ) .string: try same_string(state, left.node, right.node) } } @hide:file value_hash proc(state @mut State, value Value) usize ! Error { hash :: enum_index(value.kind) negative :: if (value.negative) usize(1) else usize(0) boolean :: if (value.boolean) usize(1) else usize(0) return match value.kind { .type: mix(hash, try type_hash(state, value.node)) .integer: mix(mix(hash, usize(value.magnitude)), negative) .boolean: mix(hash, boolean) .function: mix(hash, usize(binding(state, value.node).data.declaration)) .string: string_hash(hash, ast.literal_text(&state.tree, value.node)) .real: mix(hash, usize(value.magnitude)) .aggregate: mix(mix(hash, try type_hash(state, value.annotation)), try constant_hash(state, value.node)) .invalid, .null: hash } } @hide:file string_hash proc(seed usize, text []u8) usize { result := seed cursor := usize(1) while (cursor < text.len - 1) { byte :: ast.string_next(text, &cursor) catch { return 0 } result = mix(result, usize(byte)) } return result } @hide:file type_hash proc(state @mut State, id ast.NodeId) usize ! Error { return try type_hash_context(state, id, false) } @hide:file type_hash_context proc(state @mut State, id ast.NodeId, metadata bool) usize ! Error { node :: try canonical_type(state, id) if (node == no_node) return 0 resolved :: binding(state, node) if (resolved.kind == .builtin) return mix(1, enum_index(builtin_identity(resolved.data.builtin))) kind :: ast.expr_kind(&state.tree, node) hash := enum_index(kind) match kind { .union_tag_type: return mix(hash, try type_hash_context(state, ast.type_operand(&state.tree, node), metadata)) .record_type: { if ((!metadata and !type_record_storage(state, node, 0)) or !structural_type_record(state, node)) return mix(hash, usize(node)) for (0..ast.record_field_count(&state.tree, node)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, node, i)) if (field.name) |name| hash = mix(hash, usize(ast.name_id(&state.tree, name))) if (field.type) |annotation| hash = mix(hash, try type_hash_context(state, annotation, true)) } return hash } .pointer_type: { view :: ast.pointer_type(&state.tree, node) mutable :: if (view.mutable) usize(1) else usize(0) many :: if (view.many) usize(1) else usize(0) hash = mix(hash, mutable) hash = mix(hash, many) return mix(hash, try type_hash_context(state, view.element, metadata)) } .array_type: { view :: ast.array_type(&state.tree, node) mutable :: if (view.mutable) usize(1) else usize(0) hash = mix(hash, mutable) if (view.count) |count| hash = mix(hash, try value_hash(state, try evaluate(state, count))) return mix(hash, try type_hash_context(state, view.element, metadata)) } .slice_type: { view :: ast.slice_type(&state.tree, node) mutable :: if (view.mutable) usize(1) else usize(0) hash = mix(hash, mutable) if (view.sentinel) |sentinel| hash = mix(hash, try value_hash(state, try evaluate(state, sentinel))) return mix(hash, try type_hash_context(state, view.element, metadata)) } .optional_type: return mix(hash, try type_hash_context(state, ast.type_operand(&state.tree, node), metadata)) .sum_type: { view :: ast.binary_expr(&state.tree, node) # set fingerprints must ignore ordering, grouping and repeated members. # colliding fingerprints still use full type equality in the cache chain. return try type_hash_context(state, view.left, metadata) | try type_hash_context(state, view.right, metadata) } .fallible_type: { view :: ast.binary_expr(&state.tree, node) return mix(mix(hash, try type_hash_context(state, view.left, metadata)), try type_hash_context(state, view.right, metadata)) } .function_type: { hash = mix(hash, try type_hash_context(state, ast.callable_result(&state.tree, node), metadata)) for (0..ast.function_param_count(&state.tree, node)) |i| { parameter :: ast.function_param(&state.tree, node, i) hash = mix(hash, try type_hash_context(state, ast.param(&state.tree, parameter).type, metadata)) } return hash } else: return mix(hash, usize(node)) } } @hide:file value_block_yield proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! Error { if (depth >= 128) return no_node class :: ast.node_class(&state.tree, id) if (class != .block and class != .statement) return no_node if (class == .statement) { kind :: ast.stmt_kind(&state.tree, id) if (kind == .yield_stmt) return ast.yield_stmt(&state.tree, id).value? if (kind != .block_stmt and kind != .if_stmt and kind != .while_stmt and kind != .for_stmt) return no_node } result := no_node for (0..ast.child_count(&state.tree, id)) |i| { child :: ast.child(&state.tree, id, i) value :: try value_block_yield(state, child, depth + 1) if (value == no_node or try expression_type(state, value, depth + 1) == no_node) continue result = value if (!flexible(state, value)) return result } return result } @hide:file value_block_type proc(state @mut State, block ast.NodeId, depth usize) ast.NodeId ! Error { value :: try value_block_yield(state, block, depth) return if (value != no_node) try expression_type(state, value, depth + 1) else try builtin(state, block, .void) } @hide:file expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! Error { if (id == no_node) return no_node if (depth >= 128) return no_node try expand(state, id) resolved :: binding(state, id) if (resolved.kind == .builtin and resolved.data.builtin != .type) return try builtin(state, id, .type) if (resolved.kind == .declaration) { target :: resolved.data.declaration if (hashmap.get(&state.capture_types, target)) |annotation| return try canonical_type(state, annotation) class :: ast.node_class(&state.tree, target) if (class == .parameter) { annotation :: ast.param(&state.tree, target).type return try canonical_type(state, annotation) } if (class == .statement) { declaration :: ast.statement_decl(&state.tree, target) if (declaration.type) |annotation| return try canonical_type(state, annotation) return try expression_type(state, declaration.initializer, depth + 1) } if (class == .function) { params := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(¶ms) for (0..ast.function_param_count(&state.tree, target)) |i| try arraylist.append(¶ms, ast.function_param(&state.tree, target, i)) result :: try ast.add_function_type(state.builder, ast.node_span(&state.tree, id), ast.function_decl(&state.tree, target).return_type, params.items) try refresh(state) return result } return no_node } kind :: ast.expr_kind(&state.tree, id) match kind { .record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type, .enum_type, .union_type, .union_tag_type, .distinct_type, .fallible_type, .sum_type, .alias_expr: return try builtin(state, id, .type) .typed_constant, .union_constant, .slice_constant: { annotation :: if (kind == .union_constant) ast.union_constant(&state.tree, id).type else if (kind == .slice_constant) ast.slice_constant(&state.tree, id).type else ast.typed_constant(&state.tree, id).type return if (is_type(state, annotation)) annotation else try canonical_type(state, annotation) } .literal_int: return try builtin(state, id, .i64) .literal_float: return try builtin(state, id, .f64) .literal_bool: return try builtin(state, id, .bool) .literal_string: return try builtin(state, id, .string) .literal_void: return try builtin(state, id, .void) .block_expr: return try value_block_type(state, ast.block_expr(&state.tree, id), depth + 1) .match_expr: { result := no_node for (0..ast.match_arm_count(&state.tree, id)) |i| { arm :: ast.match_arm_view(&state.tree, ast.match_arm(&state.tree, id, i)) annotation :: if (arm.expression) try expression_type(state, arm.body, depth + 1) else try value_block_type(state, arm.body, depth + 1) if (annotation == no_node) continue result = annotation if (!arm.expression or !flexible(state, arm.body)) return result } return result } .builtin_call: { name :: ast.literal_text(&state.tree, ast.builtin_call(&state.tree, id).name) count :: ast.call_arg_count(&state.tree, id) if ((mem.eql(name, "sizeof") or mem.eql(name, "alignof")) and count == 1) return try builtin(state, id, .usize) if ((mem.eql(name, "maxval") or mem.eql(name, "minval")) and count == 1) return try canonical_type(state, ast.call_arg(&state.tree, id, 0)) if ((mem.eql(name, "ptrcast") or mem.eql(name, "constcast")) and count == 2) return try canonical_type(state, ast.call_arg(&state.tree, id, 0)) if ((mem.eql(name, "divtrunc") or mem.eql(name, "rem")) and count == 2) { left :: ast.call_arg(&state.tree, id, 0) right :: ast.call_arg(&state.tree, id, 1) operand :: if (flexible(state, left)) right else left return try expression_type(state, operand, depth + 1) } return no_node } .catch_expr, .try_expr: { operand :: if (kind == .catch_expr) ast.catch_expr(&state.tree, id).operand else ast.type_operand(&state.tree, id) type :: try expression_type(state, operand, depth + 1) if (type == no_node or ast.expr_kind(&state.tree, type) != .fallible_type) return no_node return try canonical_type(state, ast.binary_expr(&state.tree, type).left) } .record_literal: { view :: ast.record_literal(&state.tree, id) if (view.type) |annotation| return try canonical_type(state, annotation) fields := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(&fields) 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) return no_node type :: try expression_type(state, field.value?, depth + 1) if (type == no_node) return no_node generated :: try ast.add_field(state.builder, field.span, field.name, type, null) try refresh(state) try arraylist.append(&fields, generated) } result :: try ast.add_record_type(state.builder, ast.node_span(&state.tree, id), fields.items) try refresh(state) return result } .array_literal: { count :: ast.array_element_count(&state.tree, id) if (count == 0) return no_node element := try expression_type(state, ast.array_element(&state.tree, id, 0), depth + 1) if (element == no_node) return no_node # inferred values have concrete default widths even when a cast or # procedure result used the int/float annotation shorthand. scalar :: binding(state, element) if (scalar.kind == .builtin) { if (scalar.data.builtin == .int) element = try builtin(state, element, .i64) if (scalar.data.builtin == .float) element = try builtin(state, element, .f64) } length :: try ast.add_integer(state.builder, ast.node_span(&state.tree, id), u64(count)) try refresh(state) result :: try ast.add_array_type(state.builder, ast.node_span(&state.tree, id), length, element, false) try refresh(state) return result } .call: { callee :: ast.call_expr(&state.tree, id).callee destination :: binding(state, callee) if (destination.kind == .builtin) return callee if (destination.kind == .declaration and ast.node_class(&state.tree, destination.data.declaration) == .function) return try canonical_type(state, ast.function_decl(&state.tree, destination.data.declaration).return_type) signature :: try expression_type(state, callee, depth + 1) if (signature != no_node and is_type(state, signature)) return try canonical_type(state, callee) if (signature != no_node and ast.expr_kind(&state.tree, signature) == .function_type) return try canonical_type(state, ast.callable_result(&state.tree, signature)) return no_node } .unary: { view :: ast.unary_expr(&state.tree, id) operand :: try expression_type(state, view.operand, depth + 1) if (operand == no_node) return no_node if (view.operator == .address_of) { mutable := false target :: binding(state, view.operand) if (target.kind == .declaration and ast.node_class(&state.tree, target.data.declaration) == .statement) mutable = ast.statement_decl(&state.tree, target.data.declaration).mutability == .variable result :: try ast.add_pointer_type(state.builder, view.span, operand, mutable, false) try refresh(state) return result } if (view.operator == .dereference and ast.expr_kind(&state.tree, operand) == .pointer_type) return try canonical_type(state, ast.pointer_type(&state.tree, operand).element) if (view.operator == .unwrap and ast.expr_kind(&state.tree, operand) == .optional_type) return try canonical_type(state, ast.type_operand(&state.tree, operand)) return operand } .binary: { view :: ast.binary_expr(&state.tree, id) if ( view.operator == .equal or view.operator == .not_equal or view.operator == .less or view.operator == .less_equal or view.operator == .greater or view.operator == .greater_equal or view.operator == .logical_and or view.operator == .logical_or ) return try builtin(state, id, .bool) if ((view.operator == .bit_and or view.operator == .bit_or or view.operator == .bit_xor) and flexible(state, view.left)) return try expression_type(state, view.right, depth + 1) return try expression_type(state, view.left, depth + 1) } .selection: { view :: ast.selection_expr(&state.tree, id) base := try expression_type(state, view.base, depth + 1) if (base == no_node) return no_node if (is_type(state, base)) { selected :: try canonical_type(state, view.base) if (selected != no_node and ast.expr_kind(&state.tree, selected) == .enum_type) return selected } if (ast.expr_kind(&state.tree, base) == .pointer_type) base = try canonical_type(state, ast.pointer_type(&state.tree, base).element) if (base == no_node) return no_node member :: ast.literal_text(&state.tree, view.member) base_kind :: ast.expr_kind(&state.tree, base) if ( mem.eql(member, "len") and ( base_kind == .array_type or base_kind == .slice_type or (binding(state, base).kind == .builtin and binding(state, base).data.builtin == .string) ) ) return try builtin(state, id, .usize) if (mem.eql(member, "ptr")) { element := no_node mutable := false if (base_kind == .array_type) { element = ast.array_type(&state.tree, base).element mutable = ast.array_type(&state.tree, base).mutable } if (base_kind == .slice_type) { element = ast.slice_type(&state.tree, base).element mutable = ast.slice_type(&state.tree, base).mutable } if (binding(state, base).kind == .builtin and binding(state, base).data.builtin == .string) element = try builtin(state, id, .u8) if (element != no_node) { result :: try ast.add_pointer_type(state.builder, view.span, element, mutable, true) try refresh(state) return result } } if (base_kind != .record_type and base_kind != .union_type) return no_node slot :: type_record_slot(&state.tree, base, view.member) if (slot == no_index) return no_node return try canonical_type(state, ast.field(&state.tree, ast.record_field(&state.tree, base, slot)).type?) } .index: { view :: ast.index_expr(&state.tree, id) base :: try expression_type(state, view.base, depth + 1) if (base == no_node) return no_node base_kind :: ast.expr_kind(&state.tree, base) if (base_kind == .array_type) { element :: ast.array_type(&state.tree, base).element return if (is_type(state, element)) element else try canonical_type(state, element) } if (base_kind == .slice_type) return try canonical_type(state, ast.slice_type(&state.tree, base).element) if (base_kind == .pointer_type) return try canonical_type(state, ast.pointer_type(&state.tree, base).element) if (binding(state, base).kind == .builtin and binding(state, base).data.builtin == .string) return try builtin(state, id, .u8) return no_node } .slice: { view :: ast.slice_expr(&state.tree, id) base :: try expression_type(state, view.base, depth + 1) if (base == no_node) return no_node base_kind :: ast.expr_kind(&state.tree, base) element := no_node mutable := false if (base_kind == .array_type) { element = ast.array_type(&state.tree, base).element mutable = ast.array_type(&state.tree, base).mutable } if (base_kind == .pointer_type) { element = ast.pointer_type(&state.tree, base).element mutable = ast.pointer_type(&state.tree, base).mutable } if (base_kind == .slice_type) { element = ast.slice_type(&state.tree, base).element mutable = ast.slice_type(&state.tree, base).mutable } if (element == no_node) return no_node result :: try ast.add_slice_type(state.builder, ast.node_span(&state.tree, id), element, mutable, null) try refresh(state) return result } else: return no_node } } @hide:file parameter_index proc(state @State, function, id ast.NodeId) usize { for (0..ast.function_param_count(&state.tree, function)) |i| { if (ast.function_param(&state.tree, function, i) == id and ast.param(&state.tree, id).comptime) return i } return no_index } @hide:file infer_value proc(state @mut State, function, pattern ast.NodeId, actual Value, arguments []mut Value, contextual bool) bool ! Error { resolved :: binding(state, pattern) if (resolved.kind == .declaration) { index :: parameter_index(state, function, resolved.data.declaration) if (index != no_index) { if (arguments[index].kind == .invalid) { arguments[index] = actual return true } if (contextual and arguments[index].kind == .type and actual.kind == .type) return true return try value_equal(state, arguments[index], actual) } } return try value_equal(state, try evaluate(state, pattern), actual) } @hide:file infer proc(state @mut State, function, pattern, actual ast.NodeId, arguments []mut Value, contextual bool) bool ! Error { resolved :: binding(state, pattern) if (resolved.kind == .declaration) { index :: parameter_index(state, function, resolved.data.declaration) if (index != no_index) { if (arguments[index].kind == .type) { expected :: try canonical_type(state, arguments[index].node) # a fixed slice type supplies borrowing/string context; ordinary # argument checking owns element, mutability and sentinel validation. if (expected != no_node and ast.expr_kind(&state.tree, expected) == .slice_type) return true } return try infer_value(state, function, pattern, Value{ kind = .type, node = actual }, arguments, contextual) } } # only normalize the actual type: the pattern may still contain unbound # parameters or a type-generator call whose arguments must be inferred. concrete :: try canonical_type(state, actual) if (concrete == no_node) return false kind :: ast.expr_kind(&state.tree, pattern) actual_kind :: ast.expr_kind(&state.tree, concrete) match kind { .pointer_type: { if (actual_kind != .pointer_type) return false return try infer(state, function, ast.pointer_type(&state.tree, pattern).element, ast.pointer_type(&state.tree, concrete).element, arguments, contextual) } .slice_type: { element := no_node if (actual_kind == .slice_type) element = ast.slice_type(&state.tree, concrete).element if (actual_kind == .array_type) element = ast.array_type(&state.tree, concrete).element if (element == no_node) return false return try infer(state, function, ast.slice_type(&state.tree, pattern).element, element, arguments, contextual) } .array_type: { if (actual_kind != .array_type) return false p :: ast.array_type(&state.tree, pattern) a :: ast.array_type(&state.tree, concrete) if (p.count) |count| { if (a.count == null or !try infer_value(state, function, count, try evaluate(state, a.count?), arguments, false)) return false } return try infer(state, function, p.element, a.element, arguments, contextual) } .optional_type: { element :: if (actual_kind == .optional_type) ast.type_operand(&state.tree, concrete) else concrete return try infer(state, function, ast.type_operand(&state.tree, pattern), element, arguments, contextual) } .fallible_type: { if (actual_kind != .fallible_type) return false p :: ast.binary_expr(&state.tree, pattern) a :: ast.binary_expr(&state.tree, concrete) return (try infer(state, function, p.left, a.left, arguments, contextual) and try infer(state, function, p.right, a.right, arguments, contextual)) } .function_type: { if (actual_kind != .function_type) return false count :: ast.function_param_count(&state.tree, pattern) if (count != ast.function_param_count(&state.tree, concrete)) return false for (0..count) |i| { p :: ast.param(&state.tree, ast.function_param(&state.tree, pattern, i)).type a :: ast.param(&state.tree, ast.function_param(&state.tree, concrete, i)).type if (!try infer(state, function, p, a, arguments, contextual)) return false } return try infer(state, function, ast.callable_result(&state.tree, pattern), ast.callable_result(&state.tree, concrete), arguments, contextual) } .call: { callee :: binding(state, ast.call_expr(&state.tree, pattern).callee) if (callee.kind != .declaration) return false for (0..(state.instances.items.len)) |i| { instance :: state.instances.items[i] if (instance.type == no_node or instance.function != callee.data.declaration) continue if (try canonical_type(state, instance.type) != concrete) continue count :: ast.call_arg_count(&state.tree, pattern) if (count != instance.arguments.len) return false for (0..count) |j| { if (!try infer_value(state, function, ast.call_arg(&state.tree, pattern, j), instance.arguments[j], arguments, contextual)) return false } return true } return false } else: return true } } @hide:file infer_result proc( state @mut State, function, pattern, actual ast.NodeId, arguments []mut Value, ) bool ! Error { concrete :: try canonical_type(state, actual) if (concrete == no_node) return true if ( ast.expr_kind(&state.tree, concrete) == .optional_type and ast.expr_kind(&state.tree, pattern) != .optional_type ) { resolved :: binding(state, pattern) if (resolved.kind == .declaration) { index :: parameter_index(state, function, resolved.data.declaration) if (index != no_index) { if (arguments[index].kind == .invalid) return try infer(state, function, pattern, concrete, arguments, false) if (arguments[index].kind == .type and try same_type(state, arguments[index].node, concrete)) return true } } return try infer_result(state, function, pattern, ast.type_operand(&state.tree, concrete), arguments) } if ( ast.expr_kind(&state.tree, concrete) == .fallible_type and ast.expr_kind(&state.tree, pattern) != .fallible_type ) return try infer_result(state, function, pattern, ast.binary_expr(&state.tree, concrete).left, arguments) if (ast.expr_kind(&state.tree, pattern) == .fallible_type) { view :: ast.binary_expr(&state.tree, pattern) if (ast.expr_kind(&state.tree, concrete) != .fallible_type) return try infer_result(state, function, view.left, concrete, arguments) channels :: ast.binary_expr(&state.tree, concrete) return (try infer_result(state, function, view.left, channels.left, arguments) and try infer_result(state, function, view.right, channels.right, arguments)) } return try infer(state, function, pattern, concrete, arguments, false) } @hide:file materialize proc(state @mut State, value Value) Value ! Error { result := value span :: ast.node_span(&state.tree, value.node) if (value.kind == .type and nominal_type_value(state, value.node)) { # refer to declared nominal types, rather than reusing their definition as # an annotation and turning its value-role checker cell into an annotation. for (state.declarations.items) |declaration| { if ( ast.node_class(&state.tree, declaration) == .statement and ast.statement_decl(&state.tree, declaration).initializer == value.node ) { result.node = try reference(state, value.node, declaration) return result } } } if (value.kind == .integer) { result.node = try ast.add_integer(state.builder, span, value.magnitude) if (value.negative) result.node = try ast.add_unary(state.builder, span, .negate, result.node) } else if (value.kind == .boolean) { result.node = try ast.add_boolean(state.builder, span, value.boolean) } else if (value.kind == .real) { result.node = try ast.add_float(state.builder, span, ptrcast!(@f64, &value.magnitude)^) } try refresh(state) return result } @hide:file valid_value proc(state @mut State, annotation ast.NodeId, value Value) bool ! Error { type :: try canonical_type(state, annotation) if (type == no_node) return false resolved :: binding(state, type) if (resolved.kind == .builtin) { kind :: resolved.data.builtin if (kind == .bool) return value.kind == .boolean if (kind == .string) return value.kind == .string if (kind == .float or kind == .f64) return value.kind == .real if (kind == .void) return ( value.kind == .aggregate and value.annotation != no_node and try same_type(state, type, value.annotation) ) if (value.kind != .integer) return false bits := u32(64) signed := false match kind { .i8, .c_char: { bits = 8 signed = true } .i16, .c_short: { bits = 16 signed = true } .i32, .c_int: { bits = 32 signed = true } .int, .i64, .isize, .c_long, .c_longlong: signed = true .u8, .c_uchar: bits = 8 .u16, .c_ushort: bits = 16 .u32, .c_uint: bits = 32 .u64, .uint, .usize, .c_ulong, .c_ulonglong: {} else: return false } limit := maxval!(u64) if (signed) { limit = u64(1) << (bits - 1) if (!value.negative) limit -= 1 } else { if (value.negative) return false if (bits != 64) limit = (u64(1) << bits) - 1 } return value.magnitude <= limit } kind :: ast.expr_kind(&state.tree, type) if (value.kind == .aggregate) return value.annotation != no_node and try same_type(state, type, value.annotation) if (kind == .optional_type) return value.kind == .null or try valid_value(state, ast.type_operand(&state.tree, type), value) if (kind == .slice_type and value.kind == .string) { view :: ast.slice_type(&state.tree, type) child :: try canonical_type(state, view.element) return ( !view.mutable and child != no_node and binding(state, child).kind == .builtin and binding(state, child).data.builtin == .u8 ) } if (kind == .function_type and value.kind == .function) { actual :: try expression_type(state, value.node, 0) return actual != no_node and try same_type(state, type, actual) } if (kind == .distinct_type) return try valid_value(state, ast.type_operand(&state.tree, type), value) return false } @hide:file enum_index proc($T type, value T) usize { match typeinfo!(T) { .enum |info|: { inline for (info.fields) |field| { if (value == field!(T, field.name)) return field.index } unreachable } else: unreachable } } @hide:file remember proc(state @mut State, target ast.NodeId) void ! mem.AllocError { if (usize(target) >= state.original_count) return saved :: try ast.copy_node(state.builder, target) try arraylist.append(&state.rewrites.changes, Rewrite{ target = target, saved = saved }) } @hide:file replace proc(state @mut State, target, source ast.NodeId) void ! mem.AllocError { try remember(state, target) ast.replace_node(state.builder, target, source) } @hide:file builtin_identities :: $enummap.init_default( sema.BuiltinType, sema.BuiltinType, sema.BuiltinType.type, { i64 = .int, c_long = .int, c_longlong = .int, f64 = .float, uint = .u64, c_ulong = .u64, c_ulonglong = .u64, c_char = .i8, c_uchar = .u8, c_short = .i16, c_ushort = .u16, c_int = .i32, c_uint = .u32, }) @hide:file builtin_identity proc(kind sema.BuiltinType) sema.BuiltinType { if (enummap.get(&builtin_identities, kind)) |identity| return identity return kind } @hide:file integer_type proc(state @mut State, annotation ast.NodeId) bool ! Error { type :: try canonical_type(state, annotation) if (type == no_node) return false resolved :: binding(state, type) if (resolved.kind == .builtin) { return match builtin_identity(resolved.data.builtin) { .int, .i8, .i16, .i32, .isize, .u8, .u16, .u32, .u64, .usize: true else: false } } if (ast.expr_kind(&state.tree, type) == .distinct_type) return try integer_type(state, ast.type_operand(&state.tree, type)) return false } @hide:file integer_value proc(state @mut State, id ast.NodeId, magnitude u64, negative bool, annotation ast.NodeId) Value ! Error { result :: Value{ kind = .integer, node = id, magnitude = magnitude, negative = negative, annotation = annotation } if (annotation != no_node and !try valid_value(state, annotation, result)) return Value{} return result } @hide:file same_string proc(state @State, left, right ast.NodeId) bool ! Error { a :: ast.literal_text(&state.tree, left) b :: ast.literal_text(&state.tree, right) ac := usize(1) bc := usize(1) while (ac < a.len - 1 and bc < b.len - 1) { av :: ast.string_next(a, &ac) catch { return false } bv :: ast.string_next(b, &bc) catch { return false } if (av != bv) return false } return ac == a.len - 1 and bc == b.len - 1 } @hide:file flexible proc(state @State, id ast.NodeId) bool { kind :: ast.expr_kind(&state.tree, id) if (kind == .literal_int or kind == .literal_float) return true if (kind == .unary) { view :: ast.unary_expr(&state.tree, id) return (view.operator == .negate or view.operator == .bit_not) and flexible(state, view.operand) } if (kind == .binary) { view :: ast.binary_expr(&state.tree, id) return ( (view.operator == .add or view.operator == .subtract or view.operator == .multiply or view.operator == .divide or view.operator == .bit_and or view.operator == .bit_or or view.operator == .bit_xor or view.operator == .shift_left or view.operator == .shift_right or view.operator == .shift_left_saturating) and flexible(state, view.left) and flexible(state, view.right) ) } return false } @hide:file template proc(state @State, id ast.NodeId) bool { return ast.function_generic(&state.tree, id) or compiletime_call(state, id) }