import "@std/mem" import "@std/arraylist" import "@std/hashmap" 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 @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 } @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 bindings arraylist.ArrayList(sema.Binding) declarations arraylist.ArrayList(ast.NodeId) instances arraylist.ArrayList(Instance) cache hashmap.AutoHashMap(usize, usize) expanded arraylist.ArrayList(u8) 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 } #! 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, 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) needed = true if (class == .function and ast.function_generic(&original, id)) needed = true if (class == .function) { result_type :: sema.binding(resolution, ast.function_decl(&original, id).return_type) if (result_type.kind == .builtin and result_type.data.builtin == .type) 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) state := State{ allocator = allocator, builder = builder, tree = ast.view(builder, root), bindings = arraylist.init(allocator), declarations = arraylist.init(allocator), instances = arraylist.init(allocator), cache = hashmap.init(allocator), expanded = arraylist.init(allocator), diagnostics = diagnostic_store, original_count = ast.node_count(&original), rewrites = &result, mapping = mapping, resolution = resolution, } defer arraylist.deinit(&state.bindings) defer arraylist.deinit(&state.declarations) defer arraylist.deinit(&state.expanded) defer hashmap.deinit(&state.cache) 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))) for (0..ast.package_decl_count(&state.tree)) |i| { id :: ast.package_decl(&state.tree, i) if (ast.decl_kind(&state.tree, id) == .function and template(&state, id)) continue try arraylist.append(&state.declarations, id) } index := 0 while (index < state.declarations.items.len) { try expand(&state, state.declarations.items[index]) index += 1 } 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 proc(state @mut State, id ast.NodeId) void ! Error { try refresh(state) if (state.expanded.items[usize(id)] != 0) return state.expanded.items[usize(id)] = 1 class :: ast.node_class(&state.tree, id) 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) ) return } if ( class == .expression and ast.expr_kind(&state.tree, id) == .match_expr and try specialize_match(state, id, no_node) ) 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) { operand :: ast.comptime_operand(&state.tree, id) try arraylist.append(&state.rewrites.constraints, operand) state.evaluation_diagnostic = diagnostics.no_diagnostic value :: try evaluate(state, 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) == .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) ) { try specialize_call(state, id, resolved.data.declaration) 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 == .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) } } } } # 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 ast.NodeId) bool ! Error { subject :: ast.match_expr(&state.tree, id).subject explicit :: ast.expr_kind(&state.tree, subject) == .comptime_expr if (!explicit and usize(id) < state.original_count) return false try expand(state, subject) if (!explicit 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 or (statement == no_node and !arm.expression)) { try reject_evaluation(state, id) return true } try expand(state, selected) if (statement != no_node) { replacement := no_node if (arm.expression) replacement = try ast.add_expression_stmt(state.builder, selected) else replacement = try ast.add_block_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 } # only probe side-effect-free expressions. ordinary calls stay runtime unless the # source requests evaluation with $(); probing must not execute FFI or emit traps. @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: return true .typed_constant: return branch_constant(state, ast.typed_constant(&state.tree, id).value, depth + 1) .enum_literal: return true .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) 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: return true 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 } @hide:file specialize_call proc(state @mut State, call, function 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) # a type result has no runtime call: every input participates in specialization. type_result :: is_type(state, ast.function_decl(&state.tree, function).return_type) runtime := usize(0) for (0..count) |i| { if (!type_result and !ast.param(&state.tree, ast.function_param(&state.tree, function, i)).comptime) runtime += 1 } supplied :: ast.call_arg_count(&state.tree, call) explicit :: supplied == count if (!explicit and (type_result or supplied != runtime)) { 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) position := 0 for (0..count) |i| { parameter :: ast.function_param(&state.tree, function, i) view :: ast.param(&state.tree, parameter) if (view.comptime and !explicit) continue argument :: ast.call_arg(&state.tree, call, position) position += 1 try expand(state, argument) if (type_result or view.comptime) { 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) 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 } } else { try arraylist.append(&runtime_arguments, argument) } } if (!explicit) { for (0..usize(2)) |phase| { 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 :: flexible(state, argument) if (contextual != (phase == 1)) continue actual :: try expression_type(state, argument, 0) if (actual == no_node or !try infer(state, function, view.type, actual, arguments, 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 } } key := usize(function) 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 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 }) 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) { try reject(state, call, .specialization_cycle) return } if (explicit) { signature :: state.instances.items[instance].signature for (0..count) |i| { parameter :: ast.function_param(&state.tree, signature, i) view :: ast.param(&state.tree, parameter) if ((!type_result and !view.comptime) or is_type(state, view.type)) continue original :: ast.call_arg(&state.tree, call, i) constraint :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, original), view.type, original) try refresh(state) try arraylist.append(&state.rewrites.constraints, constraint) } } callee :: try reference(state, call, target) replacement := no_node 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 :: is_type(state, ast.function_decl(&state.tree, function).return_type) 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 (is_type(state, view.return_type)) { 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)) if (returned == no_node) { try reject_type_result(state, call, returned, constraints_start) 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 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) return kind == .record_literal or kind == .array_literal or kind == .enum_literal } @hide:file argument_annotation proc( state @mut State, function, annotation ast.NodeId, arguments []Value, ) ast.NodeId ! Error { 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]) 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 { if (!is_type(state, annotation)) { type :: try canonical_type(state, annotation) if (type != no_node and (contextual_literal(state, id) or ast.expr_kind(&state.tree, type) == .fallible_type)) return try evaluate_ir(state, id, type) } return try evaluate(state, id) } # values leaving the temporary interpreter must own no interpreter addresses or module IDs. # rebuild contextual source literals while the module and its escaped flat cells are alive. @hide:file serialize_value proc( state @mut State, module @ir.Module, checked @typecheck.Result, type typecheck.Type, value ir.Value, span ast.Span, ) ast.NodeId ! Error { result := no_node 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, array.element, child, span) 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) # untagged storage has no active member; choosing one could discard bytes. if (record.is_union and record.tag_type.kind != .enumeration) return no_node 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) { result = try ast.add_enum_literal(state.builder, span, field.name?) try refresh(state) return result } child :: ir.aggregate_item(module, value, i) node :: try serialize_value(state, module, checked, field.type, child, span) if (node == no_node) return no_node generated :: try ast.add_field(state.builder, span, field.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, channel, child, span) 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) { result = try ast.add_enum_literal(state.builder, span, member.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) return no_node result = try ast.add_literal(state.builder, .null, span) } 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) return try serialize_value(state, module, checked, container.element, child, span) } } 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) { a :: ast.typed_constant(&state.tree, left) b :: ast.typed_constant(&state.tree, right) return try same_type(state, a.type, b.type) and try constant_equal(state, a.value, b.value) } 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 == .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) { value :: ast.typed_constant(&state.tree, id) return mix(mix(hash, try type_hash(state, value.type)), try constant_hash(state, value.value)) } 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 == .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| try arraylist.append(&diagnostic_store.list, diagnostic) original_diagnostics :: diagnostics.count(&diagnostic_store) checked := try typecheck.check(state.allocator, &tree, state.resolution, &diagnostic_store) defer typecheck.deinit(&checked) if (diagnostics.count(&diagnostic_store) != original_diagnostics) { diagnostic :: diagnostic_store.list.items[original_diagnostics] state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, diagnostic.code, diagnostic.span) return Value{} } module := try lower.lower_evaluation(state.allocator, &tree, state.resolution, &checked, wrapper, &diagnostic_store) defer ir.deinit(&module) outcome := try interpreter.run_evaluation(state.allocator, &module, &diagnostic_store) defer interpreter.deinit(&outcome) if (outcome.trap) |trap| { if (usize(trap) < original_diagnostics) state.evaluation_diagnostic = trap else { diagnostic :: diagnostics.get(&diagnostic_store, trap)? state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, diagnostic.code, diagnostic.span) } return Value{} } value :: outcome.value source_type :: typecheck.type_of(&checked, result_type) node :: try serialize_value(state, &module, &checked, source_type, value, span) if (node == no_node) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span) return Value{} } if (ir.aggregate(value.type) 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 } @hide:file TypeExecution :: struct { locals arraylist.ArrayList(TypeLocal) mapping []mut ast.NodeId first_owned ast.NodeId returned ast.NodeId = no_node loop_depth usize = 0 } @hide:file TypeFlow :: enum { next, returned, break, continue, invalid } # 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 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, } defer arraylist.deinit(&execution.locals) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) if (try execute_type_block(state, &execution, block) != .returned) return no_node return execution.returned } @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 { if (usize(id) >= execution.mapping.len) 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) { execution.mapping[usize(id)] = local.value return } } } for (0..ast.child_count(&state.tree, id)) |i| 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 { 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 for (0..ast.child_count(&state.tree, id)) |i| bind_type_snapshot(state, execution, ast.child(&state.tree, id, i), start) } @hide:file type_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error { prepare_type_snapshot(state, execution, id) start :: ast.NodeId(ast.node_count(&state.tree)) result :: try ast.clone_subtree(state.builder, id, execution.mapping) try refresh(state) bind_type_snapshot(state, execution, id, start) return result } @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 } # 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 { 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) 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 } } if ( i > 0 and arm.expression != ast.match_arm_view( &state.tree, ast.match_arm(&state.tree, id, 0), ).expression ) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, ast.node_span(&state.tree, arm_id)) 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) } span :: ast.node_span(&state.tree, arm_id) index :: try ast.add_integer(state.builder, span, u64(i)) selector :: try ast.add_match_arm(state.builder, span, patterns.items, arm.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), ast.match_expr(&state.tree, id).subject, arms.items) 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 annotation :: try expression_type(state, subject, 0) value :: 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)) for (0..ast.record_field_count(&state.tree, annotation)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, annotation, i)) if ( field.name != null and payload.name != null and ast.name_id(&state.tree, field.name?) == ast.name_id(&state.tree, payload.name?) ) { 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 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), } defer arraylist.deinit(&execution.locals) defer mem.free(state.allocator, execution.mapping) mem.set(execution.mapping, no_node) try arraylist.append(&execution.locals, TypeLocal{ declaration = arm.capture?, value = value, annotation = ast.typed_constant(&state.tree, value).type, }) return try type_snapshot(state, &execution, arm.body) } @hide:file type_value_expression proc(state @mut State, input ast.NodeId) ast.NodeId ! Error { 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)) if (!arm.expression) return no_node expression = try type_match_body(state, expression, arm) if (expression == no_node) return no_node } return expression } # normalize only selected expression arms before ordinary expansion can visit them. # 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 (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 == .index or kind == .selection) { base :: if (kind == .index) ast.index_expr(&state.tree, id).base else ast.selection_expr(&state.tree, id).base if (ast.expr_kind(&state.tree, base) != .typed_constant) return true array :: ast.typed_constant(&state.tree, base) if (!try type_array_storage(state, array.type)) return true selected := no_node if (kind == .index) { index :: try type_array_index(state, ast.index_expr(&state.tree, id).index, ast.array_element_count(&state.tree, array.value)) if (index == no_index) return false selected = ast.array_element(&state.tree, array.value, index) } else { member :: ast.selection_expr(&state.tree, id).member if (!mem.eql(ast.source_text(&state.tree, ast.node_span(&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) } # 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 arrays are persistent AST values, never runtime storage. keep the same # typed-constant snapshots as ordinary aggregates, but validate their leaves here. @hide:file type_array_storage proc(state @mut State, annotation ast.NodeId) bool ! Error { current := annotation for (0..128) |_| { if (is_type(state, current)) return current != annotation current = try canonical_type(state, current) if (current == no_node or ast.expr_kind(&state.tree, current) != .array_type) return false current = ast.array_type(&state.tree, current).element } return false } @hide:file type_array_index proc(state @mut State, expression ast.NodeId, count usize) 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)) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .index_out_of_bounds, 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 false 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) } @hide:file type_array_value proc(state @mut State, expression, annotation ast.NodeId) ast.NodeId ! Error { 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 } 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) 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 } } 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 } state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, ast.node_span(&state.tree, expression)) return no_node } # rebuild only the indexed 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 index; rebuild its child before copying this parent. first := target while (true) { if (ast.expr_kind(&state.tree, first) != .index) { state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .invalid_assignment_target, ast.node_span(&state.tree, target)) return no_node } base :: ast.index_expr(&state.tree, first).base if (base == root) break first = base } array :: ast.typed_constant(&state.tree, current) descriptor :: ast.array_type(&state.tree, array.type) if (!descriptor.mutable) { 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) if (index == no_index) return no_node previous :: ast.array_element(&state.tree, array.value, index) updated := no_node if (first == target) updated = try type_execution_value(state, input, descriptor.element) else 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 :: 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 :: 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 } @hide:file type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.NodeId ! Error { if (annotation == no_node) return no_node 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 }) 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 if (try type_array_storage(state, annotation)) return try type_array_value(state, expression, annotation) try expand(state, expression) resolved :: binding(state, expression) if (resolved.kind == .invalid) { state.evaluation_diagnostic = resolved.data.diagnostic return no_node } if (is_type(state, annotation)) { 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) 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 } value :: try evaluate_ir(state, expression, 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{} } 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) 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 } @hide:file execute_type_block proc(state @mut State, execution @mut TypeExecution, block ast.NodeId) TypeFlow ! Error { saved :: execution.locals.items.len defer execution.locals.items = execution.locals.items[..saved] 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 execute_type_statement(state, execution, statement) if (flow != .next) return flow } return .next } @hide:file execute_type_statement proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error { match ast.stmt_kind(&state.tree, statement) { .return_stmt: { if (ast.return_stmt(&state.tree, statement).value) |value| { expression :: try type_snapshot(state, execution, value) execution.returned = try type_execution_value(state, expression, try builtin(state, expression, .type)) if (execution.returned != no_node) return .returned } return .invalid } .declaration: { declaration :: ast.statement_decl(&state.tree, statement) snapshot :: try type_snapshot(state, execution, declaration.initializer) expression :: try type_value_expression(state, snapshot) if (expression == no_node) return .invalid annotation := no_node if (declaration.type) |type| annotation = try type_snapshot(state, execution, type) else if (nominal_type_value(state, expression)) annotation = try builtin(state, expression, .type) else { 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, }) } .assignment_stmt: { assignment :: ast.assignment_stmt(&state.tree, statement) root := assignment.target while (true) { kind :: ast.expr_kind(&state.tree, root) if (kind == .selection and binding(state, root).kind == .none) 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 (try type_array_storage(state, execution.locals.items[index].annotation)) { 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(state, execution, assignment.value) value :: try type_execution_value(state, expression, execution.locals.items[index].annotation) if (value == no_node) return .invalid execution.locals.items[index].value = value } .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 (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) 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 == .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) return .invalid snapshot :: try type_snapshot(state, execution, expression) 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 try arraylist.append(&execution.locals, TypeLocal{ declaration = capture, value = value, annotation = ast.typed_constant(&state.tree, value).type, }) } if (!arm.expression) return try execute_type_block(state, execution, arm.body) 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 an array 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) 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 collection :: try type_execution_value(state, snapshot, annotation) if (collection == no_node) return .invalid count := Value{} if (try type_array_storage(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) 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 (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 :: try ast.add_index(state.builder, span, current, index) try refresh(state) element_type :: 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 == .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) 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 == .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 (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 == .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 { .typed_constant: { view :: ast.typed_constant(&state.tree, id) 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, .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) 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) .record_literal, .array_literal, .selection, .index, .match_expr, .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)) 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| { 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 } @hide:file same_type 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 (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 { .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(state, p.element, q.element) } .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(state, p.element, q.element) } .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(state, p.element, q.element) } .optional_type: return try same_type(state, ast.type_operand(&state.tree, a), ast.type_operand(&state.tree, b)) .fallible_type: { p :: ast.binary_expr(&state.tree, a) q :: ast.binary_expr(&state.tree, b) return try same_type(state, p.left, q.left) and try same_type(state, p.right, q.right) } .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(state, ast.callable_result(&state.tree, a), ast.callable_result(&state.tree, b))) 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(state, p, q)) 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 { 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 { .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(state, view.element)) } .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(state, view.element)) } .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(state, view.element)) } .optional_type: return mix(hash, try type_hash(state, ast.type_operand(&state.tree, node))) .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(state, view.left) | try type_hash(state, view.right) } .fallible_type: { view :: ast.binary_expr(&state.tree, node) return mix(mix(hash, try type_hash(state, view.left)), try type_hash(state, view.right)) } .function_type: { hash = mix(hash, try type_hash(state, ast.callable_result(&state.tree, node))) for (0..ast.function_param_count(&state.tree, node)) |i| { parameter :: ast.function_param(&state.tree, node, i) hash = mix(hash, try type_hash(state, ast.param(&state.tree, parameter).type)) } return hash } else: return mix(hash, usize(node)) } } @hide:file expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! Error { 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 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, .distinct_type, .fallible_type, .sum_type, .alias_expr: return try builtin(state, id, .type) .typed_constant: return try canonical_type(state, ast.typed_constant(&state.tree, id).type) .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) .catch_expr: { type :: try expression_type(state, ast.catch_expr(&state.tree, id).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 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 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) 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 (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.source_text(&state.tree, ast.node_span(&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 (base_kind != .record_type and base_kind != .union_type) return no_node for (0..ast.record_field_count(&state.tree, base)) |i| { field :: ast.field(&state.tree, ast.record_field(&state.tree, base, i)) if (field.name) |name| { if (ast.name_id(&state.tree, name) == ast.name_id(&state.tree, view.member)) return try canonical_type(state, field.type?) } } return no_node } .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 and parameter_index(state, function, resolved.data.declaration) != no_index) return try infer_value(state, function, pattern, Value{ kind = .type, node = actual }, arguments, contextual) kind :: ast.expr_kind(&state.tree, pattern) actual_kind :: ast.expr_kind(&state.tree, actual) 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, actual).element, arguments, contextual) } .slice_type: { element := no_node if (actual_kind == .slice_type) element = ast.slice_type(&state.tree, actual).element if (actual_kind == .array_type) element = ast.array_type(&state.tree, actual).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, actual) 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, actual) else actual return try infer(state, function, ast.type_operand(&state.tree, pattern), element, 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) != actual) 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 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_identity proc(kind sema.BuiltinType) sema.BuiltinType { return match kind { .i64, .c_long, .c_longlong: .int .f64: .float .uint, .c_ulong, .c_ulonglong: .u64 .c_char: .i8 .c_uchar: .u8 .c_short: .i16 .c_ushort: .u16 .c_int: .i32 .c_uint: .u32 else: 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 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) 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 is_type(state, ast.function_decl(&state.tree, id).return_type) }